diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 000000000..8df560e25 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,164 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_as + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - close_sinks + # - comment_references + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + # - diagnostic_describe_all_properties + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - lines_longer_than_80_chars + - list_remove_unrelated_type + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_duplicate_case_values + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - null_closures + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + # - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + # - prefer_mixin + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + # - public_member_api_docs + - recursive_getters + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + # - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + # - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - unsafe_html + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_key_in_widget_constructors + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks \ No newline at end of file diff --git a/flutter_local_notifications/.gitignore b/flutter_local_notifications/.gitignore index bec8719bb..73d6d6c7b 100644 --- a/flutter_local_notifications/.gitignore +++ b/flutter_local_notifications/.gitignore @@ -22,13 +22,14 @@ # Flutter/Dart/Pub related **/doc/api/ +**/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ -pubspec.lock # Android related **/android/**/gradle-wrapper.jar diff --git a/flutter_local_notifications/CHANGELOG.md b/flutter_local_notifications/CHANGELOG.md index b1a97d151..3f78e388a 100644 --- a/flutter_local_notifications/CHANGELOG.md +++ b/flutter_local_notifications/CHANGELOG.md @@ -1,3 +1,37 @@ +# [1.5.0] + +* Added macOS implementation of the plugin +* The `schedule`, `showDailyAtTime` and `showWeeklyAtDayAndTime` methods has been marked as a deprecated due to problems with time zones, particularly when it comes to daylight savings. +* Added the `zonedSchedule` method to the plugin that allows for scheduling notifications to occur on a specific date and time relative a specific time zone. This can be used to schedule daily and weekly notifications as well. The example app has been updated to demonstrate its usage. Note that to support time zone-based scheduling, the plugin now depends on the `timezone` package so that an instance of the `TZDateTime` class is required to the specify the time the notification should occur. This should work in most cases as it is IANA-based and native platforms have time zones that are IANA-based as well. To support time zone aware dates on older versions of Android (which use older Java APIs), the plugin depends on the [ThreeTen Android Backport library](https://github.com/JakeWharton/ThreeTenABP). Once Flutter's support for Android Studio 4.0 and Android Gradle plugin 4.0 has stabilised, the plugin will be updated to make use of [desugaring](https://developer.android.com/studio/releases/gradle-plugin#j8-library-desugaring) instead of relying on the ThreeTen Android Backport library. +* [Android] Fixed issue [670] where `getNotificationAppLaunchDetails()` behaved inconsistently depending on if it was called before or after `initialize()` +* [Android] Added the `getActiveNotifications()` method to the `AndroidFlutterLocalNotificationsPlugin` class thanks to the PR from [Vincent Kammerer](https://github.com/vkammerer). This can be used to query the active notifications and is only applicable to Android 6.0 or newer +* [Android] Fixed an issue where the error message for an invalid source resource wasn't formatted correctly to include the name of the specified resource +* [Android] Added `androidAllowWhileIdle` boolean argument to the `periodicallyShow` method. When set to true, this changes how recurring notifications are shown so that the Android `AlarmManager` API is used to schedule a notification with exact timing. When the notification appears, the next one is scheduled after that. This is get around the limitations where the `AlarmManager` APIs don't provide a way for work to be repeated with precising timing regardless of the power mode. + The example app has been updated to include these changes so that it can be used as a reference as well +* [Android] Added support for full-screen notifications via the `fullScreenIntent` argument that has been added to the `AndroidNotificationDetails` class. Thanks to the PR from [Nadav Fima](https://github.com/nadavfima) +* [Android] Bumped compile SDK to 30 (Android 11) +* [Android] Added ability to specify shortcut id that can be used for conversations. See https://developer.android.com/guide/topics/ui/conversations for more info. Note the plugin doesn't provide the ability to publish shortcuts so developers will likely need to look into writing their own code to do so and save the shortcut id so that it can be linked to notifications +* [iOS] Updated the details in the plugin's podspec file +* [iOS] Added ability to specify a subtitle for a notification via the `subtitle` property of the `IOSNotificationDetails` class. This property is only application to iOS versions 10 or newer +* **Breaking change** The `InitializationSettings` and `NotificationDetails` classes no longer have positional parameters but now have named parameters called `android` and `iOS` for passing in data specific to Android and iOS. There `macOS` named parameter has also been added for passing data specific to macOS +* **Breaking change** The `toMap` method that was used internally to transfer data over platform channels is no longer publicly accessible +* **Breaking change** All enum values have been renamed to follow lower camel case convention. This affects the following enums + * `Day` + * `AndroidNotificationChannelAction` + * `Importance` (note: as `default` is a keyword, what use to be `Default` is now `defaultImportance`) + * `Priority` (note: as `default` is a keyword, what use to be `Default` is now `defaultPriority`) + * `GroupAlertBehavior` + * `NotificationVisibility` + * `RepeatInterval` +* **Breaking change** assertions have been added to the `IOSInitializationSettings` constructor to prevent null values being passed in +* Updated example app so that code for demonstrating functionality that is specific to a platform are only visible when running on the appropriate platform +* Bumped Android dependencies +* Updated example app's Proguard rules file to match latest configuration required by GSON +* Bumped lower bound of Dart SDK dependency to 2.6 +* Updated and fixed wording in API docs +* Readme now has a table of contents. Thanks to the PR from [AscΓͺnio](https://github.com/Ascenio) + + # [1.4.4+5] * Updated the `platform` package version range constraint so that 3.x null safety releases could be used (currently used in Flutter 1.22 stable) @@ -46,8 +80,8 @@ Please note that there are a number of breaking changes in this release to improve the developer experience when using the plugin APIs. The changes should hopefully be straightforward but please through the changelog carefully just in case. The steps migrate your code has been covered below but the Git history of the example application's `main.dart` file can also be used as reference. -* [Android] **BREAKING CHANGE** The `style` property of the `AndroidNotificationDetails` class has been removed as it was redundant. No changes are needed unless your application was displaying media notifications (i.e. `style` was set to `AndroidNotificationStyle.Media`). If this is the case, you can migrate your code by setting the `styleInformation` property of the `AndroidNotificationDetails` to an instance of the `MediaNotificationStyleInformation` class. This class is a new addition in this release -* [Android] **BREAKING CHANGE** The `AndroidNotificationSound` abstract class has been introduced to represent Android notification sounds. The `sound` property of the `AndroidNotificationDetails` class has changed from being a `String` type to an `AndroidNotificationSound` type. In this release, the `AndroidNotificationSound` has the following subclasses +* [Android] **Breaking change** The `style` property of the `AndroidNotificationDetails` class has been removed as it was redundant. No changes are needed unless your application was displaying media notifications (i.e. `style` was set to `AndroidNotificationStyle.Media`). If this is the case, you can migrate your code by setting the `styleInformation` property of the `AndroidNotificationDetails` to an instance of the `MediaNotificationStyleInformation` class. This class is a new addition in this release +* [Android] **Breaking change** The `AndroidNotificationSound` abstract class has been introduced to represent Android notification sounds. The `sound` property of the `AndroidNotificationDetails` class has changed from being a `String` type to an `AndroidNotificationSound` type. In this release, the `AndroidNotificationSound` has the following subclasses * `RawResourceAndroidNotificationSound`: use this when the sound is raw resource associated with the Android application. Previously, this was the only type of sound supported so applications using the plugin prior to 1.4.0 can migrate their application by using this class. For example, if your previous code was @@ -70,7 +104,7 @@ Please note that there are a number of breaking changes in this release to impro ``` * `UriAndroidNotificationSound`: use this when a URI refers to the sound on the Android device. This is a new feature being supported as part of this release. Developers may need to write their code to access native Android APIs (e.g. the `RingtoneManager` APIs) to obtain the URIs they need. -* [Android] **BREAKING CHANGE** The `BitmapSource` enum has been replaced by the newly `AndroidBitmap` abstract class and its subclasses. This removes the need to specify the name/path of the bitmap and the source of the bitmap as two separate properties (e.g. the `largeIcon` and `largeIconBitmapSource` properties of the `AndroidNotificationDetails` class). This change affects the following classes +* [Android] **Breaking change** The `BitmapSource` enum has been replaced by the newly `AndroidBitmap` abstract class and its subclasses. This removes the need to specify the name/path of the bitmap and the source of the bitmap as two separate properties (e.g. the `largeIcon` and `largeIconBitmapSource` properties of the `AndroidNotificationDetails` class). This change affects the following classes * `AndroidNotificationDetails`: the `largeIcon` is now an `AndroidBitmap` type instead of a `String` and the `largeIconBitmapSource` property has been removed * `BigPictureStyleInformation`: the `largeIcon` is now an `AndroidBitmap` type instead of a `String` and the `largeIconBitmapSource` property has been removed. The `bigPicture` is now a `AndroidBitmap` type instead of a `String` and the `bigPictureBitmapSource` property has been removed @@ -102,7 +136,7 @@ Please note that there are a number of breaking changes in this release to impro largeIcon: DrawableResourceAndroidBitmap('sample_large_icon'), ) ``` -* [Android] **BREAKING CHANGE** The `IconSource` enum has been replaced by the newly added `AndroidIcon` abstract class and its subclasses. This change was done for similar reasons in replacing the `BitmapSource` enum. This only affects the `Person` class, which is used when displaying each person in a messaging-style notification. Here the `icon` property is now an `AndroidIcon` type instead of a `String` and the `iconSource` property has been removed. +* [Android] **Breaking change** The `IconSource` enum has been replaced by the newly added `AndroidIcon` abstract class and its subclasses. This change was done for similar reasons in replacing the `BitmapSource` enum. This only affects the `Person` class, which is used when displaying each person in a messaging-style notification. Here the `icon` property is now an `AndroidIcon` type instead of a `String` and the `iconSource` property has been removed. The following describes how each `IconSource` value maps to the `AndroidIcon` subclasses @@ -128,7 +162,7 @@ Please note that there are a number of breaking changes in this release to impro ``` The `AndroidIcon` also has a `BitmapAssetAndroidIcon` subclass to enables the usage of bitmap icons that have been registered as a Flutter asset via the `pubspec.yaml` file. -* [Android] **BREAKING CHANGE** All properties in the `AndroidNotificationDetails`, `DefaultStyleInformation` and `InboxStyleInformation` classes have been made `final` +* [Android] **Breaking change** All properties in the `AndroidNotificationDetails`, `DefaultStyleInformation` and `InboxStyleInformation` classes have been made `final` * The `DefaultStyleInformation` class now implements the `StyleInformation` class instead of extending it * Where possible, classes in the plugins have been updated to provide `const` constructors * Updates to API docs and readme @@ -137,7 +171,7 @@ Please note that there are a number of breaking changes in this release to impro # [1.3.0] -* [iOS] **BREAKING CHANGE** Plugin will now throw a `PlatformException` if there was an error returned upon calling the native [`addNotificationRequest`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649508-addnotificationrequest) method. Previously the error was logged on the native side the using [`NSLog`](https://developer.apple.com/documentation/foundation/1395275-nslog) function. +* [iOS] **Breaking change** Plugin will now throw a `PlatformException` if there was an error returned upon calling the native [`addNotificationRequest`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649508-addnotificationrequest) method. Previously the error was logged on the native side the using [`NSLog`](https://developer.apple.com/documentation/foundation/1395275-nslog) function. * [iOS] Added ability to associate notifications with attachments. Only applicable to iOS 10+ where the UserNotification APIs are used. Thanks to the PR from [Pavel Sipaylo](https://github.com/psipaylo) * Updated readme on using `firebase_messaging` together with `flutter_local_notifications` to let the community that `firebase_messaging` 6.0.13 can be used to resolve compatibility issues around callbacks when both plugins are used together @@ -560,7 +594,7 @@ Please note that there are a number of breaking changes in this release to impro ## [0.1.2] -* [Android] Bug fix in calculating when to show a scheduled notification. Ensure scheduled Android notifications will remain scheduled even after rebooting. +* [Android] Bug fix in calculating when to show a scheduled notification. Ensure scheduled Android notifications will remain scheduled even after rebooting. ## [0.1.1] diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index a0062ff20..de83e0a08 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -5,9 +5,48 @@ A cross platform plugin for displaying local notifications. +## Table of contents +- **[πŸ“± Supported platforms](#-supported-platforms)** +- **[✨ Features](#-features)** +- **[⚠ Caveats and limitations](#-caveats-and-limitations)** + - [Compatibility with firebase_messaging](#compatibility-with-firebase_messaging) + - [Scheduled Android notifications](#scheduled-android-notifications) + - [Recurring Android notifications](#recurring-android-notifications) + - [iOS pending notifications limit](#ios-pending-notifications-limit) + - [Scheduled notifications and daylight savings](#scheduled-notifications-and-daylight-savings) + - [Custom notification sounds](#custom-notification-sounds) + - [macOS differences](#macos-differences) +- **[πŸ“· Screenshots](#-screenshots)** +- **[πŸ‘ Acknowledgements](#-acknowledgements)** +- **[βš™οΈ Android Setup](#️-android-setup)** + - [Custom notification icons and sounds](#custom-notification-icons-and-sounds) + - [Scheduled notifications](#scheduled-notifications) + - [Fullscreen intent notifications](#fullscreen-intent-notifications) + - [Release build configuration](#release-build-configuration) +- **[βš™οΈ iOS setup](#️-ios-setup)** + - [General setup](#general-setup) + - [Handling notifications whilst the app is in the foreground](#handling-notifications-whilst-the-app-is-in-the-foreground) +- **[❓ Usage](#-usage)** + - [Example app](#example-app) + - [API reference](#api-reference) +- **[Initialisation](#initialisation)** + - [[iOS (all supported versions) and macOS 10.14+] Requesting notification permissions](#ios-all-supported-versions-and-macos-1014-requesting-notification-permissions) + - [Displaying a notification](#displaying-a-notification) + - [Scheduling a notification](#scheduling-a-notification) + - [Periodically show a notification with a specified interval](#periodically-show-a-notification-with-a-specified-interval) + - [Retrieveing pending notification requests](#retrieveing-pending-notification-requests) + - [[Android only] Retrieving active notifications](#android-only-retrieving-active-notifications) + - [[Android only] Grouping notifications](#android-only-grouping-notifications) + - [Cancelling/deleting a notification](#cancellingdeleting-a-notification) + - [Cancelling/deleting all notifications](#cancellingdeleting-all-notifications) + - [Getting details on if the app was launched via a notification created by this plugin](#getting-details-on-if-the-app-was-launched-via-a-notification-created-by-this-plugin) + - [[iOS only] Periodic notifications showing up after reinstallation](#ios-only-periodic-notifications-showing-up-after-reinstallation) +- **[πŸ“ˆ Testing](#-testing)** + ## πŸ“± Supported platforms -* **Android API 16+** (4.1+, the minimum version supported by Flutter). Uses the NotificationCompat APIs so it can be run older Android devices -* **iOS 8.0+** (the minimum version supported by Flutter). Supports the old and new iOS notification APIs (the User Notifications Framework introduced in iOS 10 but will use the UILocalNotification APIs for devices predating iOS 10) +* **Android 4.1+**. Uses the [NotificationCompat APIs](https://developer.android.com/reference/androidx/core/app/NotificationCompat) so it can be run older Android devices +* **iOS 8.0+**. On iOS versions older than 10, the plugin will use the UILocalNotification APIs. The [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework) is used on iOS 10 or newer. +* **macOS 10.11+**. On macOS versions older than 10.14, the plugin will use the [NSUserNotification APIs](https://developer.apple.com/documentation/foundation/nsusernotification). The [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework) is used on macOS 10.14 or newer. ## ✨ Features @@ -40,8 +79,9 @@ A cross platform plugin for displaying local notifications. * [Android] Show progress notifications * [Android] Configure notification visibility on the lockscreen * [Android] Ability to create and delete notification channels -* [iOS] Request notification permissions and customise the permissions being requested around displaying notifications -* [iOS] Display notifications with attachments +* [Android] Retrieve the list of active notifications +* [iOS (all supported versions) & macOS 10.14+] Request notification permissions and customise the permissions being requested around displaying notifications +* [iOS 10 or newer and macOS 10.14 or newer] Display notifications with attachments ## ⚠ Caveats and limitations The cross-platform facing API exposed by the `FlutterLocalNotificationsPlugin` class doesn't expose platform-specific methods as its goal is to provide an abstraction for all platforms. As such, platform-specific configuration is passed in as data. There are platform-specific implementations of the plugin that can be obtained by calling the [`resolvePlatformSpecificImplementation`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/resolvePlatformSpecificImplementation.html). An example of using this is provided in the section on requesting permissions on iOS. In spite of this, there may still be gaps that don't cover your use case and don't make sense to add as they don't fit with the plugin's architecture or goals. Developers can fork or maintain their own code for showing notifications in these situations. @@ -56,13 +96,19 @@ Some Android OEMs have their own customised Android OS that can prevent applicat This feature uses the [Alarm Manager](https://developer.android.com/reference/android/app/AlarmManager) API. This is standard practice but does mean the delivery of the notifications/alarms are inexact and this is documented Android behaviour as per the previous link. It has been reported that Samsung's implementation of Android has imposed a maximum of 500 alarms that can be scheduled via this API and exceptions can occur when going over the limit. ##### iOS pending notifications limit -There is a limit imposed by iOS where it will only keep 64 notifications that will fire the soonest +There is a limit imposed by iOS where it will only keep 64 notifications that will fire the soonest. ##### Scheduled notifications and daylight savings -Daylight saving issues for scheduled notifications is a known issue. This functionality may be deprecated to be replaced by another that only deals with elapsed time since epoch instead of a date. +The notification APIs used on iOS versions older than 10 (aka the `UILocalNotification` APIs) have limited supported for time zones. ##### Custom notification sounds -[iOS restrictions](https://developer.apple.com/documentation/usernotifications/unnotificationsound?language=objc) apply (e.g. supported file formats) +[iOS and macOS restrictions](https://developer.apple.com/documentation/usernotifications/unnotificationsound?language=objc) apply (e.g. supported file formats). + +##### macOS differences + +Due to limitations currently within the macOS Flutter engine, `getNotificationAppLaunchDetails` will return null on macOS versions older than 10.14. These limitations will mean that conflicts may occur when using this plugin with other notification plugins (e.g. for push notifications). + +The `schedule`, `showDailyAtTime` and `showWeeklyAtDayAndTime` methods that were implemented before macOS support was added and have been marked as deprecated aren't implemented on macOS. ## πŸ“· Screenshots @@ -70,7 +116,6 @@ Daylight saving issues for scheduled notifications is a known issue. This functi | ------------- | ------------- | | | | - ## πŸ‘ Acknowledgements * [Javier Lecuona](https://github.com/javiercbk) for submitting the PR that added the ability to have notifications shown daily @@ -79,19 +124,6 @@ Daylight saving issues for scheduled notifications is a known issue. This functi * [Zhang Jing](https://github.com/byrdkm17) for adding 'ticker' support for Android notifications * ...and everyone else for their contributions. They are greatly appreciated -## πŸ“ˆ Testing - -As the plugin class is not static, it is possible to mock and verify its behaviour when writing tests as part of your application. -Check the source code for a sample test suite that has been kindly implemented (_test/flutter_local_notifications_test.dart_) that demonstrates how this can be done. - -If you decide to use the plugin class directly as part of your tests, the methods will be mostly no-op and methods that return data will return default values. - -Part of this is because the plugin detects if you're running on a supported plugin to determine which platform implementation of the plugin should be used. If it's neither Android or iOS, then it defaults to the aforementioned behaviour to reduce friction when writing tests. If this not desired then consider using mocks. - -Note there is also a [named constructor](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/FlutterLocalNotificationsPlugin.private.html) that can be used to pass the platform for the plugin to resolve the desired platform-specific implementation. - - - ## βš™οΈ Android Setup #### Custom notification icons and sounds @@ -138,6 +170,23 @@ If the vibration pattern of an Android notification will be customised then add ``` + +#### Fullscreen intent notifications + +If your application needs the ability to schedule full-screen intent notifications, add the following to the manifest (i.e. your application's `AndroidManifest.xml` file) + +```xml + +``` + +You'll also need to add the next attributes to the activity you're opening, usually the class that extends FlutterActivity. +These make sure the screen turns on and shows when the device is locked. +```xml + +``` + For reference, the example app's `AndroidManifest.xml` file can be found [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml) @@ -186,44 +235,44 @@ Here is an example: ```dart // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project -var initializationSettingsAndroid = +const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); -var initializationSettingsIOS = IOSInitializationSettings( - onDidReceiveLocalNotification: onDidReceiveLocalNotification); -var initializationSettings = InitializationSettings( +final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + onDidReceiveLocalNotification: onDidReceiveLocalNotification); +final InitializationSettings initializationSettings = InitializationSettings( initializationSettingsAndroid, initializationSettingsIOS); flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: onSelectNotification); ... - Future onDidReceiveLocalNotification( - int id, String title, String body, String payload) async { - // display a dialog with the notification details, tap ok to go to another page - showDialog( - context: context, - builder: (BuildContext context) => CupertinoAlertDialog( - title: Text(title), - content: Text(body), - actions: [ - CupertinoDialogAction( - isDefaultAction: true, - child: Text('Ok'), - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SecondScreen(payload), - ), - ); - }, - ) - ], - ), - ); - } - +Future onDidReceiveLocalNotification( + int id, String title, String body, String payload) async { + // display a dialog with the notification details, tap ok to go to another page + showDialog( + context: context, + builder: (BuildContext context) => CupertinoAlertDialog( + title: Text(title), + content: Text(body), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + child: Text('Ok'), + onPressed: () async { + Navigator.of(context, rootNavigator: true).pop(); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SecondScreen(payload), + ), + ); + }, + ) + ], + ), + ); +} ``` @@ -244,13 +293,20 @@ Checkout the lovely [API documentation](https://pub.dev/documentation/flutter_lo The first step is to create a new instance of the plugin class and then initialise it with the settings to use for each platform ```dart -FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); +FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project -var initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); -var initializationSettingsIOS = IOSInitializationSettings( - onDidReceiveLocalNotification: onDidReceiveLocalNotification); -var initializationSettings = InitializationSettings( - initializationSettingsAndroid, initializationSettingsIOS); +const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('app_icon'); +final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + onDidReceiveLocalNotification: onDidReceiveLocalNotification); +final MacOSInitializationSettings initializationSettingsMacOS = + MacOSInitializationSettings(); +final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + macOS: initializationSettingsMacOS); await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification); ``` @@ -260,157 +316,180 @@ Initialisation should only be done **once**, and this can be done is in the `mai ```dart Future selectNotification(String payload) async { if (payload != null) { - debugPrint('notification payload: ' + payload); + debugPrint('notification payload: $payload'); } await Navigator.push( context, - MaterialPageRoute(builder: (context) => SecondScreen(payload)), + MaterialPageRoute(builder: (context) => SecondScreen(payload)), ); } ``` In the real world, this payload could represent the id of the item you want to display the details of. Once the initialisation is complete, then you can manage the displaying of notifications. -On iOS, initialisation may show a prompt to requires users to give the application permission to display notifications (note: permissions don't need to be requested on Android). Depending on when this happens, this may not be the ideal user experience for your application. If so, please refer to the next section on how to work around this. +The `IOSInitializationSettings` and `MacOSInitializationSettings` provides default settings on how the notification be presented when it is triggered and the application is in the foreground. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled. + +On iOS and macOS, initialisation may show a prompt to requires users to give the application permission to display notifications (note: permissions don't need to be requested on Android). Depending on when this happens, this may not be the ideal user experience for your application. If so, please refer to the next section on how to work around this. + + ⚠ If the app has been launched by tapping on a notification created by this plugin, calling `initialize` is what will trigger the `onSelectNotification` to trigger to handle the notification that the user tapped on. An alternative to handling the "launch notification" is to call the `getNotificationAppLaunchDetails` method that is available in the plugin. This could be used, for example, to change the home route of the app for deep-linking. Calling `initialize` will still cause the `onSelectNotification` callback to fire for the launch notification. It will be up to developers to ensure that they don't process the same notification twice (e.g. by storing and comparing the notification id). -### [iOS only] Requesting notification permissions +### [iOS (all supported versions) and macOS 10.14+] Requesting notification permissions -The constructor for the `IOSInitializationSettings` class has three named parameters (`requestSoundPermission`, `requestBadgePermission` and `requestAlertPermission`) that controls which permissions are being requested. If you want to request permissions at a later point in your application on iOS, set all of the above to false when initialising the plugin. +The constructor for the `IOSInitializationSettings` and `MacOSInitializationSettings` classes has three named parameters (`requestSoundPermission`, `requestBadgePermission` and `requestAlertPermission`) that controls which permissions are being requested. If you want to request permissions at a later point in your application on iOS, set all of the above to false when initialising the plugin. ```dart -FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); -var initializationSettingsAndroid = - AndroidInitializationSettings('app_icon'); -var initializationSettingsIOS = IOSInitializationSettings( - requestSoundPermission: false, - requestBadgePermission: false, - requestAlertPermission: false, - onDidReceiveLocalNotification: onDidReceiveLocalNotification, - ); -var initializationSettings = InitializationSettings( - initializationSettingsAndroid, initializationSettingsIOS); -await flutterLocalNotificationsPlugin.initialize(initializationSettings, - onSelectNotification: onSelectNotification); + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('app_icon'); + final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + requestSoundPermission: false, + requestBadgePermission: false, + requestAlertPermission: false, + onDidReceiveLocalNotification: onDidReceiveLocalNotification, + ); + final MacOSInitializationSettings initializationSettingsMacOS = + MacOSInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false); + final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + macOS: initializationSettingsMacOS); + await flutterLocalNotificationsPlugin.initialize(initializationSettings, + onSelectNotification: onSelectNotification); ``` Then call the `requestPermissions` method with desired permissions at the appropriate point in your application + +For iOS: + +```dart +final bool result = await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); +``` + +For macOS: + ```dart -var result = await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() - ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ); +final bool result = await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); ``` -Here the call to `flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()` returns the iOS implementation of the plugin that contains APIs specific to iOS if the application is running on iOS. The `?.` operator is used here as the result will be null when run on other platforms. Developers may alternatively choose to guard this call by checking the platform their application is running on. +Here the call to `flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()` returns the iOS implementation of the plugin that contains APIs specific to iOS if the application is running on iOS. Similarly, the macOS implementation is returned by calling `flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()`. The `?.` operator is used as the result will be null when run on other platforms. Developers may alternatively choose to guard this call by checking the platform their application is running on. ### Displaying a notification ```dart -var androidPlatformChannelSpecifics = AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', - importance: Importance.Max, priority: Priority.High, ticker: 'ticker'); -var iOSPlatformChannelSpecifics = IOSNotificationDetails(); -var platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); +const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'your channel id', 'your channel name', 'your channel description', + importance: Importance.max, + priority: Priority.high, + showWhen: false); +const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.show( 0, 'plain title', 'plain body', platformChannelSpecifics, payload: 'item x'); ``` -In this block of code, the details for each platform have been specified. This includes the channel details that is required for Android 8.0+. The payload has been specified ('item x'), that will passed back through your application when the user has tapped on a notification. +In this block of code, the details specific to the Android platform is specified. This includes the channel details that is required for Android 8.0+. Whilst not shown, it's possible to specify details for iOS and macOS as well using the optional `iOS` and `macOS` named parameters if needed. The payload has been specified ('item x'), that will passed back through your application when the user has tapped on a notification. Note that for Android devices that notifications will only in appear in the tray and won't appear as a toast aka heads-up notification unless things like the priority/importance has been set appropriately. Refer to the Android docs (https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Heads-up) for additional information. Note that the "ticker" text is passed here though it is optional and specific to Android. This allows for text to be shown in the status bar on older versions of Android when the notification is shown. -On Android devices, notifications will **only** in appear in the tray and **won't** appear as a toast (heads-up notification) unless things like the priority/importance has been set appropriately. Refer to the [Android docs] (https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Heads-up) for more information. +### Scheduling a notification -The "ticker" text is passed here though it is optional and specific to Android. This allows for text to be shown in the status bar on older versions of Android when the notification is shown. +Starting in version 1.5 of the plugin, scheduling notifications now requires developers to specify a date and time relative to a specific time zone. This is to solve issues with daylight savings that existed in the `schedule` method that is now deprecated. A new `zonedSchedule` method is provided that expects an instance `TZDateTime` class provided by the [`timezone`](https://pub.dev/packages/timezone) package. As the `flutter_local_notifications` plugin already depends on the `timezone` package, it's not necessary for developers to add the `timezone` package as a direct dependency. In other words, the `timezone` package will be a transitive dependency after you add the `flutter_local_notifications` plugin as a dependency in your application. -### Scheduling a notification +Usage of the `timezone` package requires initialisation that is covered in the package's readme. For convenience the following are code snippets used by the example app. + +Import the `timezone` package ```dart -var scheduledNotificationDateTime = - DateTime.now().add(Duration(seconds: 5)); -var androidPlatformChannelSpecifics = - AndroidNotificationDetails('your other channel id', - 'your other channel name', 'your other channel description'); -var iOSPlatformChannelSpecifics = - IOSNotificationDetails(); -NotificationDetails platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); -await flutterLocalNotificationsPlugin.schedule( - 0, - 'scheduled title', - 'scheduled body', - scheduledNotificationDateTime, - platformChannelSpecifics); +import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/timezone.dart' as tz; ``` -On Android devices, the default behaviour is that the notification may not be delivered at the specified time when the device in a low-power idle mode. This behaviour can be changed by setting the optional parameter named `androidAllowWhileIdle` to true when calling the `schedule` method. +Initialise the time zone database -### Periodically showing a notification with a specified interval +```dart +tz.initializeTimeZones(); +``` + +Once the time zone database has been initialised, developers may optionally want to set a default local location/time zone ```dart -// Show a notification every minute with the first appearance happening a minute after invoking the method -var androidPlatformChannelSpecifics = - AndroidNotificationDetails('repeating channel id', - 'repeating channel name', 'repeating description'); -var iOSPlatformChannelSpecifics = - IOSNotificationDetails(); -var platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); -await flutterLocalNotificationsPlugin.periodicallyShow(0, 'repeating title', - 'repeating body', RepeatInterval.EveryMinute, platformChannelSpecifics); +tz.setLocalLocation(tz.getLocation(timeZoneName)); ``` -### Showing a daily notification at a specific time +The `timezone` package doesn't provide a way to obtain the current time zone on the device so developers will need to use platform channels (which is what the example app does) or use other packages that may be able to provide the information (e.g. [`flutter_native_timezone`](https://pub.dev/packages/flutter_native_timezone)). + +Assuming the local location has been set, the `zonedScheduled` method can then be called in a manner similar to the following code ```dart -var time = Time(10, 0, 0); -var androidPlatformChannelSpecifics = - AndroidNotificationDetails('repeatDailyAtTime channel id', - 'repeatDailyAtTime channel name', 'repeatDailyAtTime description'); -var iOSPlatformChannelSpecifics = - IOSNotificationDetails(); -var platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); -await flutterLocalNotificationsPlugin.showDailyAtTime( +await flutterLocalNotificationsPlugin.zonedSchedule( 0, - 'show daily title', - 'Daily notification shown at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}', - time, - platformChannelSpecifics); + 'scheduled title', + 'scheduled body', + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), + const NotificationDetails( + android: AndroidNotificationDetails('your channel id', + 'your channel name', 'your channel description')), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime); ``` -### Showing a weekly notification on specific day and time +On Android, the `androidAllowWhileIdle` is used to determine if the notification should be delivered at the specified time even when the device in a low-power idle mode. + +The `uiLocalNotificationDateInterpretation` is required as on iOS versions older than 10 as time zone support is limited. This means it's not possible schedule a notification for another time zone and have iOS adjust the time the notification will appear when daylight savings happens. With this parameter, it is used to determine if the scheduled date should be interpreted as absolute time or wall clock time. + +There is an optional `scheduledNotificationRepeatFrequency` parameter that can be used to schedule a notification to appear on a daily or weekly basis. + +### Periodically show a notification with a specified interval ```dart -var time = Time(10, 0, 0); -var androidPlatformChannelSpecifics = - AndroidNotificationDetails('show weekly channel id', - 'show weekly channel name', 'show weekly description'); -var iOSPlatformChannelSpecifics = - IOSNotificationDetails(); -var platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); -await flutterLocalNotificationsPlugin.showWeeklyAtDayAndTime( - 0, - 'show weekly title', - 'Weekly notification shown on Monday at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}', - Day.Monday, - time, - platformChannelSpecifics); +const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails('repeating channel id', + 'repeating channel name', 'repeating description'); +const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); +await flutterLocalNotificationsPlugin.periodicallyShow(0, 'repeating title', + 'repeating body', RepeatInterval.everyMinute, platformChannelSpecifics, + androidAllowWhileIdle: true); ``` ### Retrieveing pending notification requests ```dart -var pendingNotificationRequests = - await flutterLocalNotificationsPlugin.pendingNotificationRequests(); +final List pendingNotificationRequests = + await flutterLocalNotificationsPlugin.pendingNotificationRequests(); +``` + +### [Android only] Retrieving active notifications + +```dart +final List activeNotifications = + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.getActiveNotifications(); ``` ### [Android only] Grouping notifications @@ -419,53 +498,58 @@ This is a "translation" of the sample available at https://developer.android.com For iOS, you could just display the summary notification (not shown in the example) as otherwise the following code would show three notifications ```dart -String groupKey = 'com.android.example.WORK_EMAIL'; -String groupChannelId = 'grouped channel id'; -String groupChannelName = 'grouped channel name'; -String groupChannelDescription = 'grouped channel description'; +const String groupKey = 'com.android.example.WORK_EMAIL'; +const String groupChannelId = 'grouped channel id'; +const String groupChannelName = 'grouped channel name'; +const String groupChannelDescription = 'grouped channel description'; // example based on https://developer.android.com/training/notify-user/group.html -AndroidNotificationDetails firstNotificationAndroidSpecifics = +const AndroidNotificationDetails firstNotificationAndroidSpecifics = AndroidNotificationDetails( groupChannelId, groupChannelName, groupChannelDescription, - importance: Importance.Max, - priority: Priority.High, + importance: Importance.max, + priority: Priority.high, groupKey: groupKey); -NotificationDetails firstNotificationPlatformSpecifics = - NotificationDetails(firstNotificationAndroidSpecifics, null); +const NotificationDetails firstNotificationPlatformSpecifics = + NotificationDetails(android: firstNotificationAndroidSpecifics); await flutterLocalNotificationsPlugin.show(1, 'Alex Faarborg', 'You will not believe...', firstNotificationPlatformSpecifics); -AndroidNotificationDetails secondNotificationAndroidSpecifics = +const AndroidNotificationDetails secondNotificationAndroidSpecifics = AndroidNotificationDetails( groupChannelId, groupChannelName, groupChannelDescription, - importance: Importance.Max, - priority: Priority.High, + importance: Importance.max, + priority: Priority.high, groupKey: groupKey); -NotificationDetails secondNotificationPlatformSpecifics = - NotificationDetails(secondNotificationAndroidSpecifics, null); +const NotificationDetails secondNotificationPlatformSpecifics = + NotificationDetails(android: secondNotificationAndroidSpecifics); await flutterLocalNotificationsPlugin.show( 2, 'Jeff Chang', 'Please join us to celebrate the...', secondNotificationPlatformSpecifics); -// create the summary notification required for older devices that pre-date Android 7.0 (API level 24) -List lines = List(); -lines.add('Alex Faarborg Check this out'); -lines.add('Jeff Chang Launch Party'); -InboxStyleInformation inboxStyleInformation = InboxStyleInformation( +// Create the summary notification to support older devices that pre-date +/// Android 7.0 (API level 24). +/// +/// Recommended to create this regardless as the behaviour may vary as +/// mentioned in https://developer.android.com/training/notify-user/group +const List lines = [ + 'Alex Faarborg Check this out', + 'Jeff Chang Launch Party' +]; +const InboxStyleInformation inboxStyleInformation = InboxStyleInformation( lines, - contentTitle: '2 new messages', + contentTitle: '2 messages', summaryText: 'janedoe@example.com'); -AndroidNotificationDetails androidPlatformChannelSpecifics = +const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( groupChannelId, groupChannelName, groupChannelDescription, styleInformation: inboxStyleInformation, groupKey: groupKey, setAsGroupSummary: true); -NotificationDetails platformChannelSpecifics = - NotificationDetails(androidPlatformChannelSpecifics, null); +const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.show( - 3, 'Attention', 'Two new messages', platformChannelSpecifics); + 3, 'Attention', 'Two messages', platformChannelSpecifics); ``` ### Cancelling/deleting a notification @@ -485,8 +569,8 @@ await flutterLocalNotificationsPlugin.cancelAll(); ### Getting details on if the app was launched via a notification created by this plugin ```dart - var notificationAppLaunchDetails = - await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); +final NotificationAppLaunchDetails notificationAppLaunchDetails = + await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); ``` ### [iOS only] Periodic notifications showing up after reinstallation @@ -511,3 +595,14 @@ if(!UserDefaults.standard.bool(forKey: "Notification")) { UserDefaults.standard.set(true, forKey: "Notification") } ``` + +## πŸ“ˆ Testing + +As the plugin class is not static, it is possible to mock and verify its behaviour when writing tests as part of your application. +Check the source code for a sample test suite that has been kindly implemented (_test/flutter_local_notifications_test.dart_) that demonstrates how this can be done. + +If you decide to use the plugin class directly as part of your tests, the methods will be mostly no-op and methods that return data will return default values. + +Part of this is because the plugin detects if you're running on a supported plugin to determine which platform implementation of the plugin should be used. If it's neither Android or iOS, then it defaults to the aforementioned behaviour to reduce friction when writing tests. If this not desired then consider using mocks. + +If a platform-specific implementation of the plugin is required for your tests, a [named constructor](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/FlutterLocalNotificationsPlugin.private.html) is available that allows you to specify the platform required e.g. a [`FakePlatform`](https://api.flutter.dev/flutter/package-platform_platform/FakePlatform-class.html). diff --git a/flutter_local_notifications/analysis_options.yaml b/flutter_local_notifications/analysis_options.yaml deleted file mode 100644 index 9653b58e1..000000000 --- a/flutter_local_notifications/analysis_options.yaml +++ /dev/null @@ -1,5 +0,0 @@ -include: package:pedantic/analysis_options.yaml - -linter: - rules: - - sort_constructors_first \ No newline at end of file diff --git a/flutter_local_notifications/android/build.gradle b/flutter_local_notifications/android/build.gradle index c4935b497..5208dad42 100644 --- a/flutter_local_notifications/android/build.gradle +++ b/flutter_local_notifications/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:3.6.3' } } @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 16 @@ -34,7 +34,8 @@ android { } dependencies { - implementation "androidx.core:core:1.2.0" + implementation "androidx.core:core:1.3.0" implementation "androidx.media:media:1.1.0" - implementation "com.google.code.gson:gson:2.8.5" + implementation "com.google.code.gson:gson:2.8.6" + implementation "com.jakewharton.threetenabp:threetenabp:1.2.3" } 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 9686576fb..0e64b0693 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 @@ -15,7 +15,9 @@ import android.media.AudioAttributes; import android.media.RingtoneManager; import android.net.Uri; -import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.service.notification.StatusBarNotification; import android.text.Html; import android.text.Spanned; @@ -32,6 +34,7 @@ import com.dexterous.flutterlocalnotifications.models.NotificationChannelDetails; import com.dexterous.flutterlocalnotifications.models.NotificationDetails; import com.dexterous.flutterlocalnotifications.models.PersonDetails; +import com.dexterous.flutterlocalnotifications.models.ScheduledNotificationRepeatFrequency; import com.dexterous.flutterlocalnotifications.models.styles.BigPictureStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.BigTextStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.DefaultStyleInformation; @@ -43,10 +46,15 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; +import com.jakewharton.threetenabp.AndroidThreeTen; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; @@ -78,11 +86,13 @@ public class FlutterLocalNotificationsPlugin implements MethodCallHandler, Plugi private static final String INITIALIZE_METHOD = "initialize"; private static final String CREATE_NOTIFICATION_CHANNEL_METHOD = "createNotificationChannel"; private static final String DELETE_NOTIFICATION_CHANNEL_METHOD = "deleteNotificationChannel"; + private static final String GET_ACTIVE_NOTIFICATIONS_METHOD = "getActiveNotifications"; private static final String PENDING_NOTIFICATION_REQUESTS_METHOD = "pendingNotificationRequests"; private static final String SHOW_METHOD = "show"; private static final String CANCEL_METHOD = "cancel"; private static final String CANCEL_ALL_METHOD = "cancelAll"; private static final String SCHEDULE_METHOD = "schedule"; + private static final String ZONED_SCHEDULE_METHOD = "zonedSchedule"; private static final String PERIODICALLY_SHOW_METHOD = "periodicallyShow"; private static final String SHOW_DAILY_AT_TIME_METHOD = "showDailyAtTime"; private static final String SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD = "showWeeklyAtDayAndTime"; @@ -94,19 +104,18 @@ public class FlutterLocalNotificationsPlugin implements MethodCallHandler, Plugi private static final String INVALID_BIG_PICTURE_ERROR_CODE = "INVALID_BIG_PICTURE"; private static final String INVALID_SOUND_ERROR_CODE = "INVALID_SOUND"; private static final String INVALID_LED_DETAILS_ERROR_CODE = "INVALID_LED_DETAILS"; + private static final String GET_ACTIVE_NOTIFICATIONS_ERROR_CODE = "GET_ACTIVE_NOTIFICATIONS_ERROR_CODE"; + private static final String GET_ACTIVE_NOTIFICATIONS_ERROR_MESSAGE = "Android version must be 6.0 or newer to use getActiveNotifications"; private static final String INVALID_LED_DETAILS_ERROR_MESSAGE = "Must specify both ledOnMs and ledOffMs to configure the blink cycle on older versions of Android before Oreo"; private static final String NOTIFICATION_LAUNCHED_APP = "notificationLaunchedApp"; private static final String INVALID_DRAWABLE_RESOURCE_ERROR_MESSAGE = "The resource %s could not be found. Please make sure it has been added as a drawable resource to your Android head project."; private static final String INVALID_RAW_RESOURCE_ERROR_MESSAGE = "The resource %s could not be found. Please make sure it has been added as a raw resource to your Android head project."; - static String NOTIFICATION_ID = "notification_id"; - static String NOTIFICATION = "notification"; static String NOTIFICATION_DETAILS = "notificationDetails"; - static String REPEAT = "repeat"; static Gson gson; private MethodChannel channel; private Context applicationContext; private Activity mainActivity; - private boolean initialized; + private Intent launchIntent; public static void registerWith(Registrar registrar) { FlutterLocalNotificationsPlugin plugin = new FlutterLocalNotificationsPlugin(); @@ -116,16 +125,27 @@ public static void registerWith(Registrar registrar) { } static void rescheduleNotifications(Context context) { + initAndroidThreeTen(context); ArrayList scheduledNotifications = loadScheduledNotifications(context); for (NotificationDetails scheduledNotification : scheduledNotifications) { if (scheduledNotification.repeatInterval == null) { - scheduleNotification(context, scheduledNotification, false); + if (scheduledNotification.timeZoneName == null) { + scheduleNotification(context, scheduledNotification, false); + } else { + zonedScheduleNotification(context, scheduledNotification, false); + } } else { repeatNotification(context, scheduledNotification, false); } } } + private static void initAndroidThreeTen(Context context) { + if (VERSION.SDK_INT < VERSION_CODES.O) { + AndroidThreeTen.init(context); + } + } + private static Notification createNotification(Context context, NotificationDetails notificationDetails) { setupNotificationChannel(context, NotificationChannelDetails.fromNotificationDetails(notificationDetails)); Intent intent = new Intent(context, getMainActivityClass(context)); @@ -159,6 +179,14 @@ private static Notification createNotification(Context context, NotificationDeta builder.setWhen(notificationDetails.when); } + if (BooleanUtils.getValue(notificationDetails.fullScreenIntent)) { + builder.setFullScreenIntent(pendingIntent, true); + } + + if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { + builder.setShortcutId(notificationDetails.shortcutId); + } + setVisibility(notificationDetails, builder); applyGrouping(notificationDetails, builder); setSound(context, notificationDetails, builder); @@ -232,7 +260,7 @@ private static void saveScheduledNotifications(Context context, ArrayList scheduledNotifications = loadScheduledNotifications(context); for (Iterator it = scheduledNotifications.iterator(); it.hasNext(); ) { NotificationDetails notificationDetails = it.next(); @@ -249,7 +277,7 @@ private static Spanned fromHtml(String html) { if (html == null) { return null; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); } else { return Html.fromHtml(html); @@ -275,34 +303,41 @@ private static void scheduleNotification(Context context, final NotificationDeta } } - private static void repeatNotification(Context context, NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { + private static void zonedScheduleNotification(Context context, final NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { Gson gson = buildGson(); String notificationDetailsJson = gson.toJson(notificationDetails); Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); - notificationIntent.putExtra(REPEAT, true); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationDetails.id, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - AlarmManager alarmManager = getAlarmManager(context); - long repeatInterval = 0; - switch (notificationDetails.repeatInterval) { - case EveryMinute: - repeatInterval = 60000; - break; - case Hourly: - repeatInterval = 60000 * 60; - break; - case Daily: - repeatInterval = 60000 * 60 * 24; - break; - case Weekly: - repeatInterval = 60000 * 60 * 24 * 7; - break; - default: - break; + long epochMilli = VERSION.SDK_INT >= VERSION_CODES.O ? ZonedDateTime.of(LocalDateTime.parse(notificationDetails.scheduledDateTime), ZoneId.of(notificationDetails.timeZoneName)).toInstant().toEpochMilli() : org.threeten.bp.ZonedDateTime.of(org.threeten.bp.LocalDateTime.parse(notificationDetails.scheduledDateTime), org.threeten.bp.ZoneId.of(notificationDetails.timeZoneName)).toInstant().toEpochMilli(); + if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) { + AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent); + } else { + AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent); + } + if (updateScheduledNotificationsCache) { + saveScheduledNotification(context, notificationDetails); } + } + + static void scheduleNextRepeatingNotification(Context context, NotificationDetails notificationDetails) { + long repeatInterval = calculateRepeatIntervalMilliseconds(notificationDetails); + long notificationTriggerTime = calculateNextNotificationTrigger(notificationDetails.calledAt, repeatInterval); + Gson gson = buildGson(); + String notificationDetailsJson = gson.toJson(notificationDetails); + Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); + notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationDetails.id, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = getAlarmManager(context); + AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent); + saveScheduledNotification(context, notificationDetails); + } - long startTimeMilliseconds = notificationDetails.calledAt; + private static void repeatNotification(Context context, NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { + long repeatInterval = calculateRepeatIntervalMilliseconds(notificationDetails); + + long notificationTriggerTime = notificationDetails.calledAt; if (notificationDetails.repeatTime != null) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); @@ -313,22 +348,58 @@ private static void repeatNotification(Context context, NotificationDetails noti calendar.set(Calendar.DAY_OF_WEEK, notificationDetails.day); } - startTimeMilliseconds = calendar.getTimeInMillis(); + notificationTriggerTime = calendar.getTimeInMillis(); } - // ensure that start time is in the future - long currentTime = System.currentTimeMillis(); - while (startTimeMilliseconds < currentTime) { - startTimeMilliseconds += repeatInterval; - } + notificationTriggerTime = calculateNextNotificationTrigger(notificationTriggerTime, repeatInterval); - alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, startTimeMilliseconds, repeatInterval, pendingIntent); + Gson gson = buildGson(); + String notificationDetailsJson = gson.toJson(notificationDetails); + Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); + notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationDetails.id, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = getAlarmManager(context); + if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) { + AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent); + } else { + alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, notificationTriggerTime, repeatInterval, pendingIntent); + } if (updateScheduledNotificationsCache) { saveScheduledNotification(context, notificationDetails); } } + private static long calculateNextNotificationTrigger(long notificationTriggerTime, long repeatInterval) { + // ensures that time is in the future + long currentTime = System.currentTimeMillis(); + while (notificationTriggerTime < currentTime) { + notificationTriggerTime += repeatInterval; + } + return notificationTriggerTime; + } + + private static long calculateRepeatIntervalMilliseconds(NotificationDetails notificationDetails) { + long repeatInterval = 0; + switch (notificationDetails.repeatInterval) { + case EveryMinute: + repeatInterval = 60000; + break; + case Hourly: + repeatInterval = 60000 * 60; + break; + case Daily: + repeatInterval = 60000 * 60 * 24; + break; + case Weekly: + repeatInterval = 60000 * 60 * 24 * 7; + break; + default: + break; + } + return repeatInterval; + } + private static void saveScheduledNotification(Context context, NotificationDetails notificationDetails) { ArrayList scheduledNotifications = loadScheduledNotifications(context); ArrayList scheduledNotificationsToSave = new ArrayList<>(); @@ -625,7 +696,7 @@ private static void setBigTextStyle(NotificationDetails notificationDetails, Not } private static void setupNotificationChannel(Context context, NotificationChannelDetails notificationChannelDetails) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel notificationChannel = notificationManager.getNotificationChannel(notificationChannelDetails.id); // only create/update the channel when needed/specified. Allow this happen to when channelAction may be null to support cases where notifications had been @@ -690,12 +761,47 @@ static void showNotification(Context context, NotificationDetails notificationDe notificationManagerCompat.notify(notificationDetails.id, notification); } + static void zonedScheduleNextNotification(Context context, NotificationDetails notificationDetails) { + String nextFireDate = getNextFireDate(notificationDetails); + if (nextFireDate == null) { + return; + } + notificationDetails.scheduledDateTime = nextFireDate; + initAndroidThreeTen(context); + zonedScheduleNotification(context, notificationDetails, true); + } + + static String getNextFireDate(NotificationDetails notificationDetails) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + if (notificationDetails.scheduledNotificationRepeatFrequency == ScheduledNotificationRepeatFrequency.Daily) { + LocalDateTime localDateTime = LocalDateTime.parse(notificationDetails.scheduledDateTime).plusDays(1); + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime); + } else if (notificationDetails.scheduledNotificationRepeatFrequency == ScheduledNotificationRepeatFrequency.Weekly) { + LocalDateTime localDateTime = LocalDateTime.parse(notificationDetails.scheduledDateTime).plusWeeks(1); + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime); + } + } else { + if (notificationDetails.scheduledNotificationRepeatFrequency == ScheduledNotificationRepeatFrequency.Daily) { + org.threeten.bp.LocalDateTime localDateTime = org.threeten.bp.LocalDateTime.parse(notificationDetails.scheduledDateTime).plusDays(1); + return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime); + } else if (notificationDetails.scheduledNotificationRepeatFrequency == ScheduledNotificationRepeatFrequency.Weekly) { + org.threeten.bp.LocalDateTime localDateTime = org.threeten.bp.LocalDateTime.parse(notificationDetails.scheduledDateTime).plusWeeks(1); + return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime); + } + } + return null; + } + + private static NotificationManagerCompat getNotificationManager(Context context) { return NotificationManagerCompat.from(context); } private void setActivity(Activity flutterActivity) { this.mainActivity = flutterActivity; + if (mainActivity != null) { + launchIntent = mainActivity.getIntent(); + } } private void onAttachedToEngine(Context context, BinaryMessenger binaryMessenger) { @@ -717,6 +823,7 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { public void onAttachedToActivity(ActivityPluginBinding binding) { binding.addOnNewIntentListener(this); mainActivity = binding.getActivity(); + launchIntent = mainActivity.getIntent(); } @Override @@ -754,6 +861,10 @@ public void onMethodCall(MethodCall call, Result result) { schedule(call, result); break; } + case ZONED_SCHEDULE_METHOD: { + zonedSchedule(call, result); + break; + } case PERIODICALLY_SHOW_METHOD: case SHOW_DAILY_AT_TIME_METHOD: case SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD: { @@ -775,6 +886,9 @@ public void onMethodCall(MethodCall call, Result result) { case DELETE_NOTIFICATION_CHANNEL_METHOD: deleteNotificationChannel(call, result); break; + case GET_ACTIVE_NOTIFICATIONS_METHOD: + getActiveNotifications(result); + break; default: result.notImplemented(); break; @@ -820,6 +934,15 @@ private void schedule(MethodCall call, Result result) { } } + private void zonedSchedule(MethodCall call, Result result) { + Map arguments = call.arguments(); + NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + if (notificationDetails != null) { + zonedScheduleNotification(applicationContext, notificationDetails, true); + result.success(null); + } + } + private void show(MethodCall call, Result result) { Map arguments = call.arguments(); NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); @@ -832,10 +955,10 @@ private void show(MethodCall call, Result result) { private void getNotificationAppLaunchDetails(Result result) { Map notificationAppLaunchDetails = new HashMap<>(); String payload = null; - Boolean notificationLaunchedApp = !initialized && mainActivity != null && SELECT_NOTIFICATION.equals(mainActivity.getIntent().getAction()) && !launchedActivityFromHistory(mainActivity.getIntent()); + Boolean notificationLaunchedApp = mainActivity != null && SELECT_NOTIFICATION.equals(mainActivity.getIntent().getAction()) && !launchedActivityFromHistory(mainActivity.getIntent()); notificationAppLaunchDetails.put(NOTIFICATION_LAUNCHED_APP, notificationLaunchedApp); if (notificationLaunchedApp) { - payload = mainActivity.getIntent().getStringExtra(PAYLOAD); + payload = launchIntent.getStringExtra(PAYLOAD); } notificationAppLaunchDetails.put(PAYLOAD, payload); result.success(notificationAppLaunchDetails); @@ -847,6 +970,9 @@ private void initialize(MethodCall call, Result result) { if (!isValidDrawableResource(applicationContext, defaultIcon, result, INVALID_ICON_ERROR_CODE)) { return; } + + initAndroidThreeTen(applicationContext); + SharedPreferences sharedPreferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(DEFAULT_ICON, defaultIcon); @@ -855,7 +981,6 @@ private void initialize(MethodCall call, Result result) { if (mainActivity != null && !launchedActivityFromHistory(mainActivity.getIntent())) { sendNotificationPayloadMessage(mainActivity.getIntent()); } - initialized = true; result.success(true); } @@ -890,7 +1015,7 @@ private boolean hasInvalidRawSoundResource(Result result, NotificationDetails no if (!StringUtils.isNullOrEmpty(notificationDetails.sound) && (notificationDetails.soundSource == null || notificationDetails.soundSource == SoundSource.RawResource)) { int soundResourceId = applicationContext.getResources().getIdentifier(notificationDetails.sound, "raw", applicationContext.getPackageName()); if (soundResourceId == 0) { - result.error(INVALID_SOUND_ERROR_CODE, INVALID_RAW_RESOURCE_ERROR_MESSAGE, null); + result.error(INVALID_SOUND_ERROR_CODE, String.format(INVALID_RAW_RESOURCE_ERROR_MESSAGE, notificationDetails.sound), null); return true; } } @@ -922,7 +1047,7 @@ private void cancelNotification(Integer id) { alarmManager.cancel(pendingIntent); NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); notificationManager.cancel(id); - removeNotificationFromCache(id, applicationContext); + removeNotificationFromCache(applicationContext, id); } private void cancelAllNotifications(Result result) { @@ -972,11 +1097,38 @@ private void createNotificationChannel(MethodCall call, Result result) { } private void deleteNotificationChannel(MethodCall call, Result result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { NotificationManager notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); String channelId = call.arguments(); notificationManager.deleteNotificationChannel(channelId); result.success(null); } } + + private void getActiveNotifications(Result result) { + if (VERSION.SDK_INT < VERSION_CODES.M) { + result.error(GET_ACTIVE_NOTIFICATIONS_ERROR_CODE, GET_ACTIVE_NOTIFICATIONS_ERROR_MESSAGE, null); + return; + } + NotificationManager notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); + try { + StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications(); + List> activeNotificationsPayload = new ArrayList<>(); + + for (StatusBarNotification activeNotification : activeNotifications) { + HashMap activeNotificationPayload = new HashMap<>(); + activeNotificationPayload.put("id", activeNotification.getId()); + Notification notification = activeNotification.getNotification(); + if (VERSION.SDK_INT >= VERSION_CODES.O) { + activeNotificationPayload.put("channelId", notification.getChannelId()); + } + activeNotificationPayload.put("title", notification.extras.getString("android.title")); + activeNotificationPayload.put("body", notification.extras.getString("android.text")); + activeNotificationsPayload.add(activeNotificationPayload); + } + result.success(activeNotificationsPayload); + } catch (Throwable e) { + result.error(GET_ACTIVE_NOTIFICATIONS_ERROR_CODE, e.getMessage(), e.getStackTrace()); + } + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java index c9c183787..d87bd368b 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; + import androidx.core.app.NotificationManagerCompat; import com.dexterous.flutterlocalnotifications.models.NotificationDetails; @@ -22,31 +23,32 @@ public class ScheduledNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); String notificationDetailsJson = intent.getStringExtra(FlutterLocalNotificationsPlugin.NOTIFICATION_DETAILS); - boolean repeat = intent.getBooleanExtra(FlutterLocalNotificationsPlugin.REPEAT, false); - - // TODO: remove this branching logic as it's legacy code to fix an issue where notifications weren't reporting the correct time - if(StringUtils.isNullOrEmpty(notificationDetailsJson)) { - Notification notification = intent.getParcelableExtra(FlutterLocalNotificationsPlugin.NOTIFICATION); + if (StringUtils.isNullOrEmpty(notificationDetailsJson)) { + // This logic is needed for apps that used the plugin prior to 0.3.4 + Notification notification = intent.getParcelableExtra("notification"); notification.when = System.currentTimeMillis(); - int notificationId = intent.getIntExtra(FlutterLocalNotificationsPlugin.NOTIFICATION_ID, + int notificationId = intent.getIntExtra("notification_id", 0); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(notificationId, notification); - if (repeat) { - return; + boolean repeat = intent.getBooleanExtra("repeat", false); + if (!repeat) { + FlutterLocalNotificationsPlugin.removeNotificationFromCache(context, notificationId); } - FlutterLocalNotificationsPlugin.removeNotificationFromCache(notificationId, context); } else { Gson gson = FlutterLocalNotificationsPlugin.buildGson(); Type type = new TypeToken() { }.getType(); - NotificationDetails notificationDetails = gson.fromJson(notificationDetailsJson, type); + NotificationDetails notificationDetails = gson.fromJson(notificationDetailsJson, type); FlutterLocalNotificationsPlugin.showNotification(context, notificationDetails); - if (repeat) { - return; + if (notificationDetails.scheduledNotificationRepeatFrequency != null) { + FlutterLocalNotificationsPlugin.zonedScheduleNextNotification(context, notificationDetails); + } else if (notificationDetails.repeatInterval != null) { + FlutterLocalNotificationsPlugin.scheduleNextRepeatingNotification(context, notificationDetails); + } else { + FlutterLocalNotificationsPlugin.removeNotificationFromCache(context, notificationDetails.id); } - FlutterLocalNotificationsPlugin.removeNotificationFromCache(notificationDetails.id, context); } } 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 5ac99d59b..512d6b897 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 @@ -1,7 +1,8 @@ package com.dexterous.flutterlocalnotifications.models; import android.graphics.Color; -import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import com.dexterous.flutterlocalnotifications.BitmapSource; import com.dexterous.flutterlocalnotifications.NotificationStyle; @@ -15,7 +16,6 @@ import com.dexterous.flutterlocalnotifications.models.styles.StyleInformation; import java.util.ArrayList; -import java.util.Iterator; import java.util.Map; public class NotificationDetails { @@ -105,6 +105,11 @@ public class NotificationDetails { private static final String WHEN = "when"; private static final String ADDITIONAL_FLAGS = "additionalFlags"; + private static final String SCHEDULED_DATE_TIME = "scheduledDateTime"; + private static final String TIME_ZONE_NAME = "timeZoneName"; + private static final String SCHEDULED_NOTIFICATION_REPEAT_FREQUENCY = "scheduledNotificationRepeatFrequency"; + private static final String FULL_SCREEN_INTENT = "fullScreenIntent"; + private static final String SHORTCUT_ID = "shortcutId"; public Integer id; public String title; @@ -154,7 +159,12 @@ public class NotificationDetails { public String category; public int[] additionalFlags; public Boolean showWhen; + public String scheduledDateTime; + public String timeZoneName; + public ScheduledNotificationRepeatFrequency scheduledNotificationRepeatFrequency; public Long when; + public Boolean fullScreenIntent; + public String shortcutId; @@ -167,6 +177,11 @@ public static NotificationDetails from(Map arguments) { notificationDetails.id = (Integer) arguments.get(ID); notificationDetails.title = (String) arguments.get(TITLE); notificationDetails.body = (String) arguments.get(BODY); + notificationDetails.scheduledDateTime = (String) arguments.get(SCHEDULED_DATE_TIME); + notificationDetails.timeZoneName = (String) arguments.get(TIME_ZONE_NAME); + if(arguments.containsKey(SCHEDULED_NOTIFICATION_REPEAT_FREQUENCY)) { + notificationDetails.scheduledNotificationRepeatFrequency = ScheduledNotificationRepeatFrequency.values()[(Integer) arguments.get(SCHEDULED_NOTIFICATION_REPEAT_FREQUENCY)]; + } if (arguments.containsKey(MILLISECONDS_SINCE_EPOCH)) { notificationDetails.millisecondsSinceEpoch = (Long) arguments.get(MILLISECONDS_SINCE_EPOCH); } @@ -184,6 +199,7 @@ public static NotificationDetails from(Map arguments) { if (arguments.containsKey(DAY)) { notificationDetails.day = (Integer) arguments.get(DAY); } + readPlatformSpecifics(arguments, notificationDetails); return notificationDetails; } @@ -215,6 +231,8 @@ private static void readPlatformSpecifics(Map arguments, Notific notificationDetails.allowWhileIdle = (Boolean) platformChannelSpecifics.get(ALLOW_WHILE_IDLE); notificationDetails.timeoutAfter = parseLong(platformChannelSpecifics.get(TIMEOUT_AFTER)); notificationDetails.category = (String) platformChannelSpecifics.get(CATEGORY); + notificationDetails.fullScreenIntent = (Boolean) platformChannelSpecifics.get((FULL_SCREEN_INTENT)); + notificationDetails.shortcutId = (String) platformChannelSpecifics.get(SHORTCUT_ID); notificationDetails.additionalFlags = (int[]) platformChannelSpecifics.get(ADDITIONAL_FLAGS); } } @@ -293,7 +311,7 @@ private static void readLedInformation(NotificationDetails notificationDetails, } private static void readChannelInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { notificationDetails.channelId = (String) platformChannelSpecifics.get(CHANNEL_ID); notificationDetails.channelName = (String) platformChannelSpecifics.get(CHANNEL_NAME); notificationDetails.channelDescription = (String) platformChannelSpecifics.get(CHANNEL_DESCRIPTION); diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/ScheduledNotificationRepeatFrequency.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/ScheduledNotificationRepeatFrequency.java new file mode 100644 index 000000000..2b48ad464 --- /dev/null +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/ScheduledNotificationRepeatFrequency.java @@ -0,0 +1,6 @@ +package com.dexterous.flutterlocalnotifications.models; + +public enum ScheduledNotificationRepeatFrequency { + Daily, + Weekly +} diff --git a/flutter_local_notifications/example/analysis_options.yaml b/flutter_local_notifications/example/analysis_options.yaml deleted file mode 100644 index d4fcc1ad8..000000000 --- a/flutter_local_notifications/example/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file diff --git a/flutter_local_notifications/example/android/app/build.gradle b/flutter_local_notifications/example/android/app/build.gradle index 9fe71508c..5ba544458 100644 --- a/flutter_local_notifications/example/android/app/build.gradle +++ b/flutter_local_notifications/example/android/app/build.gradle @@ -64,4 +64,5 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation "androidx.core:core:1.2.0" + implementation "com.jakewharton.threetenabp:threetenabp:1.2.3" } diff --git a/flutter_local_notifications/example/android/app/proguard-rules.pro b/flutter_local_notifications/example/android/app/proguard-rules.pro index 26c2d5a10..bc8b29c75 100644 --- a/flutter_local_notifications/example/android/app/proguard-rules.pro +++ b/flutter_local_notifications/example/android/app/proguard-rules.pro @@ -21,7 +21,7 @@ # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) --keep class * implements com.google.gson.TypeAdapter +-keep class * extends com.google.gson.TypeAdapter -keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonDeserializer diff --git a/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutterlocalnotifications/MainActivityTest.java b/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutterlocalnotifications/MainActivityTest.java index 4e9147972..5c60399f6 100644 --- a/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutterlocalnotifications/MainActivityTest.java +++ b/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutterlocalnotifications/MainActivityTest.java @@ -3,12 +3,13 @@ import androidx.test.rule.ActivityTestRule; import com.dexterous.flutterlocalnotificationsexample.MainActivity; -import dev.flutter.plugins.e2e.FlutterRunner; + +import dev.flutter.plugins.e2e.FlutterTestRunner; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) +@RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule - public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} + public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); +} \ No newline at end of file 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 482abd0fa..974e807d5 100644 --- a/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml +++ b/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ +