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 10520339a..6b257de74 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
@@ -443,6 +443,50 @@ 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 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);
+ bubbleIntent.putExtras(extra);
+ }
+
+ int actionFlags = PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT;
+ PendingIntent pendingBubbleIntent =
+ PendingIntent.getActivity(context, notificationDetails.id, bubbleIntent, actionFlags);
+
+ IconCompat icon =
+ IconCompat.createWithResource(
+ context, getDrawableResourceId(context, notificationDetails.icon));
+
+ if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) {
+
+ var bubbleBuilder =
+ new androidx.core.app.NotificationCompat.BubbleMetadata.Builder()
+ .setIntent(pendingBubbleIntent)
+ .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) {
+ 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 47c086da6..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
@@ -106,6 +106,10 @@ 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 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";
@@ -174,6 +178,10 @@ public class NotificationDetails implements Serializable {
public Integer ledOnMs;
public Integer ledOffMs;
public String ticker;
+ public String bubbleActivity;
+ public String bubbleExtra;
+ public Integer bubbleDesiredHeight;
+ public Boolean bubbleAutoExpand;
public Integer visibility;
@SerializedName(value = "scheduleMode", alternate = "allowWhileIdle")
@@ -280,6 +288,12 @@ private static void readPlatformSpecifics(
readLedInformation(notificationDetails, platformChannelSpecifics);
readLargeIconInformation(notificationDetails, platformChannelSpecifics);
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/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 @@
+
+
+
+
+
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(
+ MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Text('This is your bubble UI'),
+ Text('created with extra data: $extraData'),
+ ],
+ )),
+ ),
+ ),
+ );
+}
+
/// IMPORTANT: running the following code on its own won't work as there is
/// setup required for each platform head project.
///
@@ -387,6 +416,11 @@ class _HomePageState extends State {
await _showNotificationCustomSound();
},
),
+ if (Platform.isAndroid)
+ PaddedElevatedButton(
+ buttonText:
+ 'Show notification with conversation bubble',
+ onPressed: _showNotificationWithBubble),
if (kIsWeb || !Platform.isLinux) ...[
PaddedElevatedButton(
buttonText:
@@ -1412,6 +1446,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: 1000,
+ autoExpand: false,
+ extra: 'my_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..51ea85f38 100644
--- a/flutter_local_notifications/example/pubspec.yaml
+++ b/flutter_local_notifications/example/pubspec.yaml
@@ -9,10 +9,12 @@ 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
path_provider: ^2.1.5
+ receive_intent: ^0.2.7
timezone: ^0.10.0
dev_dependencies:
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';
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..89bd62793
--- /dev/null
+++ b/flutter_local_notifications/lib/src/platform_specifics/android/bubble.dart
@@ -0,0 +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 {
+ /// 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;
+}
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 bd4bbabb3..a315e5041 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,6 +213,10 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails {
'ledOnMs': ledOnMs,
'ledOffMs': ledOffMs,
'ticker': ticker,
+ 'bubbleActivity': bubble?.activity,
+ 'bubbleExtra': bubble?.extra,
+ 'bubbleDesiredHeight': bubble?.desiredHeight,
+ 'bubbleAutoExpand': bubble?.autoExpand,
'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 e4c9c95c9..ba64f6a43 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';
@@ -144,6 +145,7 @@ class AndroidNotificationDetails {
this.ledOnMs,
this.ledOffMs,
this.ticker,
+ this.bubble,
this.visibility,
this.timeoutAfter,
this.category,
@@ -332,6 +334,10 @@ class AndroidNotificationDetails {
/// Specifies the "ticker" text which is sent to accessibility services.
final String? ticker;
+ /// 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.
///
/// Defaults to creating the notification channel using the provided details
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';
}