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'; }