Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,50 @@ protected static Notification createNotification(
setProgress(notificationDetails, builder);
setCategory(notificationDetails, builder);
setTimeoutAfter(notificationDetails, builder);

if (notificationDetails.bubbleActivity != null) {
Copy link
Owner

Choose a reason for hiding this comment

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

Sorry I missed this before but can you refactor out all the changes to a separate method

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();
Copy link
Owner

Choose a reason for hiding this comment

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

is there a reason for swallowing the exception? can this be surfaced as an exception on the Flutter with appropriate details for users of the plugin to handle?

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure the best way to do this, I would appreciate some guidance here

Copy link
Owner

Choose a reason for hiding this comment

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

When catching theClassNotFoundException, you can throw a RuntimeException that wraps around the ClassNotFoundException. My understanding is Flutter will handle catching this and on the Dart side, a PlatformException will be thrown so that developers can handle accordingly. Are you able to reproduce the scenarios where the ClassNotFoundException will occur? If so, then once you make the changes then you'll be able to confirm this

}
}

Notification notification = builder.build();
if (notificationDetails.additionalFlags != null
&& notificationDetails.additionalFlags.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".BubbleActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:allowEmbedded="true"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.dexterous.flutter_local_notifications_example

import androidx.annotation.NonNull
import android.content.ContentResolver
import android.content.Context
import android.media.RingtoneManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel
import java.util.*

Expand Down Expand Up @@ -32,3 +34,10 @@ class MainActivity: FlutterActivity() {
+ context.resources.getResourceEntryName(resId))
}
}

class BubbleActivity : FlutterActivity() {
override fun getDartEntrypointFunctionName() = "bubbleEntry"

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) =
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
94 changes: 94 additions & 0 deletions flutter_local_notifications/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_shortcuts_new/flutter_shortcuts_new.dart';
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;

Expand Down Expand Up @@ -74,6 +76,33 @@ void notificationTapBackground(NotificationResponse notificationResponse) {
}
}

@pragma('vm:entry-point')
Future<void> 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: <Widget>[
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.
///
Expand Down Expand Up @@ -387,6 +416,11 @@ class _HomePageState extends State<HomePage> {
await _showNotificationCustomSound();
},
),
if (Platform.isAndroid)
PaddedElevatedButton(
buttonText:
'Show notification with conversation bubble',
onPressed: _showNotificationWithBubble),
if (kIsWeb || !Platform.isLinux) ...<Widget>[
PaddedElevatedButton(
buttonText:
Expand Down Expand Up @@ -1412,6 +1446,66 @@ class _HomePageState extends State<HomePage> {
);
}

Future<void> _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>[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<void> _showNotificationCustomVibrationIconLed() async {
final Int64List vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
Expand Down
2 changes: 2 additions & 0 deletions flutter_local_notifications/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/// Defines metadata to be set for the notification's chat bubble
Copy link
Owner

Choose a reason for hiding this comment

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

This is a general comment on all the API docs

  • There are casing issues with the API docs and lack of punctuation marks
  • Look to follow these guidelines around documentation that were mentioned in the contribution guide. You can reference the existing API docs to see examples. A couple of issues that stick to me are violations of this and I also try to have the docs follow this where possible.

/// 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -144,6 +145,7 @@ class AndroidNotificationDetails {
this.ledOnMs,
this.ledOffMs,
this.ticker,
this.bubble,
this.visibility,
this.timeoutAfter,
this.category,
Expand Down Expand Up @@ -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
Expand Down
Loading