Skip to content

Continuing Bubble Implementation#5928

Open
rapterjet2004 wants to merge 1 commit into
masterfrom
issue-3385-arlex-attempt
Open

Continuing Bubble Implementation#5928
rapterjet2004 wants to merge 1 commit into
masterfrom
issue-3385-arlex-attempt

Conversation

@rapterjet2004
Copy link
Copy Markdown
Contributor

@rapterjet2004 rapterjet2004 commented Mar 4, 2026


ToDo

  • refactor conversationInfoActivity after the rebase on master

Changes

  • Refactoring NotificationUtils.createConversationBubble to use kotlin flows and best practices
  • Refactoring NotificationWorker.addBubble for clarity and reducing nesting, using the return-if-error technique, and the proper usage of the builder pattern for all functions related to the bubble feature
  • Added better error handling

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

@rapterjet2004 rapterjet2004 self-assigned this Mar 4, 2026
@rapterjet2004 rapterjet2004 added the 2. developing Work in progress label Mar 4, 2026
@rapterjet2004 rapterjet2004 force-pushed the issue-3385-arlex-attempt branch from 56dcd8e to 90a4fa3 Compare March 9, 2026 14:53
}

// Use the same notification ID calculation as NotificationWorker
notificationManager.notify(notificationId, notification)

Check failure

Code scanning / CodeQL

Use of implicit PendingIntents High

An implicit Intent is created
and sent to an unspecified third party through a PendingIntent.

Copilot Autofix

AI 3 months ago

General approach: Ensure all PendingIntents used with notifications or bubbles are (a) based on explicit Intents and (b) created with FLAG_IMMUTABLE where mutation is not required. In particular, avoid FLAG_MUTABLE unless there is a concrete need.

Best fix here: The contentIntent used in the notification is already explicit and immutable, so it is fine. The only remaining mutable PendingIntent is bubbleIntent:

val bubbleIntent = android.app.PendingIntent.getActivity(
    context,
    bubbleRequestCode,
    BubbleActivity.newIntent(context, bubbleInfo.roomToken, data.conversationName),
    android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_MUTABLE
)

The BubbleActivity.newIntent(...) factory is expected to return an explicit Intent targeting BubbleActivity, and there is no indication that any caller needs to modify this PendingIntent. We can therefore safely switch this to FLAG_IMMUTABLE. This keeps existing behavior (the Intent content is still the same and FLAG_UPDATE_CURRENT is preserved) while eliminating mutability so the analyzer’s concern is addressed.

Concretely:

  • In NotificationUtils.kt, in the createBubbleNotification function around lines 692–701, change the flags used when creating bubbleIntent from FLAG_UPDATE_CURRENT or FLAG_MUTABLE to FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE.
  • No other code or imports need to be changed; we only adjust the flags.
Suggested changeset 1
app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
--- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
@@ -697,7 +697,7 @@
             context,
             bubbleRequestCode,
             BubbleActivity.newIntent(context, bubbleInfo.roomToken, data.conversationName),
-            android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_MUTABLE
+            android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
         )
 
         val contentIntent = android.app.PendingIntent.getActivity(
EOF
@@ -697,7 +697,7 @@
context,
bubbleRequestCode,
BubbleActivity.newIntent(context, bubbleInfo.roomToken, data.conversationName),
android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_MUTABLE
android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
)

val contentIntent = android.app.PendingIntent.getActivity(
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flag mutable is required for bubbles to work, and shouldn't be a security risk, as the content intent flags are set to IMMUTABLE

@rapterjet2004 rapterjet2004 added 3. to review Waiting for reviews and removed 2. developing Work in progress labels Mar 11, 2026
@sowjanyakch

This comment was marked as resolved.

@rapterjet2004 rapterjet2004 force-pushed the issue-3385-arlex-attempt branch from a90d336 to 315ee3b Compare March 16, 2026 18:04
@github-actions
Copy link
Copy Markdown
Contributor

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/5928.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.

@mahibi
Copy link
Copy Markdown
Collaborator

mahibi commented Mar 18, 2026

i wanted to review but #5979 happens to me again

@mahibi
Copy link
Copy Markdown
Collaborator

mahibi commented Mar 25, 2026

giving it another try.
Somehow i don't get bubbled conversations anymore when i change it in the settings.
Only when i go to a chat and explicitly select "Create bubble", then a bubble is created.
But as soon as i get a push, the bubble disappears instead to show the notification.

@rapterjet2004 does it work for you as expected?

@mahibi mahibi force-pushed the issue-3385-arlex-attempt branch from 2826d6a to 933a46b Compare April 2, 2026 14:37
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 2, 2026

APK file: https://github.com/nextcloud/talk-android/actions/runs/23905922700/artifacts/6243947187
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@mahibi

This comment was marked as outdated.

This was referenced Apr 2, 2026
@rapterjet2004 rapterjet2004 force-pushed the issue-3385-arlex-attempt branch 4 times, most recently from 6ed3553 to 87c5130 Compare April 2, 2026 16:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 3, 2026

APK file: https://github.com/nextcloud/talk-android/actions/runs/23946962562/artifacts/6259706638
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 3, 2026

APK file: https://github.com/nextcloud/talk-android/actions/runs/23948011150/artifacts/6260084009
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@rapterjet2004 rapterjet2004 force-pushed the issue-3385-arlex-attempt branch from 1a3fb2f to d174829 Compare April 8, 2026 14:54
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

APK file: https://github.com/nextcloud/talk-android/actions/runs/24193631626/artifacts/6351236070
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@rapterjet2004 rapterjet2004 force-pushed the issue-3385-arlex-attempt branch from c185810 to 1fd84eb Compare April 10, 2026 14:15
@github-actions
Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/24247358242/artifacts/6372834294
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@github-actions
Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/24249183788/artifacts/6373597825
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@AndyScherzinger AndyScherzinger requested a review from Copilot April 10, 2026 19:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues the conversation “bubble” feature implementation by adding user-facing settings (global + per-conversation) and wiring bubble creation/maintenance into notification handling and chat UI flows.

Changes:

  • Adds bubble-related strings, menu items, and settings rows in Settings and Conversation Info screens.
  • Introduces bubble preference persistence (global enable/force + per-conversation) and navigation helpers to focus relevant settings.
  • Refactors/extends notification code to support bubble notifications, shortcuts, and bubble lifecycle management.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
app/src/main/res/values/strings.xml Adds UI strings for bubble settings and actions.
app/src/main/res/menu/menu_conversation.xml Adds menu actions to create a bubble / open in app.
app/src/main/res/layout/item_notification_settings.xml Adds per-conversation bubble toggle row.
app/src/main/res/layout/activity_settings.xml Adds global bubble enable + “force all” settings rows.
app/src/main/res/layout/activity_conversation_info.xml Adds an ID for scrolling/highlighting bubble setting.
app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt Stores/reads global bubble preferences.
app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java Exposes global bubble preferences in the interface.
app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt Adds bubble capability checks, bubble dismissal helpers, bubble icon loading, and bubble notification creation.
app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt Adds intent extras for focusing bubble settings in UI.
app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt Implements bubble settings UI + navigation to system bubble settings.
app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt Changes multi-notification IDs container type.
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt Refactors chat notification building and adds bubble metadata handling.
app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt Adds per-conversation bubble toggle behavior + highlight/scroll support.
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt Adds “Create bubble” menu action and flow to guide users to enable settings.
app/src/main/java/com/nextcloud/talk/chat/BubbleActivity.kt Adds a bubble-hosted chat activity variant with “open in app”.
app/src/main/AndroidManifest.xml Registers BubbleActivity with embedded/resizeable/document settings.
app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt Updates DAO test calls to match updated DAO signature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1718 to +1719
private const val APP_NOTIFICATION = $$"com.android.settings.Settings$AppBubbleNotificationSettingsActivity"
private const val BUBBLE_NOTIFICATION = $$"com.android.settings.Settings$BubbleNotificationSettingsActivity"
Comment on lines 2786 to +2788

menu.findItem(R.id.create_conversation_bubble)?.isVisible = NotificationUtils.deviceSupportsBubbles

Comment on lines +101 to +117
val deviceSupportsBubbles = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q

fun areSystemBubblesEnabled(context: Context): Boolean {
if (!deviceSupportsBubbles) {
return false
}

return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Settings.Secure.getInt(
context.contentResolver,
"notification_bubbles",
1
) == 1
} else {
// Android 10 (Q) — bubbles always enabled
true
}
Comment on lines +120 to +127
fun isSystemBubblePreferenceAll(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return false
}

val notificationManager = context.getSystemService(NotificationManager::class.java)
return notificationManager?.bubblePreference == NotificationManager.BUBBLE_PREFERENCE_ALL
}
val channel = NotificationChannel(notificationChannel.id, notificationChannel.name, importance).apply {
description = notificationChannel.description
enableLights(true)
lightColor = R.color.colorPrimary
Comment on lines +352 to 358
fun dismissBubblesWithoutExplicitSettings(context: Context?, conversationUser: User) {
dismissBubbles(context, conversationUser) { roomToken ->
!com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule(
conversationUser,
roomToken
).getBoolean("bubble_switch", false)
}
}

fun isNotificationVisible(context: Context?, notificationId: Int): Boolean {
val notificationManager = context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
.setShortLabel(fallbackConversationLabel)
.setLongLabel(fallbackConversationLabel)
.setIcon(bubbleIcon)
.setIntent(Intent(Intent.ACTION_DEFAULT))
@mahibi
Copy link
Copy Markdown
Collaborator

mahibi commented May 5, 2026

@rapterjet2004 could you have a look at the copilot review and rebase on master?

@rapterjet2004 rapterjet2004 force-pushed the issue-3385-arlex-attempt branch 2 times, most recently from 54da9ac to 656ef32 Compare May 21, 2026 15:54
- Refactoring createConversationBubble to use best practices, keeping ChatActivity.kt simple
- Refactoring NotificationWorker functions related to bubbling to now properly follow the builder pattern
- better error handling of edge cases
- reimplementing UI in jetpack compose after rebase

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
@mahibi mahibi force-pushed the issue-3385-arlex-attempt branch from 656ef32 to 35c9a7e Compare May 29, 2026 13:48
@mahibi
Copy link
Copy Markdown
Collaborator

mahibi commented May 29, 2026

I rebased on master again. Given the amount of changes and the amount of findings by AI reviews this won't be merged for v24.0. (Sorry that the reviews took so long @arlexTech . We will try to ship this soon after 24.0)

@rapterjet2004 could you take a look at the copilot review and fix if necessary?
Also at these points that claude suggested to me:

Issues

Bugs / Correctness

  1. autoCancelOnClick change affects TYPE_REMINDER as a side-effect
    NotificationWorker.kt:164-166
    val autoCancelOnClick = TYPE_RECORDING != pushMessage.type &&
    TYPE_CHAT != pushMessage.type &&
    TYPE_REMINDER != pushMessage.type // <— reminder has no bubble
    The comment says "Bubbles need the notification to stay alive," but reminder notifications don't have bubbles and never did. This makes reminder notifications sticky (non-auto-cancel, ongoing) on tap — a regression. It should only apply to TYPE_CHAT.

  2. focusBubbleSetting is never reset to false
    ConversationInfoActivity.kt:188 sets viewModel.setFocusBubble(true) once. The animateColorAsState in ConversationInfoScreen.kt:498 animates to primary when focusBubbleSetting = true, but nothing ever sets it back to false. The highlight border stays on the
    bubble row indefinitely after scrolling to it. There should be a LaunchedEffect in the Composable, or the ViewModel resets the flag after emitting it once (e.g. via a SharedFlow).

  3. BubbleActivity navigation inconsistency

  • Toolbar nav-icon click → openConversationList()
  • onSupportNavigateUp() (deprecated) → openInMainApp()

These do different things. The deprecated onSupportNavigateUp is also likely dead — the toolbar click listener overrides it. Both paths should do the same thing (probably openConversationList()), and the @deprecated override can be dropped.

  1. addBubbleMetadata re-checks shouldBubble internally
    NotificationWorker.kt:428-433 — addBubbleMetadata checks shouldBubble(it) internally, but all callers have already evaluated this condition and control whether the function is called at all. The internal guard creates a confusing double-check. The function
    should trust its callers or be redesigned so only one place makes the shouldBubble decision.

Code Quality

  1. BUBBLE_DESIRED_HEIGHT_PX is declared twice
    NotificationWorker.kt:621 defines private const val BUBBLE_DESIRED_HEIGHT_PX = 600
    NotificationUtils.kt:701 defines const val BUBBLE_DESIRED_HEIGHT_PX = 600
    The private one in NotificationWorker shadows the public one. Pick one location (the NotificationUtils constant, which is already const val) and reference it from NotificationWorker.

  2. handleExistingBubble is not private
    NotificationWorker.kt:218 — the extension function fun NotificationCompat.Builder.handleExistingBubble(...) is scoped inside NotificationWorker but not marked private. It should be private fun.

  3. focusBubbleSetting is never reset to false
    ConversationInfoActivity.kt:188 sets viewModel.setFocusBubble(true) once. The animateColorAsState in ConversationInfoScreen.kt:498 animates to primary when focusBubbleSetting = true, but nothing ever sets it back to false. The highlight border stays on the
    bubble row indefinitely after scrolling to it. There should be a LaunchedEffect in the Composable, or the ViewModel resets the flag after emitting it once (e.g. via a SharedFlow).

  4. BubbleActivity navigation inconsistency

  • Toolbar nav-icon click → openConversationList()
  • onSupportNavigateUp() (deprecated) → openInMainApp()

These do different things. The deprecated onSupportNavigateUp is also likely dead — the toolbar click listener overrides it. Both paths should do the same thing (probably openConversationList()), and the @deprecated override can be dropped.

  1. addBubbleMetadata re-checks shouldBubble internally
    NotificationWorker.kt:428-433 — addBubbleMetadata checks shouldBubble(it) internally, but all callers have already evaluated this condition and control whether the function is called at all. The internal guard creates a confusing double-check. The function
    should trust its callers or be redesigned so only one place makes the shouldBubble decision.

Code Quality

  1. BUBBLE_DESIRED_HEIGHT_PX is declared twice
    NotificationWorker.kt:621 defines private const val BUBBLE_DESIRED_HEIGHT_PX = 600
    NotificationUtils.kt:701 defines const val BUBBLE_DESIRED_HEIGHT_PX = 600
    The private one in NotificationWorker shadows the public one. Pick one location (the NotificationUtils constant, which is already const val) and reference it from NotificationWorker.

  2. handleExistingBubble is not private
    NotificationWorker.kt:218 — the extension function fun NotificationCompat.Builder.handleExistingBubble(...) is scoped inside NotificationWorker but not marked private. It should be private fun.

  3. NOTIFICATION_LEVEL_DEFAULT = 1 is unused
    ChatActivity.kt:4629 — added but never referenced anywhere in this diff or the surrounding code.

  4. launchIntentSafely throws NullPointerException deliberately
    SettingsActivity.kt:
    if (intent.resolveActivity(packageManager) == null) {
    throw NullPointerException("Intent to resolveActivity was Null!!!")
    }
    This is using exception flow control in an unusual way. Just return false directly:
    if (intent.resolveActivity(packageManager) == null) return@runCatching false

  5. Hardcoded string "notification_bubbles" in areSystemBubblesEnabled
    NotificationUtils.kt:723 — Settings.Secure.getInt(context.contentResolver, "notification_bubbles", ...). The constant Settings.Global.NOTIFICATION_BUBBLES exists from API 29. Should use that or define a named constant, not an inline magic string.

  6. dismissBubblesWithoutExplicitSettings creates a new DatabaseStorageModule per notification
    NotificationUtils.kt:356-361 — instantiating a new DatabaseStorageModule(conversationUser, roomToken) inside a scanNotifications loop (once per active notification) hits the database repeatedly. Should batch or use ArbitraryStorageManager directly.


Memory

  1. bubbleIconCache is never evicted
    NotificationUtils.kt:705 — ConcurrentHashMap<String, IconCompat>. Icons are added on every bubble notification but never removed. In a long-running app process with many conversations, this will grow without bound. Consider a size-bounded cache (LruCache) or
    clearing stale entries when bubbles are dismissed.

Minor

  1. resolveActivity package visibility on API 30+
    SettingsActivity.navigateToSystemBubbleSettings — intent.resolveActivity(packageManager) returns null for all intents on API 30+ unless the target package is declared in in AndroidManifest.xml. com.android.settings is a system package so it may be
    visible, but this is worth verifying.

  2. @Suppress("LongMethod", ...) added to NotificationWorker
    The suppression was added because the class and its methods grew. Not a blocker, but the class is already large and this signals the bubble logic could benefit from extraction into a dedicated helper/manager class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to "bubble" conversations

6 participants