From ddfade47aa028ff691a3d9e5b3e3a751b3aa5548 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:58:54 +1030 Subject: [PATCH 01/12] Implement bubble support --- .../FlutterLocalNotificationsPlugin.java | 26 +++++++++++++++++++ .../models/NotificationDetails.java | 3 +++ .../android/method_channel_mappers.dart | 1 + .../android/notification_details.dart | 3 +++ 4 files changed, 33 insertions(+) diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index 9ee5660cf..b50bc1c0f 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -415,6 +415,32 @@ protected static Notification createNotification( setProgress(notificationDetails, builder); setCategory(notificationDetails, builder); setTimeoutAfter(notificationDetails, builder); + + + if(notificationDetails.bubbleActivity != null) { + try { + Class cls = Class.forName(notificationDetails.bubbleActivity); + Intent testIntent = new Intent(context, cls); + int actionFlags = PendingIntent.FLAG_MUTABLE; + PendingIntent bubbleIntent = PendingIntent.getActivity(context, notificationDetails.id, testIntent, actionFlags); + + IconCompat icon = IconCompat.createWithResource(context, getDrawableResourceId(context, notificationDetails.icon)); + + if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { + + androidx.core.app.NotificationCompat.BubbleMetadata bubbleData = + new androidx.core.app.NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon) + .setDesiredHeight(600) + .build(); + + builder.setBubbleMetadata(bubbleData); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + Notification notification = builder.build(); if (notificationDetails.additionalFlags != null && notificationDetails.additionalFlags.length > 0) { diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java index 2f56f6ff5..217abca60 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java @@ -103,6 +103,7 @@ public class NotificationDetails implements Serializable { private static final String VISIBILITY = "visibility"; private static final String TICKER = "ticker"; + private static final String BUBBLE_ACTIVITY = "bubbleActivity"; private static final String SCHEDULE_MODE = "scheduleMode"; private static final String CATEGORY = "category"; private static final String TIMEOUT_AFTER = "timeoutAfter"; @@ -168,6 +169,7 @@ public class NotificationDetails implements Serializable { public Integer ledOnMs; public Integer ledOffMs; public String ticker; + public String bubbleActivity; public Integer visibility; @SerializedName(value = "scheduleMode", alternate = "allowWhileIdle") @@ -269,6 +271,7 @@ private static void readPlatformSpecifics( readLedInformation(notificationDetails, platformChannelSpecifics); readLargeIconInformation(notificationDetails, platformChannelSpecifics); notificationDetails.ticker = (String) platformChannelSpecifics.get(TICKER); + notificationDetails.bubbleActivity = (String) platformChannelSpecifics.get(BUBBLE_ACTIVITY); notificationDetails.visibility = (Integer) platformChannelSpecifics.get(VISIBILITY); if (platformChannelSpecifics.containsKey(SCHEDULE_MODE)) { notificationDetails.scheduleMode = diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart index f808d77b0..9c43e5105 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart @@ -209,6 +209,7 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'ledOnMs': ledOnMs, 'ledOffMs': ledOffMs, 'ticker': ticker, + 'bubbleActivity': bubbleActivity, 'visibility': visibility?.index, 'timeoutAfter': timeoutAfter, 'category': category?.name, diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart index bf7ffff8f..50ed21f4a 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart @@ -133,6 +133,7 @@ class AndroidNotificationDetails { this.ledOnMs, this.ledOffMs, this.ticker, + this.bubbleActivity, this.visibility, this.timeoutAfter, this.category, @@ -310,6 +311,8 @@ class AndroidNotificationDetails { /// Specifies the "ticker" text which is sent to accessibility services. final String? ticker; + final String? bubbleActivity; + /// The action to take for managing notification channels. /// /// Defaults to creating the notification channel using the provided details From 7e3ae5a5bff96ed74cbf4c5453618845b2c624c7 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:32:48 +1030 Subject: [PATCH 02/12] Add way to pass extra data to bubble intent --- .../FlutterLocalNotificationsPlugin.java | 16 ++++++++++++++-- .../models/NotificationDetails.java | 3 +++ .../android/method_channel_mappers.dart | 1 + .../android/notification_details.dart | 3 +++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index b50bc1c0f..97dccd0a4 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -421,11 +421,22 @@ protected static Notification createNotification( try { Class cls = Class.forName(notificationDetails.bubbleActivity); Intent testIntent = new Intent(context, cls); - int actionFlags = PendingIntent.FLAG_MUTABLE; - PendingIntent bubbleIntent = PendingIntent.getActivity(context, notificationDetails.id, testIntent, actionFlags); + testIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if(notificationDetails.bubbleExtra != null) { + final Bundle extra = new Bundle(); + extra.putString("bubbleExtra", notificationDetails.bubbleExtra); + testIntent.putExtras(extra); + } + + int actionFlags = PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; + PendingIntent bubbleIntent = PendingIntent.getActivity(context, notificationDetails.id, testIntent, actionFlags); IconCompat icon = IconCompat.createWithResource(context, getDrawableResourceId(context, notificationDetails.icon)); + Log.e(TAG, "Created pending intent: $bubbleIntent"); + Log.e(TAG, bubbleIntent.toString()); + if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { androidx.core.app.NotificationCompat.BubbleMetadata bubbleData = @@ -433,6 +444,7 @@ protected static Notification createNotification( .setDesiredHeight(600) .build(); + builder.setContentIntent(bubbleIntent); builder.setBubbleMetadata(bubbleData); } } catch (ClassNotFoundException e) { diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java index 217abca60..9928b8b71 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java @@ -104,6 +104,7 @@ public class NotificationDetails implements Serializable { private static final String TICKER = "ticker"; private static final String BUBBLE_ACTIVITY = "bubbleActivity"; + private static final String BUBBLE_EXTRA = "bubbleExtra"; private static final String SCHEDULE_MODE = "scheduleMode"; private static final String CATEGORY = "category"; private static final String TIMEOUT_AFTER = "timeoutAfter"; @@ -170,6 +171,7 @@ public class NotificationDetails implements Serializable { public Integer ledOffMs; public String ticker; public String bubbleActivity; + public String bubbleExtra; public Integer visibility; @SerializedName(value = "scheduleMode", alternate = "allowWhileIdle") @@ -272,6 +274,7 @@ private static void readPlatformSpecifics( readLargeIconInformation(notificationDetails, platformChannelSpecifics); notificationDetails.ticker = (String) platformChannelSpecifics.get(TICKER); notificationDetails.bubbleActivity = (String) platformChannelSpecifics.get(BUBBLE_ACTIVITY); + notificationDetails.bubbleExtra = (String) platformChannelSpecifics.get(BUBBLE_EXTRA); notificationDetails.visibility = (Integer) platformChannelSpecifics.get(VISIBILITY); if (platformChannelSpecifics.containsKey(SCHEDULE_MODE)) { notificationDetails.scheduleMode = diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart index 9c43e5105..67facb4a0 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart @@ -210,6 +210,7 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'ledOffMs': ledOffMs, 'ticker': ticker, 'bubbleActivity': bubbleActivity, + 'bubbleExtra': bubbleExtra, 'visibility': visibility?.index, 'timeoutAfter': timeoutAfter, 'category': category?.name, diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart index 50ed21f4a..69d80034b 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart @@ -134,6 +134,7 @@ class AndroidNotificationDetails { this.ledOffMs, this.ticker, this.bubbleActivity, + this.bubbleExtra, this.visibility, this.timeoutAfter, this.category, @@ -313,6 +314,8 @@ class AndroidNotificationDetails { final String? bubbleActivity; + final String? bubbleExtra; + /// The action to take for managing notification channels. /// /// Defaults to creating the notification channel using the provided details From a502fb4559c76fd70e23ba606c9b3452b0360daf Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 15 Nov 2023 18:20:48 +1030 Subject: [PATCH 03/12] Update FlutterLocalNotificationsPlugin.java --- .../FlutterLocalNotificationsPlugin.java | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index 97dccd0a4..ff40f14d5 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -444,7 +444,6 @@ protected static Notification createNotification( .setDesiredHeight(600) .build(); - builder.setContentIntent(bubbleIntent); builder.setBubbleMetadata(bubbleData); } } catch (ClassNotFoundException e) { From 2ed87e412371cb5aeaceaced0dd49bd812ea85be Mon Sep 17 00:00:00 2001 From: github-actions <> Date: Mon, 20 Nov 2023 01:47:04 +0000 Subject: [PATCH 04/12] Google Java Format --- .../FlutterLocalNotificationsPlugin.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index e2f901df0..395f5c755 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -417,23 +417,25 @@ protected static Notification createNotification( setCategory(notificationDetails, builder); setTimeoutAfter(notificationDetails, builder); - - if(notificationDetails.bubbleActivity != null) { + if (notificationDetails.bubbleActivity != null) { try { Class cls = Class.forName(notificationDetails.bubbleActivity); Intent testIntent = new Intent(context, cls); testIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if(notificationDetails.bubbleExtra != null) { + if (notificationDetails.bubbleExtra != null) { final Bundle extra = new Bundle(); extra.putString("bubbleExtra", notificationDetails.bubbleExtra); testIntent.putExtras(extra); } int actionFlags = PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; - PendingIntent bubbleIntent = PendingIntent.getActivity(context, notificationDetails.id, testIntent, actionFlags); + PendingIntent bubbleIntent = + PendingIntent.getActivity(context, notificationDetails.id, testIntent, actionFlags); - IconCompat icon = IconCompat.createWithResource(context, getDrawableResourceId(context, notificationDetails.icon)); + IconCompat icon = + IconCompat.createWithResource( + context, getDrawableResourceId(context, notificationDetails.icon)); Log.e(TAG, "Created pending intent: $bubbleIntent"); Log.e(TAG, bubbleIntent.toString()); @@ -441,9 +443,9 @@ protected static Notification createNotification( if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { androidx.core.app.NotificationCompat.BubbleMetadata bubbleData = - new androidx.core.app.NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon) - .setDesiredHeight(600) - .build(); + new androidx.core.app.NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon) + .setDesiredHeight(600) + .build(); builder.setBubbleMetadata(bubbleData); } @@ -451,7 +453,6 @@ protected static Notification createNotification( e.printStackTrace(); } } - Notification notification = builder.build(); if (notificationDetails.additionalFlags != null From 0cdda35be48014df8c56ecb5294a5acc68c42a00 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 22 Jun 2025 14:10:36 +0930 Subject: [PATCH 05/12] reworking bubble implementation --- .../FlutterLocalNotificationsPlugin.java | 17 +++++++++++++---- .../models/NotificationDetails.java | 6 ++++++ .../src/platform_specifics/android/bubble.dart | 15 +++++++++++++++ .../android/method_channel_mappers.dart | 5 +++-- .../android/notification_details.dart | 10 +++++----- 5 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index 7f8042614..d2e982d2a 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -458,11 +458,20 @@ protected static Notification createNotification( if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { - androidx.core.app.NotificationCompat.BubbleMetadata bubbleData = - new androidx.core.app.NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon) - .setDesiredHeight(600) - .build(); + var bubbleBuilder = + new androidx.core.app.NotificationCompat.BubbleMetadata.Builder() + .setIntent(bubbleIntent) + .setIcon(icon); + if(notificationDetails.bubbleDesiredHeight != null) { + bubbleBuilder.setDesiredHeight(notificationDetails.bubbleDesiredHeight); + } + + if(notificationDetails.bubbleAutoExpand != null) { + bubbleBuilder.setAutoExpandBubble(notificationDetails.bubbleAutoExpand); + } + + var bubbleData = bubbleBuilder.build(); builder.setBubbleMetadata(bubbleData); } } catch (ClassNotFoundException e) { diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java index 764a4e85b..d40127250 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java @@ -108,6 +108,8 @@ public class NotificationDetails implements Serializable { private static final String TICKER = "ticker"; private static final String BUBBLE_ACTIVITY = "bubbleActivity"; private static final String BUBBLE_EXTRA = "bubbleExtra"; + private static final String BUBBLE_AUTO_EXPAND = "bubbleAutoExpand"; + private static final String BUBBLE_DESIRED_HEIGHT = "bubbleDesiredHeight"; private static final String SCHEDULE_MODE = "scheduleMode"; private static final String CATEGORY = "category"; private static final String TIMEOUT_AFTER = "timeoutAfter"; @@ -178,6 +180,8 @@ public class NotificationDetails implements Serializable { public String ticker; public String bubbleActivity; public String bubbleExtra; + public Integer bubbleDesiredHeight; + public Boolean bubbleAutoExpand; public Integer visibility; @SerializedName(value = "scheduleMode", alternate = "allowWhileIdle") @@ -286,6 +290,8 @@ private static void readPlatformSpecifics( notificationDetails.ticker = (String) platformChannelSpecifics.get(TICKER); notificationDetails.bubbleActivity = (String) platformChannelSpecifics.get(BUBBLE_ACTIVITY); notificationDetails.bubbleExtra = (String) platformChannelSpecifics.get(BUBBLE_EXTRA); + notificationDetails.bubbleAutoExpand = (Boolean) platformChannelSpecifics.get(BUBBLE_AUTO_EXPAND); + notificationDetails.bubbleDesiredHeight = (Integer) platformChannelSpecifics.get(BUBBLE_DESIRED_HEIGHT); notificationDetails.visibility = (Integer) platformChannelSpecifics.get(VISIBILITY); if (platformChannelSpecifics.containsKey(SCHEDULE_MODE)) { notificationDetails.scheduleMode = diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart b/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart new file mode 100644 index 000000000..cbbff7143 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart @@ -0,0 +1,15 @@ +/// Defines metadata to be set for the notification's chat bubble +/// See: https://developer.android.com/develop/ui/views/notifications/bubbles +class BubbleMetadata { + final String activity; + final String? extra; + final int? desiredHeight; + final bool? autoExpand; + + BubbleMetadata( + this.activity, { + this.extra, + this.autoExpand, + this.desiredHeight, + }); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart index 89e4cb5d8..0b8398d4e 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart @@ -213,8 +213,9 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'ledOnMs': ledOnMs, 'ledOffMs': ledOffMs, 'ticker': ticker, - 'bubbleActivity': bubbleActivity, - 'bubbleExtra': bubbleExtra, + 'bubbleActivity': bubble?.activity, + 'bubbleExtra': bubble?.extra, + 'bubbleDesiredHeight': bubble?.desiredHeight, 'visibility': visibility?.index, 'timeoutAfter': timeoutAfter, 'category': category?.name, diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart index 9745442ba..01b5570e7 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'dart:ui'; import 'bitmap.dart'; +import 'bubble.dart'; import 'enums.dart'; import 'notification_sound.dart'; import 'styles/style_information.dart'; @@ -135,8 +136,7 @@ class AndroidNotificationDetails { this.ledOnMs, this.ledOffMs, this.ticker, - this.bubbleActivity, - this.bubbleExtra, + this.bubble, this.visibility, this.timeoutAfter, this.category, @@ -325,9 +325,9 @@ class AndroidNotificationDetails { /// Specifies the "ticker" text which is sent to accessibility services. final String? ticker; - final String? bubbleActivity; - - final String? bubbleExtra; + /// Specifies BubbleMetadata to be passed to the notification, allowing use of + /// Android conversation bubbles api. + final BubbleMetadata? bubble; /// The action to take for managing notification channels. /// From 866c9b742ba9d85471ad6700eac25a97827a85c6 Mon Sep 17 00:00:00 2001 From: github-actions <> Date: Sun, 22 Jun 2025 04:40:56 +0000 Subject: [PATCH 06/12] Google Java Format --- .../FlutterLocalNotificationsPlugin.java | 4 ++-- .../models/NotificationDetails.java | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index d2e982d2a..c6746a1e5 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -463,11 +463,11 @@ protected static Notification createNotification( .setIntent(bubbleIntent) .setIcon(icon); - if(notificationDetails.bubbleDesiredHeight != null) { + if (notificationDetails.bubbleDesiredHeight != null) { bubbleBuilder.setDesiredHeight(notificationDetails.bubbleDesiredHeight); } - if(notificationDetails.bubbleAutoExpand != null) { + if (notificationDetails.bubbleAutoExpand != null) { bubbleBuilder.setAutoExpandBubble(notificationDetails.bubbleAutoExpand); } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java index d40127250..40bc4a534 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java @@ -290,8 +290,10 @@ private static void readPlatformSpecifics( notificationDetails.ticker = (String) platformChannelSpecifics.get(TICKER); notificationDetails.bubbleActivity = (String) platformChannelSpecifics.get(BUBBLE_ACTIVITY); notificationDetails.bubbleExtra = (String) platformChannelSpecifics.get(BUBBLE_EXTRA); - notificationDetails.bubbleAutoExpand = (Boolean) platformChannelSpecifics.get(BUBBLE_AUTO_EXPAND); - notificationDetails.bubbleDesiredHeight = (Integer) platformChannelSpecifics.get(BUBBLE_DESIRED_HEIGHT); + notificationDetails.bubbleAutoExpand = + (Boolean) platformChannelSpecifics.get(BUBBLE_AUTO_EXPAND); + notificationDetails.bubbleDesiredHeight = + (Integer) platformChannelSpecifics.get(BUBBLE_DESIRED_HEIGHT); notificationDetails.visibility = (Integer) platformChannelSpecifics.get(VISIBILITY); if (platformChannelSpecifics.containsKey(SCHEDULE_MODE)) { notificationDetails.scheduleMode = From fe814cc30eac34ac024f9b8f6ff827b53878ed34 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:05:38 +0930 Subject: [PATCH 07/12] Add bubble notification to example --- .../android/app/src/main/AndroidManifest.xml | 20 +++++ .../MainActivity.kt | 9 +++ .../example/lib/main.dart | 77 +++++++++++++++++++ .../example/pubspec.yaml | 1 + .../lib/flutter_local_notifications.dart | 2 +- 5 files changed, 108 insertions(+), 1 deletion(-) diff --git a/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml b/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml index 266a44217..6ce9d2f81 100644 --- a/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml +++ b/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml @@ -40,6 +40,26 @@ + + + + + { await _showNotificationCustomSound(); }, ), + if (Platform.isAndroid) + PaddedElevatedButton( + buttonText: + 'Show notification with conversation bubble', + onPressed: _showNotificationWithBubble), if (kIsWeb || !Platform.isLinux) ...[ PaddedElevatedButton( buttonText: @@ -1402,6 +1419,66 @@ class _HomePageState extends State { ); } + Future _showNotificationWithBubble() async { + final FlutterShortcuts shortcuts = FlutterShortcuts(); + const String shortcutId = 'my_conversation_shortcut'; + const ShortcutItem item = ShortcutItem( + id: shortcutId, + action: 'some_action', + shortLabel: 'Conversation Shortcut', + icon: 'icons/coworker.png', + conversationShortcut: true); + + await shortcuts.pushShortcutItem(shortcut: item); + + final MessagingStyleInformation? activeStyleInfo = + await AndroidFlutterLocalNotificationsPlugin() + .getActiveNotificationMessagingStyle(0); + + const Person person = Person( + name: 'Coworker', + key: '2', + uri: 'tel:9876543210', + icon: FlutterBitmapAssetAndroidIcon('icons/coworker.png'), + ); + + final Message message = Message( + 'This is a test message', + DateTime.now(), + person, + ); + + activeStyleInfo?.messages?.add(message); + + final MessagingStyleInformation style = activeStyleInfo ?? + MessagingStyleInformation(person, + conversationTitle: 'Conversation', + messages: [message], + groupConversation: false); + + final AndroidNotificationDetails details = AndroidNotificationDetails( + 'messages', 'Message Received', + importance: Importance.high, + priority: Priority.high, + icon: 'secondary_icon', + number: style.messages!.length, + subText: 'Test Message', + styleInformation: style, + shortcutId: + shortcutId, // Has to be same as the shortcut created earlier, in a real use case probably would be the ID of a chat room / user + ticker: 'Test Message', + bubble: BubbleMetadata( + 'com.dexterous.flutter_local_notifications_example.BubbleActivity', + desiredHeight: 600, + autoExpand: false, + extra: 'bubble_extra_data'), + color: const Color.fromARGB(0xff, 0x53, 0x4c, 0xdd)); + + await flutterLocalNotificationsPlugin.show( + id, null, 'Test Message', NotificationDetails(android: details), + payload: 'some_message_payload'); + } + Future _showNotificationCustomVibrationIconLed() async { final Int64List vibrationPattern = Int64List(4); vibrationPattern[0] = 0; diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index 21edacde6..2f33e65b2 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: sdk: flutter flutter_local_notifications: path: ../ + flutter_shortcuts_new: ^2.0.0 flutter_timezone: ^4.0.1 http: ^1.3.0 image: ^4.5.2 diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart index c42e2ff4c..08c93343d 100644 --- a/flutter_local_notifications/lib/flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/flutter_local_notifications.dart @@ -10,6 +10,7 @@ export 'src/notification_details.dart'; export 'src/platform_flutter_local_notifications.dart' hide MethodChannelFlutterLocalNotificationsPlugin; export 'src/platform_specifics/android/bitmap.dart'; +export 'src/platform_specifics/android/bubble.dart'; export 'src/platform_specifics/android/enums.dart' hide AndroidBitmapSource, AndroidIconSource, AndroidNotificationSoundSource; export 'src/platform_specifics/android/icon.dart' hide AndroidIcon; @@ -37,6 +38,5 @@ export 'src/platform_specifics/darwin/notification_category.dart'; export 'src/platform_specifics/darwin/notification_category_option.dart'; export 'src/platform_specifics/darwin/notification_details.dart'; export 'src/platform_specifics/darwin/notification_enabled_options.dart'; - export 'src/typedefs.dart'; export 'src/types.dart'; From f5e89113db580bdd46e7821ef7f8c759238b99bf Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:16:45 +0930 Subject: [PATCH 08/12] add reading bubble intent to example --- .../example/lib/main.dart | 27 +++++++++++++++---- .../example/pubspec.yaml | 1 + 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 54ee12d99..6156c6db9 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -14,6 +14,7 @@ import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:http/http.dart' as http; import 'package:image/image.dart' as image; import 'package:path_provider/path_provider.dart'; +import 'package:receive_intent/receive_intent.dart' as receive_intent; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; @@ -76,11 +77,27 @@ void notificationTapBackground(NotificationResponse notificationResponse) { } @pragma('vm:entry-point') -void bubbleEntry() { +Future bubbleEntry() async { + WidgetsFlutterBinding.ensureInitialized(); + + final receive_intent.Intent? intent = + await receive_intent.ReceiveIntent.getInitialIntent(); + String? extraData; + if (intent?.extra?.containsKey('bubbleExtra') == true) { + extraData = intent!.extra!['bubbleExtra'] as String; + } + runApp( - const MaterialApp( + MaterialApp( home: Scaffold( - body: Center(child: Text('This is your bubble UI')), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('This is your bubble UI'), + Text('created with extra data: $extraData'), + ], + )), ), ), ); @@ -1469,9 +1486,9 @@ class _HomePageState extends State { ticker: 'Test Message', bubble: BubbleMetadata( 'com.dexterous.flutter_local_notifications_example.BubbleActivity', - desiredHeight: 600, + desiredHeight: 1000, autoExpand: false, - extra: 'bubble_extra_data'), + extra: 'my_bubble_extra_data'), color: const Color.fromARGB(0xff, 0x53, 0x4c, 0xdd)); await flutterLocalNotificationsPlugin.show( diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index 2f33e65b2..51ea85f38 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: http: ^1.3.0 image: ^4.5.2 path_provider: ^2.1.5 + receive_intent: ^0.2.7 timezone: ^0.10.0 dev_dependencies: From 0891e4eef6f8eb2109f82feac38db6e51a23e554 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:25:41 +0930 Subject: [PATCH 09/12] document BubbleMetadata --- .../platform_specifics/android/bubble.dart | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart b/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart index cbbff7143..89bd62793 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart @@ -1,15 +1,29 @@ /// Defines metadata to be set for the notification's chat bubble /// See: https://developer.android.com/develop/ui/views/notifications/bubbles class BubbleMetadata { - final String activity; - final String? extra; - final int? desiredHeight; - final bool? autoExpand; - + /// Encapsulates the information needed to display a notification as a bubble. + /// A bubble is used to display app content in a floating window over the + /// existing foreground activity. A bubble has a collapsed state represented + /// by an icon and an expanded state that displays an activity. + /// - https://developer.android.com/reference/android/app/Notification.BubbleMetadata BubbleMetadata( this.activity, { this.extra, this.autoExpand, this.desiredHeight, }); + + /// Define which activity should be started by the bubble + /// This activity needs to be declared in your android Manfiest.xml + /// As well as in the native code + final String activity; + + /// Pass additional data to the bubble activities intent + final String? extra; + + /// the ideal height, in DPs, for the floating window created by this activity + final int? desiredHeight; + + /// whether this bubble should auto expand when it is posted. + final bool? autoExpand; } From 5badecb74e5ed1cfbf8976428a20254be6c1dfca Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:33:29 +0930 Subject: [PATCH 10/12] Update FlutterLocalNotificationsPlugin.java --- .../FlutterLocalNotificationsPlugin.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index c6746a1e5..7de668f1e 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -436,31 +436,28 @@ protected static Notification createNotification( if (notificationDetails.bubbleActivity != null) { try { Class cls = Class.forName(notificationDetails.bubbleActivity); - Intent testIntent = new Intent(context, cls); - testIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent bubbleIntent = new Intent(context, cls); + bubbleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (notificationDetails.bubbleExtra != null) { final Bundle extra = new Bundle(); extra.putString("bubbleExtra", notificationDetails.bubbleExtra); - testIntent.putExtras(extra); + bubbleIntent.putExtras(extra); } int actionFlags = PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; - PendingIntent bubbleIntent = - PendingIntent.getActivity(context, notificationDetails.id, testIntent, actionFlags); + PendingIntent pendingBubbleIntent = + PendingIntent.getActivity(context, notificationDetails.id, bubbleIntent, actionFlags); IconCompat icon = IconCompat.createWithResource( context, getDrawableResourceId(context, notificationDetails.icon)); - Log.e(TAG, "Created pending intent: $bubbleIntent"); - Log.e(TAG, bubbleIntent.toString()); - if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { var bubbleBuilder = new androidx.core.app.NotificationCompat.BubbleMetadata.Builder() - .setIntent(bubbleIntent) + .setIntent(pendingBubbleIntent) .setIcon(icon); if (notificationDetails.bubbleDesiredHeight != null) { From 5668a22a0842bb3894d51ebdd6df5904cbd25c4c Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:36:33 +0930 Subject: [PATCH 11/12] Update method_channel_mappers.dart --- .../src/platform_specifics/android/method_channel_mappers.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart index 0b8398d4e..07989bc18 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart @@ -216,6 +216,7 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'bubbleActivity': bubble?.activity, 'bubbleExtra': bubble?.extra, 'bubbleDesiredHeight': bubble?.desiredHeight, + 'bubbleAutoExpand': bubble?.autoExpand, 'visibility': visibility?.index, 'timeoutAfter': timeoutAfter, 'category': category?.name, From 93f906985388835d24eea13ae6ccd8ee656f2cf1 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:53:50 +1030 Subject: [PATCH 12/12] Add support for linux inline reply (#1) --- .../lib/src/notifications_manager.dart | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/flutter_local_notifications_linux/lib/src/notifications_manager.dart b/flutter_local_notifications_linux/lib/src/notifications_manager.dart index 0fe413ced..3e9c076cf 100644 --- a/flutter_local_notifications_linux/lib/src/notifications_manager.dart +++ b/flutter_local_notifications_linux/lib/src/notifications_manager.dart @@ -369,6 +369,34 @@ class LinuxNotificationManager { }, ); + _dbus.subscribeSignal(_DBusMethodsSpec.notificationReplied).listen( + (DBusSignal s) async { + if (s.signature != DBusSignature('us')) { + return; + } + + final int systemId = (s.values[0] as DBusUint32).value; + final String text = (s.values[1] as DBusString).value; + + final LinuxNotificationInfo? notify = + await _storage.getBySystemId(systemId); + if (notify == null) { + return; + } + + _onDidReceiveNotificationResponse?.call( + NotificationResponse( + id: notify.id, + actionId: 'inline-reply', + payload: notify.payload, + input: text, + notificationResponseType: + NotificationResponseType.selectedNotificationAction, + ), + ); + }, + ); + _dbus.subscribeSignal(_DBusMethodsSpec.notificationClosed).listen( (DBusSignal s) async { if (s.signature != DBusSignature('uu')) { @@ -393,6 +421,7 @@ class _DBusMethodsSpec { static const String notify = 'Notify'; static const String closeNotification = 'CloseNotification'; static const String actionInvoked = 'ActionInvoked'; + static const String notificationReplied = 'NotificationReplied'; static const String notificationClosed = 'NotificationClosed'; static const String getCapabilities = 'GetCapabilities'; }