diff --git a/.cirrus.yml b/.cirrus.yml index b0b47f219..1e22aa7a6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,9 +11,12 @@ task: task: name: Build iOS example app osx_instance: - image: catalina-flutter + image: big-sur-xcode-12.4 pub_cache: folder: ~/.pub-cache + upgrade_script: + - flutter channel stable + - flutter upgrade build_script: - cd flutter_local_notifications/example - flutter build ios --no-codesign --debug @@ -21,11 +24,11 @@ task: task: name: Build macOS example app osx_instance: - image: catalina-flutter + image: big-sur-xcode-12.4 pub_cache: folder: ~/.pub-cache upgrade_script: - - flutter channel master + - flutter channel stable - flutter upgrade setup_script: - flutter config --enable-macos-desktop @@ -33,6 +36,20 @@ task: - cd flutter_local_notifications/example - flutter build macos +task: + name: Build Linux example app + container: + image: cirrusci/flutter:stable + pub_cache: + folder: ~/.pub-cache + setup_script: + - apt update + - apt install cmake ninja-build clang pkg-config libgtk-3-dev -y + - flutter config --enable-linux-desktop + build_script: + - cd flutter_local_notifications/example + - flutter build linux + task: name: Run platform interface tests container: @@ -73,4 +90,14 @@ task: test_script: - cd flutter_local_notifications - cd example - - flutter drive --driver=test_driver/flutter_local_notifications_e2e_test.dart test_driver/flutter_local_notifications_e2e.dart + - flutter drive --driver=test_driver/integration_test.dart --target=integration_test/flutter_local_notifications_test.dart + +task: + name: Run Linux plugin tests + container: + image: cirrusci/flutter:stable + pub_cache: + folder: ~/.pub-cache + test_script: + - cd flutter_local_notifications_linux + - flutter test diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 000000000..a5508881c --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,35 @@ +name: format + +on: push + +jobs: + + java_format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 # v2 minimum required + - uses: axel-op/googlejavaformat-action@v3 + with: + args: "--skip-sorting-imports --replace" + + objc_format: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 # v2 minimum required + - name: Test + run: | + which clang-format || brew install clang-format + find . -name '*.m' -exec clang-format -i {} \; + find . -path '*/ios/**/*.h' -exec clang-format -i {} \; + find . -path '*/macos/**/*.h' -exec clang-format -i {} \; + git diff --exit-code || (git commit --all -m "Clang Format" && git push) + + swift_format: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 # v2 minimum required + - name: Test + run: | + which swiftlint || brew install swiftlint + swiftlint --fix + git diff --exit-code || (git commit --all -m "Swift Format" && git push) diff --git a/.gitignore b/.gitignore index 34c62e31c..1a2736df2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -pubspec.lock \ No newline at end of file +pubspec.lock +.vscode diff --git a/README.md b/README.md index 75048dca1..097ebfe84 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ This repository consists hosts the following packages - [`flutter_local_notifications`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications): code for the cross-platform facing plugin used to display local notifications within Flutter applications - [`flutter_local_notifications_platform_interface`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface): the code for the common platform interface +- [`flutter_local_notifications_linux`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_linux): the Linux implementation of [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications). These can be found in the corresponding directories within the same name. Most developers are likely here as they are looking to use the `flutter_local_notifications` plugin. There is a readme file within each directory with more information. diff --git a/analysis_options.yaml b/analysis_options.yaml index 8cfe1ab41..4ee9df615 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,7 @@ linter: - always_specify_types - annotate_overrides - avoid_annotating_with_dynamic - - avoid_as + # - avoid_as - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses - avoid_catching_errors @@ -128,7 +128,7 @@ linter: - slash_for_doc_comments - sort_child_properties_last - sort_constructors_first - - sort_pub_dependencies + # - sort_pub_dependencies - sort_unnamed_constructors_first - test_types_in_equals - throw_in_finally diff --git a/flutter_local_notifications/CHANGELOG.md b/flutter_local_notifications/CHANGELOG.md index 1d1b1bbe5..0dc98e6a2 100644 --- a/flutter_local_notifications/CHANGELOG.md +++ b/flutter_local_notifications/CHANGELOG.md @@ -1,3 +1,166 @@ +# [9.1.4] + +* [Android] Reverted change in 9.1.0 that added the `groupKey` to `ActiveNotification` as this was a potentially breaking change. This will instead be part of a major release + +# [9.1.3] + +* [Android] Reverts Android changes done in 9.1.2 and 9.1.1 due to reported stability issues. Ths means issue [1378](https://github.com/MaikuB/flutter_local_notifications/issues/1378) may still occur though is a rare occurrence and may require a different solution and assistance from the community with regards to testing + +# [9.1.2+1] + +* **BAD** [Android] some minor code clean up from 9.1.2 changes +* Fixed a grammar issue in readme. Thanks to the PR from [ClΓ©ment Besnier](https://github.com/clemsciences) + + +# [9.1.2] + +* [Android] Fix NPE issue [1378](https://github.com/MaikuB/flutter_local_notifications/issues/1387) from change introduced in 9.1.1 in updating how notifications were written to shared preferences + +# [9.1.1] + +* **BAD** [Android] updated APIs the plugin uses to write to shared preferences in the background +* [Android] fix where there was a the `Future` for scheduling a notification could be completed prior to saving information on the scheduled notification to shared preferences. In this case the notification would still be scheduled but if the plugin was used to query the pending notifications quick enough, the plugin may have returned the incorrect number of pending notifications + +# [9.1.0] + +* **BAD** [Android] Added `groupKey` to `ActiveNotification` that would allow for finding the notification's group. Thanks to the PR from [Roman](https://github.com/drstranges) +* [Android] Migrate maven repository from jcenter to mavenCentral. Thanks to the PR from [tigertore](https://github.com/tigertore) + + +# [9.0.3] + +* [Android] Fixed issue [1362](https://github.com/MaikuB/flutter_local_notifications/issues/1362) so that the plugin refer to Android sound resources by the resource name instead of the resource id as the resource id could change over time e.g. if new resources are added. Note that this is a fix that can't be applied retroactively + +# [9.0.2] + +* [Android] Fixed issue [1357](https://github.com/MaikuB/flutter_local_notifications/issues/1357) where some details of a notification with formatted content weren't being returned +* Bumped dependencies used by example app +* Fixed grammar issue in readme in the `Scheduled Android notifications` section. Thanks to [Yousef Akiba](https://github.com/yousefakiba) for the PR +* Updated example app and readme code around importing the `timezone` dependency so it uses the `all` variant of the IANA database that contains all timezones including those that are deprecated or may link to other timezones + +# [9.0.1] + +* Fixed issue [1346](https://github.com/MaikuB/flutter_local_notifications/issues/1346) where an exception is thrown when `onSelectNotification` callback isn't specified + +# [9.0.0] + +* **Breaking change** the `SelectNotificationCallback` and `DidReceiveLocalNotificationCallback` typedefs now map to functions that returns `void` instead of a `Future`. This change was done to better communicate the plugin doesn't actually await any asynchronous computation and is similar to how button pressed callbacks work for Flutter where they are typically use [`VoidCallback`](https://api.flutter.dev/flutter/dart-ui/VoidCallback.html) +* Updated example app to show how to display notification where a byte array is used to specify the icon on Linux +* **Breaking change** the `value` property of the `Importance` class is now non-nullable +* **Breaking change** the `FlutterLocalNotificationsPlugin.private()` constructor that was visible for testing purposes has been removed. The plugin now uses the [`defaultTargetPlatform`](https://api.flutter.dev/flutter/foundation/defaultTargetPlatform.html) property from the Flutter framework to determine the platform an application running on. This removes the need for depending on the `platform` package. To write tests that require a platform-specific implementation of the plugin, the [debugDefaultTargetPlatformOverride](https://api.flutter.dev/flutter/foundation/debugDefaultTargetPlatformOverride.html) property can be used to do so +* **Breaking change** fixed issue [1306](https://github.com/MaikuB/flutter_local_notifications/issues/1306) where an Android notification channel description should have been optional. This means the `description` property of the `AndroidNotificationChannel` class and the `channelDescription` property of the `AndroidNotificationDetails` class are now named parameters +* **Breaking change** the `AndroidIcon` class is now a generic class i.e. `AndroidIcon` and it's `icon` property has been renamed to `data`. With this change, the type of the `icon` property that belongs to the `Person` class has changed from `AndroidIcon?` to `AndroidIcon?` +* [Android] Added the `ByteArrayAndroidIcon` class that implements the `AndroidIcon` class. This allows using a byte array to use as the icon for a person in a message style notification. A `ByteArrayAndroidIcon.fromBase64String()` named constructor is also available that will enable this using a base-64 encoded string. Thanks to the PR from [Alexander Petermann](https://github.com/lexxxel) +* [Android] Android 12 support +* Restored Linux support +* Fixed grammatical errors in readme. Thanks to PR from [Aneesh Rao](https://github.com/sidrao2006) +* Plugin now uses the [`clock`](https://pub.dev/packages/clock) package for internal logic that relies on geting the current time, such as validating that the date for a scheduled notification is set in the future + +# [8.2.0] + +* Added `dayOfMonthAndTime` and `dateAndTime` values to the `DateTimeComponents` enum. These allow for creating monthly and yearly notifications respectively. Thanks to the PR from [Denis Shakinov](https://github.com/DenisShakinov) + +# [8.1.1+2] + +* Bumped `timezone` dependency. Thanks to the PR from [Xavier H.](https://github.com/xvrh) + +# [8.1.1+1] + +* Updated API docs for the `initialize()` to mention that the `getNotificationAppLaunchDetails()` method should be used to handle when a notification launches an application + +# [8.1.1] + +* [Android] fixed issue [1263](https://github.com/MaikuB/flutter_local_notifications/issues/1263) around an unchecked/unsafe operation warning +* [Android] fixed issue [1246](https://github.com/MaikuB/flutter_local_notifications/issues/1246) where calling `createNotificationChannel()` wasn't update a notification channel's name/description + +# [8.1.0] + +* [Android] added the `startForegroundService()` and `stopForegroundService()` methods to the `AndroidFlutterLocalNotificationsPlugin` class. This can be used to start and stop a foreground service that shows a foreground service respectively. Refer to the API docs for more details on how to use this. The example app has been updated to demonstrate their usage. Thanks to the PR from [EPNW](https://github.com/EPNW) + +# [8.0.0] + +* **Breaking change** the `AndroidBitmap` class is now a generic class i.e. `AndroidBitmap`. This has resulted in the following changes + * the type of the `largeIcon` property that belongs to the `AndroidNotificationDetails` class has changed from `AndroidBitmap` to `AndroidBitmap` + * the type of the `largeIcon` and `bigPicture` properties that belongs to the `BigPictureStyleInformation` class has changed from `AndroidBitmap` to `AndroidBitmap` +* [Android] Added the `ByteArrayAndroidBitmap` class that implements the `AndroidBitmap` class. This allows using a byte array to use as the large icon for a notification or as big picture if the big picture style has been applied. A `ByteArrayAndroidBitmap.fromBase64String()` named constructor is also available that will enable this using a base-64 encoded string. Thanks to the PR from [Alexander Petermann](https://github.com/lexxxel) + +# [7.0.0] + +* **Breaking change** Removed support for Linux. This is because adding Linux made use of a plugin that was causing apps targeting the web to fail to build +* **Note**: as this is more an urgent release to resolve the aforementioned issue that results in a breaking change, please note this release does not include the Android 12 support changes that were in the prereleases. This is to err on the side of caution as Android 12 hasn't reached platform stability yet + +# [6.1.1] + +* **Breaking change** Removed support for Linux. This is because adding Linux made use of a plugin that was causing apps targeting the web to fail to build. This is identical to the 7.0.0 release but also done as a minor increment as 6.1.1 to fix build issues as developers would most likely use caret versioning + +# [6.1.0] **Bad build** + +* Added initial support to Linux. Thanks to the PR from [Yaroslav Pronin](https://github.com/proninyaroslav) +* Prevent crashing on the web by adding guard clauses within the plugin to check if the plugin is being used within a web app due to an [issue](https://github.com/google/platform.dart/issues/32) with getting the operating system via the [platform](https://pub.dev/packages/platform) package, which in turn relies on `dart:io`'s `Platform` APIs + +# [6.0.0] + +* Updated Flutter SDK constraint. To err on the safe side, this is why there's a major version bump for this release as the minimum version supported is 2.2 +* Updated Dart SDK constraint +* Bumped mockito dependency +* Addressed deprecation warnings that were appearing for Android builds +* Updated API docs around the `tag` property associated with the `AndroidNotificationDetails` class + +# [5.0.0+4] + +* Fixed example app to re-add attributes to the Android app's `AndroidManifest.xml` to allow full-screen intent notifications to work + +# [5.0.0+3] + +* Updated readme on how to get the local timezone +* Added link to location of example app to the readme + +# [5.0.0+2] + +* Updated example app to use the [flutter_native_timezone](https://pub.dev/packages/flutter_native_timezone) plugin to get the timezone +* Updated readme to mention effect of using same notification id +* Fixed wording and typo in full-screen intent notifications section of the readme. Thanks to PR from [Siddhartha Joshi](https://github.com/cimplesid) + +# [5.0.0+1] + +* Add link to explanation of the `onDidReceiveLocalNotification` callback to the initialisation section of the readme +* Updated testing section to clarify behaviour on platforms that aren't supported +* Updated `timezone` dependency + +# [5.0.0] + +* **Breaking change** migrated to null safety. Some arguments that were formerly null (e.g. some boolean values) are now non-nullable with a default value that should retain the old behaviour + +# [4.0.1+2] + +* [iOS/macOS] fixed issue where not requesting any permissions (i.e. all the boolean flags were set to false) would still cause a permissions prompt to appear. Thanks to the PR from [Andrey Parvatkin](https://github.com/badver) + +# [4.0.1+1] + +* Fixed typo in readme around the note relating to version 4.0 of the plugin where `onSelectNotification` will not be triggered when an app is launched by tapping on a notification. + +# [4.0.1] + +* [Android] added the `getNotificationChannels` method to the `AndroidFlutterLocalNotificationsPlugin` class. This can be used to a get list of all the notification channels on devices with Android 8.0 or newer. Thanks to the PR from [Shapovalova Vera](https://github.com/VAShapovalova) + +# [4.0.0] + +* **Breaking change** calling `initialize` will no longer trigger the `onSelectNotification` if a notification was tapped on prior to calling `initialize`. This was done as the `getNotificationAppLaunchDetails` method already provided a way to handle when an application was launched by a notification. Furthermore, calling `initialize` multiple times (e.g. on different pages) would have previously caused the `onSelectNotification` callback multiples times as well. This potentially results in the same notification being processed again +* **Breaking change** the `matchDateComponents` parameter has been renamed to `matchDateTimeComponents` +* Dates in the past can now be used with `zonedSchedule` when a value for the `matchDateTimeComponents` parameter has been specified to create recurring notifications. Thanks to the PR from [Erlend](https://github.com/erf) for implementing this and the previous change +* [Android] notification data is now saved to shared preferences in a background thread to minimise jank. Thanks to the PR from [davidlepilote](https://github.com/davidlepilote) +* [Android] the `tag` property has been added to the `AndroidNotificationDetails` class. This allows notifications on Android to be uniquely identifier through the use of the value of the `tag` and the `id` passed to the method for show/schedule the notification +* [Android] the optional `tag` argument has been added to the `cancel` method for the `FlutterLocalNotificationsPlugin` and `AndroidFlutterLocalNotificationsPlugin` classes. This can be used to cancel notifications where the `tag` has been specified +* [iOS][macOS] the `threadIdentifier` property has been added to the `IOSNotificationDetails` and `MacOSNotificationDetails` classes. This can be used to group notifications on iOS 10.0 or newer, and macOS 10.14 or newer. Thanks to the PR from [Marcin Chudy](https://github.com/mchudy) for adding this and the `tag` property for Android notifications +* The Android and iOS example applications have been recreated in Kotlin and Swift respectively +* Updated example application's dev dependency on the deprecated `e2e` for integration tests to use `integration_test` instead +* Bumped Flutter dependencies +* Example app cleanup including updating Proguard rules as specifying the rules for Flutter were no longer needed + +# [3.0.3] + +* [Android] added support for showing subtext in the notification. Thanks to the PR from [sidlatau](https://github.com/sidlatau) + # [3.0.2] * [Android] added support for showing the notification timestamp as a stopwatch instead via the `usesChronometer` argument added to the constructor of the `AndroidNotificationDetails` class. Thanks to the PR from [andymstone](https://github.com/andymstone) diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index 93446b29d..ced9bea06 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -3,7 +3,7 @@ [![pub package](https://img.shields.io/pub/v/flutter_local_notifications.svg)](https://pub.dartlang.org/packages/flutter_local_notifications) [![Build Status](https://api.cirrus-ci.com/github/MaikuB/flutter_local_notifications.svg)](https://cirrus-ci.com/github/MaikuB/flutter_local_notifications/master) -A cross platform plugin for displaying local notifications. +A cross platform plugin for displaying local notifications. ## Table of contents - **[πŸ“± Supported platforms](#-supported-platforms)** @@ -11,12 +11,12 @@ A cross platform plugin for displaying local notifications. - **[⚠ 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) - [Updating application badge](#updating-application-badge) - [Custom notification sounds](#custom-notification-sounds) - [macOS differences](#macos-differences) + - [Linux limitations](#linux-limitations) - **[πŸ“· Screenshots](#-screenshots)** - **[πŸ‘ Acknowledgements](#-acknowledgements)** - **[βš™οΈ Android Setup](#️-android-setup)** @@ -36,9 +36,9 @@ A cross platform plugin for displaying local notifications. - [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) + - [Retrieving pending notification requests](#retrieving-pending-notification-requests) - [[Android only] Retrieving active notifications](#android-only-retrieving-active-notifications) - - [[Android only] Grouping notifications](#android-only-grouping-notifications) + - [Grouping notifications](#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) @@ -49,6 +49,7 @@ A cross platform plugin for displaying local notifications. * **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. +* **Linux**. Uses the [Desktop Notifications Specification](https://developer.gnome.org/notification-spec/). ## ✨ Features @@ -61,7 +62,7 @@ A cross platform plugin for displaying local notifications. * Retrieve a list of pending notification requests that have been scheduled to be shown in the future * Cancelling/removing notification by id or all of them * Specify a custom notification sound -* Ability to handle when a user has tapped on a notification, when the app is the foreground, background or terminated +* Ability to handle when a user has tapped on a notification, when the app is in the foreground, background or is terminated * Determine if an app was launched due to tapping on a notification * [Android] Configuring the importance level * [Android] Configuring the priority @@ -69,7 +70,7 @@ A cross platform plugin for displaying local notifications. * [Android] Configure the default icon for all notifications * [Android] Configure the icon for each notification (overrides the default when specified) * [Android] Configure the large icon for each notification. The icon can be a drawable or a file on the device -* [Android] Formatting notification content via ([HTML markup](https://developer.android.com/guide/topics/resources/string-resource.html#StylingWithHTML)) +* [Android] Formatting notification content via [HTML markup](https://developer.android.com/guide/topics/resources/string-resource.html#StylingWithHTML) * [Android] Support for the following notification styles * Big picture * Big text @@ -83,8 +84,17 @@ A cross platform plugin for displaying local notifications. * [Android] Ability to create and delete notification channels * [Android] Retrieve the list of active notifications * [Android] Full-screen intent notifications +* [Android] Start a foreground service * [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 +* [Linux] Ability to to use themed/Flutter Assets icons and sound +* [Linux] Ability to to set the category +* [Linux] Configuring the urgency +* [Linux] Configuring the timeout (depends on system implementation) +* [Linux] Ability to set custom notification location (depends on system implementation) +* [Linux] Ability to set custom hints +* [Linux] Ability to suppress sound +* [Linux] Resident and transient notifications ## ⚠ 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. @@ -93,7 +103,7 @@ The cross-platform facing API exposed by the `FlutterLocalNotificationsPlugin` c Previously, there were issues that prevented this plugin working properly with the `firebase_messaging` plugin. This meant that callbacks from each plugin might not be invoked. This has been resolved since version 6.0.13 of the `firebase_messaging` plugin so please make sure you are using more recent versions of the `firebase_messaging` plugin and follow the steps covered in `firebase_messaging`'s readme file located [here](https://pub.dev/packages/firebase_messaging) ##### Scheduled Android notifications -Some Android OEMs have their own customised Android OS that can prevent applications from running in the background. Consequently, scheduled notifications may not work when the application is in the background on certain devices (e.g. by Xiaomi, Huawei). If you experience problems like this then this would be the reason why. As it's a restriction imposed by the OS, this is not something that can be resolved by the plugin. Some devices may have setting that lets users control which applications run in the background. The steps for these can be vary and but is still up to the users of your application to do given it's a setting on the phone itself. +Some Android OEMs have their own customised Android OS that can prevent applications from running in the background. Consequently, scheduled notifications may not work when the application is in the background on certain devices (e.g. by Xiaomi, Huawei). If you experience problems like this then this would be the reason why. As it's a restriction imposed by the OS, this is not something that can be resolved by the plugin. Some devices may have setting that lets users control which applications run in the background. The steps for these can vary but it is still up to the users of your application to do given it's a setting on the phone itself. It has been reported that Samsung's implementation of Android has imposed a maximum of 500 alarms that can be scheduled via the [Alarm Manager](https://developer.android.com/reference/android/app/AlarmManager) API and exceptions can occur when going over the limit. @@ -116,6 +126,14 @@ Due to limitations currently within the macOS Flutter engine, `getNotificationAp 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. +##### Linux limitations + +Capabilities depend on the system notification server implementation, therefore, not all features listed in `LinuxNotificationDetails` may be supported. One of the ways to check some capabilities is to call the `LinuxFlutterLocalNotificationsPlugin.getCapabilities()` method. + +Scheduled/pending notifications is currently not supported due to the lack of a scheduler API. + +To respond to notification after the application is terminated, your application should be registered as DBus activatable (see [DBusApplicationLaunching](https://wiki.gnome.org/HowDoI/DBusApplicationLaunching) for more information), and register action before activating the application. This is difficult to do in a plugin because plugins instantiate during application activation, so `getNotificationAppLaunchDetails` can't be implemented without changing the main user application. + ## πŸ“· Screenshots | Platform | Screenshot | @@ -123,6 +141,7 @@ The `schedule`, `showDailyAtTime` and `showWeeklyAtDayAndTime` methods that were | Android | | | iOS | | | macOS | | +| Linux | | ## πŸ‘ Acknowledgements @@ -153,7 +172,8 @@ When specifying the large icon bitmap or big picture bitmap (associated with the #### Full-screen intent notifications -If your application needs the ability to schedule full-screen intent notifications, add the following attributes to the activity you're opening. For a Flutter application that is typically only ony activity extends from `FlutterActivity`. These attributes ensure the screen turns on and shows when the device is locked. +If your application needs the ability to schedule full-screen intent notifications, add the following attributes to the activity you're opening. For a Flutter application, there is typically only one activity extends from `FlutterActivity`. These attributes ensure the screen turns on and shows when the device is locked. + ```xml _showNotificationWithActions() async { Each notification will have a internal ID & an public Action title. ### Example app -The `example` directory has a sample application that demonstrates the features of this plugin. + +The [`example`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications/example) directory has a sample application that demonstrates the features of this plugin. ### API reference -Checkout the lovely [API documentation](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/flutter_local_notifications-library.html) generated by pub. + +Checkout the lovely [API documentation](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/flutter_local_notifications-library.html) generated by pub. ## Initialisation @@ -407,10 +430,10 @@ await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification); ``` -Initialisation should only be done **once**, and this can be done is in the `main` function of your application. Alternatively, this can be done within the first page shown in your app. Developers can refer to the example app that has code for the initialising within the `main` function. The code above has been simplified for explaining the concepts. Here we have specified the default icon to use for notifications on Android (refer to the *Android setup* section) and designated the function (`selectNotification`) that should fire when a notification has been tapped on via the `onSelectNotification` callback. Specifying this callback is entirely optional but here it will trigger navigation to another page and display the payload associated with the notification. +Initialisation can be done in the `main` function of your application or can be done within the first page shown in your app. Developers can refer to the example app that has code for the initialising within the `main` function. The code above has been simplified for explaining the concepts. Here we have specified the default icon to use for notifications on Android (refer to the *Android setup* section) and designated the function (`selectNotification`) that should fire when a notification has been tapped on via the `onSelectNotification` callback. Specifying this callback is entirely optional but here it will trigger navigation to another page and display the payload associated with the notification. ```dart -Future selectNotification(String payload) async { +void selectNotification(String payload) async { if (payload != null) { debugPrint('notification payload: $payload'); } @@ -427,9 +450,10 @@ The `IOSInitializationSettings` and `MacOSInitializationSettings` provides defau 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. +For an explanation of the `onDidReceiveLocalNotification` callback associated with the `IOSInitializationSettings` class, please read [this](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications#handling-notifications-whilst-the-app-is-in-the-foreground). -⚠ 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). +*Note*: from version 4.0 of the plugin, calling `initialize` will not trigger the `onSelectNotification` callback when the application was started by tapping on a notification to trigger. Use the `getNotificationAppLaunchDetails` method that is available in the plugin if you need to handle a notification triggering the launch for an app e.g. change the home route of the app for deep-linking. ### [iOS (all supported versions) and macOS 10.14+] Requesting notification permissions @@ -495,11 +519,11 @@ Here the call to `flutterLocalNotificationsPlugin.resolvePlatformSpecificImpleme ```dart const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, - showWhen: false); + ticker: 'ticker'); const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.show( @@ -507,7 +531,9 @@ await flutterLocalNotificationsPlugin.show( payload: 'item x'); ``` -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. +Here, the first argument is the id of notification and is common to all methods that would result in a notification being shown. This is typically set a unique value per notification as using the same id multiple times would result in a notification being updated/overwritten. + +The details specific to the Android platform are also 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. The "ticker" text is passed here 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. ### Scheduling a notification @@ -518,7 +544,7 @@ Usage of the `timezone` package requires initialisation that is covered in the p Import the `timezone` package ```dart -import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; ``` @@ -531,10 +557,10 @@ tz.initializeTimeZones(); Once the time zone database has been initialised, developers may optionally want to set a default local location/time zone ```dart -tz.setLocalLocation(tz.getLocation(timeZoneName)); +tz.setLocalLocation(tz.getLocation(timeZoneName)); ``` -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)). +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](https://flutter.dev/docs/development/platform-integration/platform-channels) or use other packages that may be able to provide the information. The example app uses the [`flutter_native_timezone`](https://pub.dev/packages/flutter_native_timezone) plugin. Assuming the local location has been set, the `zonedScheduled` method can then be called in a manner similar to the following code @@ -545,8 +571,9 @@ await flutterLocalNotificationsPlugin.zonedSchedule( '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')), + android: AndroidNotificationDetails( + 'your channel id', 'your channel name', + channelDescription: 'your channel description')), androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime); @@ -564,8 +591,9 @@ If you are trying to update your code so it doesn't use the deprecated methods f ```dart const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('repeating channel id', - 'repeating channel name', 'repeating description'); + AndroidNotificationDetails( + 'repeating channel id', 'repeating channel name', + channelDescription: 'repeating description'); const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.periodicallyShow(0, 'repeating title', @@ -573,7 +601,7 @@ await flutterLocalNotificationsPlugin.periodicallyShow(0, 'repeating title', androidAllowWhileIdle: true); ``` -### Retrieveing pending notification requests +### Retrieving pending notification requests ```dart final List pendingNotificationRequests = @@ -590,10 +618,20 @@ final List activeNotifications = ?.getActiveNotifications(); ``` -### [Android only] Grouping notifications +### Grouping notifications + +#### iOS + +For iOS, you can specify `threadIdentifier` in `IOSNotificationDetails`. Notifications with the same `threadIdentifier` will get grouped together automatically. + +```dart +const IOSNotificationDetails iOSPlatformChannelSpecifics = + IOSNotificationDetails(threadIdentifier: 'thread_id'); +``` + +#### Android This is a "translation" of the sample available at https://developer.android.com/training/notify-user/group.html -For iOS, you could just display the summary notification (not shown in the example) as otherwise the following code would show three notifications ```dart const String groupKey = 'com.android.example.WORK_EMAIL'; @@ -602,8 +640,8 @@ const String groupChannelName = 'grouped channel name'; const String groupChannelDescription = 'grouped channel description'; // example based on https://developer.android.com/training/notify-user/group.html const AndroidNotificationDetails firstNotificationAndroidSpecifics = - AndroidNotificationDetails( - groupChannelId, groupChannelName, groupChannelDescription, + AndroidNotificationDetails(groupChannelId, groupChannelName, + channelDescription: groupChannelDescription, importance: Importance.max, priority: Priority.high, groupKey: groupKey); @@ -612,8 +650,8 @@ const NotificationDetails firstNotificationPlatformSpecifics = await flutterLocalNotificationsPlugin.show(1, 'Alex Faarborg', 'You will not believe...', firstNotificationPlatformSpecifics); const AndroidNotificationDetails secondNotificationAndroidSpecifics = - AndroidNotificationDetails( - groupChannelId, groupChannelName, groupChannelDescription, + AndroidNotificationDetails(groupChannelId, groupChannelName, + channelDescription: groupChannelDescription, importance: Importance.max, priority: Priority.high, groupKey: groupKey); @@ -639,8 +677,8 @@ const InboxStyleInformation inboxStyleInformation = InboxStyleInformation( contentTitle: '2 messages', summaryText: 'janedoe@example.com'); const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - groupChannelId, groupChannelName, groupChannelDescription, + AndroidNotificationDetails(groupChannelId, groupChannelName, + channelDescription: groupChannelDescription, styleInformation: inboxStyleInformation, groupKey: groupKey, setAsGroupSummary: true); @@ -696,11 +734,11 @@ if(!UserDefaults.standard.bool(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. +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. +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. +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 the platform isn't supported, it will default 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). +If a platform-specific implementation of the plugin is required for your tests, use the [debugDefaultTargetPlatformOverride](https://api.flutter.dev/flutter/foundation/debugDefaultTargetPlatformOverride.html) property provided by the Flutter framework. diff --git a/flutter_local_notifications/android/build.gradle b/flutter_local_notifications/android/build.gradle index 5208dad42..93ce4b8b5 100644 --- a/flutter_local_notifications/android/build.gradle +++ b/flutter_local_notifications/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { minSdkVersion 16 diff --git a/flutter_local_notifications/android/src/main/AndroidManifest.xml b/flutter_local_notifications/android/src/main/AndroidManifest.xml index 6d69bdf65..fbcf3e3f3 100644 --- a/flutter_local_notifications/android/src/main/AndroidManifest.xml +++ b/flutter_local_notifications/android/src/main/AndroidManifest.xml @@ -3,10 +3,11 @@ + - - + + diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ActionBroadcastReceiver.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ActionBroadcastReceiver.java index 8c2fcce0c..6ffa4a161 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ActionBroadcastReceiver.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ActionBroadcastReceiver.java @@ -4,10 +4,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationCompat.Action; import androidx.core.app.RemoteInput; import com.dexterous.flutterlocalnotifications.isolate.IsolatePreferences; import io.flutter.embedding.engine.FlutterEngine; @@ -23,91 +20,89 @@ import java.util.Map; public class ActionBroadcastReceiver extends BroadcastReceiver { - public static final String ACTION_TAPPED = "com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver.ACTION_TAPPED"; + public static final String ACTION_TAPPED = + "com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver.ACTION_TAPPED"; - @Nullable - private static ActionEventSink actionEventSink; + @Nullable private static ActionEventSink actionEventSink; - @Nullable - private static FlutterEngine engine; + @Nullable private static FlutterEngine engine; - @Override - public void onReceive(Context context, Intent intent) { - final String id = intent.getStringExtra("id"); + @Override + public void onReceive(Context context, Intent intent) { + final String id = intent.getStringExtra("id"); - final Map action = new HashMap<>(); - action.put("id", id); + final Map action = new HashMap<>(); + action.put("id", id); - action.put("payload", intent.hasExtra("payload") ? intent.getStringExtra("payload") : ""); + action.put("payload", intent.hasExtra("payload") ? intent.getStringExtra("payload") : ""); - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - if (remoteInput != null) { - action.put("input", remoteInput.getString("FlutterLocalNotificationsPluginInputResult")); - } else { - action.put("input", ""); - } + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + action.put("input", remoteInput.getString("FlutterLocalNotificationsPluginInputResult")); + } else { + action.put("input", ""); + } - if (actionEventSink == null) { - actionEventSink = new ActionEventSink(); - } - actionEventSink.addItem(action); + if (actionEventSink == null) { + actionEventSink = new ActionEventSink(); + } + actionEventSink.addItem(action); - startEngine(context); - } + startEngine(context); + } - private static class ActionEventSink implements StreamHandler { + private static class ActionEventSink implements StreamHandler { - final List> cache = new ArrayList<>(); + final List> cache = new ArrayList<>(); - @Nullable - private EventSink eventSink; + @Nullable private EventSink eventSink; - public void addItem(Map item) { - if (eventSink != null) { - eventSink.success(item); - } else { - cache.add(item); - } - } + public void addItem(Map item) { + if (eventSink != null) { + eventSink.success(item); + } else { + cache.add(item); + } + } - @Override - public void onListen(Object arguments, EventSink events) { - for (Map item : cache) { - events.success(item); - } + @Override + public void onListen(Object arguments, EventSink events) { + for (Map item : cache) { + events.success(item); + } - cache.clear(); - eventSink = events; - } + cache.clear(); + eventSink = events; + } - @Override - public void onCancel(Object arguments) { - eventSink = null; - } - } + @Override + public void onCancel(Object arguments) { + eventSink = null; + } + } - private void startEngine(Context context) { - long dispatcherHandle = IsolatePreferences.getCallbackDispatcherHandle(context); + private void startEngine(Context context) { + long dispatcherHandle = IsolatePreferences.getCallbackDispatcherHandle(context); - if (dispatcherHandle != -1L && engine == null) { - engine = new FlutterEngine(context); - FlutterMain.ensureInitializationComplete(context, null); + if (dispatcherHandle != -1L && engine == null) { + engine = new FlutterEngine(context); + FlutterMain.ensureInitializationComplete(context, null); - FlutterCallbackInformation callbackInfo = - FlutterCallbackInformation.lookupCallbackInformation(dispatcherHandle); - String dartBundlePath = FlutterMain.findAppBundlePath(); + FlutterCallbackInformation callbackInfo = + FlutterCallbackInformation.lookupCallbackInformation(dispatcherHandle); + String dartBundlePath = FlutterMain.findAppBundlePath(); - EventChannel channel = new EventChannel( - engine.getDartExecutor().getBinaryMessenger(), - "dexterous.com/flutter/local_notifications/actions"); + EventChannel channel = + new EventChannel( + engine.getDartExecutor().getBinaryMessenger(), + "dexterous.com/flutter/local_notifications/actions"); - channel.setStreamHandler(actionEventSink); - - engine - .getDartExecutor() - .executeDartCallback( - new DartExecutor.DartCallback(context.getAssets(), dartBundlePath, callbackInfo)); - } - } + channel.setStreamHandler(actionEventSink); + engine + .getDartExecutor() + .executeDartCallback( + new DartExecutor.DartCallback(context.getAssets(), dartBundlePath, callbackInfo)); + } + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/BitmapSource.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/BitmapSource.java deleted file mode 100644 index d38cbde24..000000000 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/BitmapSource.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.dexterous.flutterlocalnotifications; - -import androidx.annotation.Keep; - -@Keep -public enum BitmapSource { - DrawableResource, - FilePath -} 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 4202ce5ed..cc1cda4a9 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 @@ -19,7 +19,6 @@ import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; -import android.os.Bundle; import android.service.notification.StatusBarNotification; import android.text.Html; import android.text.Spanned; @@ -28,14 +27,15 @@ import androidx.annotation.NonNull; import androidx.core.app.AlarmManagerCompat; import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationCompat.Action; import androidx.core.app.NotificationCompat.Action.Builder; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; +import androidx.core.content.ContextCompat; import androidx.core.app.RemoteInput; import androidx.core.graphics.drawable.IconCompat; import com.dexterous.flutterlocalnotifications.isolate.IsolatePreferences; +import com.dexterous.flutterlocalnotifications.models.BitmapSource; import com.dexterous.flutterlocalnotifications.models.DateTimeComponents; import com.dexterous.flutterlocalnotifications.models.IconSource; import com.dexterous.flutterlocalnotifications.models.MessageDetails; @@ -47,6 +47,7 @@ import com.dexterous.flutterlocalnotifications.models.NotificationDetails; import com.dexterous.flutterlocalnotifications.models.PersonDetails; import com.dexterous.flutterlocalnotifications.models.ScheduledNotificationRepeatFrequency; +import com.dexterous.flutterlocalnotifications.models.SoundSource; import com.dexterous.flutterlocalnotifications.models.styles.BigPictureStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.BigTextStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.DefaultStyleInformation; @@ -69,12 +70,15 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -84,1212 +88,1715 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.FlutterMain; -/** - * FlutterLocalNotificationsPlugin - */ +/** FlutterLocalNotificationsPlugin */ @Keep -public class FlutterLocalNotificationsPlugin implements MethodCallHandler, PluginRegistry.NewIntentListener, FlutterPlugin, ActivityAware { - private static final String SHARED_PREFERENCES_KEY = "notification_plugin_cache"; - private static final String DISPATCHER_HANDLE = "dispatcher_handle"; - private static final String CALLBACK_HANDLE = "callback_handle"; - private static final String DRAWABLE = "drawable"; - private static final String DEFAULT_ICON = "defaultIcon"; - private static final String SELECT_NOTIFICATION = "SELECT_NOTIFICATION"; - private static final String SCHEDULED_NOTIFICATIONS = "scheduled_notifications"; - private static final String INITIALIZE_METHOD = "initialize"; - private static final String GET_CALLBACK_HANDLE_METHOD = "getCallbackHandle"; - private static final String CREATE_NOTIFICATION_CHANNEL_GROUP_METHOD = "createNotificationChannelGroup"; - private static final String DELETE_NOTIFICATION_CHANNEL_GROUP_METHOD = "deleteNotificationChannelGroup"; - 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"; - private static final String GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD = "getNotificationAppLaunchDetails"; - private static final String METHOD_CHANNEL = "dexterous.com/flutter/local_notifications"; - private static final String PAYLOAD = "payload"; - private static final String INVALID_ICON_ERROR_CODE = "INVALID_ICON"; - private static final String INVALID_LARGE_ICON_ERROR_CODE = "INVALID_LARGE_ICON"; - 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_DETAILS = "notificationDetails"; - static Gson gson; - private MethodChannel channel; - private Context applicationContext; - private Activity mainActivity; - private Intent launchIntent; - - public static void registerWith(Registrar registrar) { - FlutterLocalNotificationsPlugin plugin = new FlutterLocalNotificationsPlugin(); - plugin.setActivity(registrar.activity()); - registrar.addNewIntentListener(plugin); - plugin.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - static void rescheduleNotifications(Context context) { - initAndroidThreeTen(context); - ArrayList scheduledNotifications = loadScheduledNotifications(context); - for (NotificationDetails scheduledNotification : scheduledNotifications) { - if (scheduledNotification.repeatInterval == null) { - 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 = getLaunchIntent(context); - intent.setAction(SELECT_NOTIFICATION); - intent.putExtra(PAYLOAD, notificationDetails.payload); - PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationDetails.id, intent, PendingIntent.FLAG_UPDATE_CURRENT); - DefaultStyleInformation defaultStyleInformation = (DefaultStyleInformation) notificationDetails.styleInformation; - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationDetails.channelId) - .setContentTitle(defaultStyleInformation.htmlFormatTitle ? fromHtml(notificationDetails.title) : notificationDetails.title) - .setContentText(defaultStyleInformation.htmlFormatBody ? fromHtml(notificationDetails.body) : notificationDetails.body) - .setTicker(notificationDetails.ticker) - .setAutoCancel(BooleanUtils.getValue(notificationDetails.autoCancel)) - .setContentIntent(pendingIntent) - .setPriority(notificationDetails.priority) - .setOngoing(BooleanUtils.getValue(notificationDetails.ongoing)) - .setOnlyAlertOnce(BooleanUtils.getValue(notificationDetails.onlyAlertOnce)); - - if (notificationDetails.actions != null) { - int requestCode = 999; - for (NotificationAction action : notificationDetails.actions) { - IconCompat icon = null; - if (!TextUtils.isEmpty(action.icon)) { - icon = getIconFromSource(context, action.icon, action.iconSource); - } - - Intent actionIntent = new Intent(context, ActionBroadcastReceiver.class) - .setAction(ActionBroadcastReceiver.ACTION_TAPPED) - .putExtra("id", action.id) - .putExtra(PAYLOAD, notificationDetails.payload); - PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, requestCode++, actionIntent, 0); - Builder actionBuilder = new Builder(icon, action.title, actionPendingIntent); - - if (action.contextual != null) { - actionBuilder.setContextual(action.contextual); - } - if (action.showsUserInterface != null) { - actionBuilder.setShowsUserInterface(action.showsUserInterface); - } - if (action.allowGeneratedReplies != null) { - actionBuilder.setAllowGeneratedReplies(action.allowGeneratedReplies); - } - - for (NotificationActionInput input : action.inputs) { - RemoteInput.Builder remoteInput = new RemoteInput.Builder("FlutterLocalNotificationsPluginInputResult") - .setLabel(input.label); - if(input.allowFreeFormInput != null) { - remoteInput.setAllowFreeFormInput(input.allowFreeFormInput); - } - - if(input.allowedMimeTypes != null) { - for (String mimeType : input.allowedMimeTypes) { - remoteInput.setAllowDataType(mimeType, true); - } - } - if (input.choices != null) { - remoteInput.setChoices(input.choices.toArray(new CharSequence[]{})); - } - actionBuilder.addRemoteInput(remoteInput.build()); - } - builder.addAction(actionBuilder.build()); - } - } - - setSmallIcon(context, notificationDetails, builder); - if (!StringUtils.isNullOrEmpty(notificationDetails.largeIcon)) { - builder.setLargeIcon(getBitmapFromSource(context, notificationDetails.largeIcon, notificationDetails.largeIconBitmapSource)); - } - if (notificationDetails.color != null) { - builder.setColor(notificationDetails.color.intValue()); - } - - if (notificationDetails.showWhen != null) { - builder.setShowWhen(BooleanUtils.getValue(notificationDetails.showWhen)); - } - - if (notificationDetails.when != null) { - builder.setWhen(notificationDetails.when); - } - - if (notificationDetails.usesChronometer != null) { - builder.setUsesChronometer(notificationDetails.usesChronometer); - } - - 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); - setVibrationPattern(notificationDetails, builder); - setLights(notificationDetails, builder); - setStyle(context, notificationDetails, builder); - setProgress(notificationDetails, builder); - setCategory(notificationDetails, builder); - setTimeoutAfter(notificationDetails, builder); - Notification notification = builder.build(); - if (notificationDetails.additionalFlags != null && notificationDetails.additionalFlags.length > 0) { - for (int additionalFlag : notificationDetails.additionalFlags) { - notification.flags |= additionalFlag; - } - } - return notification; - } - - private static void setSmallIcon(Context context, NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (!StringUtils.isNullOrEmpty(notificationDetails.icon)) { - builder.setSmallIcon(getDrawableResourceId(context, notificationDetails.icon)); +public class FlutterLocalNotificationsPlugin + implements MethodCallHandler, PluginRegistry.NewIntentListener, FlutterPlugin, ActivityAware { + private static final String SHARED_PREFERENCES_KEY = "notification_plugin_cache"; + private static final String DISPATCHER_HANDLE = "dispatcher_handle"; + private static final String CALLBACK_HANDLE = "callback_handle"; + private static final String DRAWABLE = "drawable"; + private static final String DEFAULT_ICON = "defaultIcon"; + private static final String SELECT_NOTIFICATION = "SELECT_NOTIFICATION"; + private static final String SCHEDULED_NOTIFICATIONS = "scheduled_notifications"; + private static final String INITIALIZE_METHOD = "initialize"; + private static final String GET_CALLBACK_HANDLE_METHOD = "getCallbackHandle"; + private static final String CREATE_NOTIFICATION_CHANNEL_GROUP_METHOD = + "createNotificationChannelGroup"; + private static final String DELETE_NOTIFICATION_CHANNEL_GROUP_METHOD = + "deleteNotificationChannelGroup"; + 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 GET_NOTIFICATION_CHANNELS_METHOD = "getNotificationChannels"; + private static final String START_FOREGROUND_SERVICE = "startForegroundService"; + private static final String STOP_FOREGROUND_SERVICE = "stopForegroundService"; + 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"; + private static final String GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD = + "getNotificationAppLaunchDetails"; + private static final String METHOD_CHANNEL = "dexterous.com/flutter/local_notifications"; + private static final String PAYLOAD = "payload"; + private static final String INVALID_ICON_ERROR_CODE = "INVALID_ICON"; + private static final String INVALID_LARGE_ICON_ERROR_CODE = "INVALID_LARGE_ICON"; + 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 GET_NOTIFICATION_CHANNELS_ERROR_CODE = + "GET_NOTIFICATION_CHANNELS_ERROR_CODE"; + 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."; + private static final String CANCEL_ID = "id"; + private static final String CANCEL_TAG = "tag"; + static String NOTIFICATION_DETAILS = "notificationDetails"; + static Gson gson; + private MethodChannel channel; + private Context applicationContext; + private Activity mainActivity; + private Intent launchIntent; + + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + FlutterLocalNotificationsPlugin plugin = new FlutterLocalNotificationsPlugin(); + plugin.setActivity(registrar.activity()); + registrar.addNewIntentListener(plugin); + plugin.onAttachedToEngine(registrar.context(), registrar.messenger()); + } + + static void rescheduleNotifications(Context context) { + initAndroidThreeTen(context); + ArrayList scheduledNotifications = loadScheduledNotifications(context); + for (NotificationDetails scheduledNotification : scheduledNotifications) { + if (scheduledNotification.repeatInterval == null) { + if (scheduledNotification.timeZoneName == null) { + scheduleNotification(context, scheduledNotification, false); } else { - SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); - String defaultIcon = sharedPreferences.getString(DEFAULT_ICON, null); - if (StringUtils.isNullOrEmpty(defaultIcon)) { - // for backwards compatibility: this is for handling the old way references to the icon used to be kept but should be removed in future - builder.setSmallIcon(notificationDetails.iconResourceId); - - } else { - builder.setSmallIcon(getDrawableResourceId(context, defaultIcon)); - } - } - } - - @NonNull - static Gson buildGson() { - if (gson == null) { - RuntimeTypeAdapterFactory styleInformationAdapter = - RuntimeTypeAdapterFactory - .of(StyleInformation.class) - .registerSubtype(DefaultStyleInformation.class) - .registerSubtype(BigTextStyleInformation.class) - .registerSubtype(BigPictureStyleInformation.class) - .registerSubtype(InboxStyleInformation.class) - .registerSubtype(MessagingStyleInformation.class); - GsonBuilder builder = new GsonBuilder().registerTypeAdapterFactory(styleInformationAdapter); - gson = builder.create(); - } - return gson; - } - - private static ArrayList loadScheduledNotifications(Context context) { - ArrayList scheduledNotifications = new ArrayList<>(); - SharedPreferences sharedPreferences = context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); - String json = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS, null); - if (json != null) { - Gson gson = buildGson(); - Type type = new TypeToken>() { - }.getType(); - scheduledNotifications = gson.fromJson(json, type); - } - return scheduledNotifications; - } - - private static void saveScheduledNotifications(Context context, ArrayList scheduledNotifications) { - Gson gson = buildGson(); - String json = gson.toJson(scheduledNotifications); - SharedPreferences sharedPreferences = context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(SCHEDULED_NOTIFICATIONS, json); - editor.commit(); - } - - static void removeNotificationFromCache(Context context, Integer notificationId) { - ArrayList scheduledNotifications = loadScheduledNotifications(context); - for (Iterator it = scheduledNotifications.iterator(); it.hasNext(); ) { - NotificationDetails notificationDetails = it.next(); - if (notificationDetails.id.equals(notificationId)) { - it.remove(); - break; - } - } - saveScheduledNotifications(context, scheduledNotifications); - } - - @SuppressWarnings("deprecation") - private static Spanned fromHtml(String html) { - if (html == null) { - return null; - } - if (VERSION.SDK_INT >= VERSION_CODES.N) { - return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); - } else { - return Html.fromHtml(html); - } - } - - private static void scheduleNotification(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); - 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, notificationDetails.millisecondsSinceEpoch, pendingIntent); - } else { - AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, notificationDetails.millisecondsSinceEpoch, pendingIntent); - } - - if (updateScheduledNotificationsCache) { - saveScheduledNotification(context, notificationDetails); - } - } - - 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); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationDetails.id, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - AlarmManager alarmManager = getAlarmManager(context); - 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); - } - - 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()); - calendar.set(Calendar.HOUR_OF_DAY, notificationDetails.repeatTime.hour); - calendar.set(Calendar.MINUTE, notificationDetails.repeatTime.minute); - calendar.set(Calendar.SECOND, notificationDetails.repeatTime.second); - if (notificationDetails.day != null) { - calendar.set(Calendar.DAY_OF_WEEK, notificationDetails.day); - } - - notificationTriggerTime = calendar.getTimeInMillis(); - } - - notificationTriggerTime = calculateNextNotificationTrigger(notificationTriggerTime, 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); - - 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<>(); - for (NotificationDetails scheduledNotification : scheduledNotifications) { - if (scheduledNotification.id.equals(notificationDetails.id)) { - continue; - } - scheduledNotificationsToSave.add(scheduledNotification); - } - scheduledNotificationsToSave.add(notificationDetails); - saveScheduledNotifications(context, scheduledNotificationsToSave); - } - - private static int getDrawableResourceId(Context context, String name) { - return context.getResources().getIdentifier(name, DRAWABLE, context.getPackageName()); - } - - private static Bitmap getBitmapFromSource(Context context, String bitmapPath, BitmapSource bitmapSource) { - Bitmap bitmap = null; - if (bitmapSource == BitmapSource.DrawableResource) { - bitmap = BitmapFactory.decodeResource(context.getResources(), getDrawableResourceId(context, bitmapPath)); - } else if (bitmapSource == BitmapSource.FilePath) { - bitmap = BitmapFactory.decodeFile(bitmapPath); - } - - return bitmap; - } - - private static IconCompat getIconFromSource(Context context, String iconPath, IconSource iconSource) { + 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); + } + } + + protected static Notification createNotification( + Context context, NotificationDetails notificationDetails) { + NotificationChannelDetails notificationChannelDetails = + NotificationChannelDetails.fromNotificationDetails(notificationDetails); + if (canCreateNotificationChannel(context, notificationChannelDetails)) { + setupNotificationChannel(context, notificationChannelDetails); + } + Intent intent = getLaunchIntent(context); + intent.setAction(SELECT_NOTIFICATION); + intent.putExtra(PAYLOAD, notificationDetails.payload); + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (VERSION.SDK_INT >= VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pendingIntent = + PendingIntent.getActivity(context, notificationDetails.id, intent, flags); + DefaultStyleInformation defaultStyleInformation = + (DefaultStyleInformation) notificationDetails.styleInformation; + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, notificationDetails.channelId) + .setContentTitle( + defaultStyleInformation.htmlFormatTitle + ? fromHtml(notificationDetails.title) + : notificationDetails.title) + .setContentText( + defaultStyleInformation.htmlFormatBody + ? fromHtml(notificationDetails.body) + : notificationDetails.body) + .setTicker(notificationDetails.ticker) + .setAutoCancel(BooleanUtils.getValue(notificationDetails.autoCancel)) + .setContentIntent(pendingIntent) + .setPriority(notificationDetails.priority) + .setOngoing(BooleanUtils.getValue(notificationDetails.ongoing)) + .setOnlyAlertOnce(BooleanUtils.getValue(notificationDetails.onlyAlertOnce)); + + if (notificationDetails.actions != null) { + int requestCode = 999; + for (NotificationAction action : notificationDetails.actions) { IconCompat icon = null; - switch (iconSource) { - case DrawableResource: - icon = IconCompat.createWithResource(context, getDrawableResourceId(context, iconPath)); - break; - case BitmapFilePath: - icon = IconCompat.createWithBitmap(BitmapFactory.decodeFile(iconPath)); - break; - case ContentUri: - icon = IconCompat.createWithContentUri(iconPath); - break; - case FlutterBitmapAsset: - try { - AssetFileDescriptor assetFileDescriptor = context.getAssets().openFd(FlutterMain.getLookupKeyForAsset(iconPath)); - FileInputStream fileInputStream = assetFileDescriptor.createInputStream(); - icon = IconCompat.createWithBitmap(BitmapFactory.decodeStream(fileInputStream)); - fileInputStream.close(); - assetFileDescriptor.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - break; - default: - break; - } - return icon; - } - - /** - * Sets the visibility property to the input Notification Builder - * - * @throws IllegalArgumentException If `notificationDetails.visibility` is not null but also - * not matches any known index. - */ - private static void setVisibility(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (notificationDetails.visibility == null) { - return; - } - - int visibility; - switch (notificationDetails.visibility) { - case 0: // Private - visibility = NotificationCompat.VISIBILITY_PRIVATE; - break; - case 1: // Public - visibility = NotificationCompat.VISIBILITY_PUBLIC; - break; - case 2: // Secret - visibility = NotificationCompat.VISIBILITY_SECRET; - break; - - default: - throw new IllegalArgumentException("Unknown index: " + notificationDetails.visibility); - } - - builder.setVisibility(visibility); - } - - private static void applyGrouping(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - boolean isGrouped = false; - if (!StringUtils.isNullOrEmpty(notificationDetails.groupKey)) { - builder.setGroup(notificationDetails.groupKey); - isGrouped = true; - } - - if (isGrouped) { - if (BooleanUtils.getValue(notificationDetails.setAsGroupSummary)) { - builder.setGroupSummary(true); - } - - builder.setGroupAlertBehavior(notificationDetails.groupAlertBehavior); - } - } - - private static void setVibrationPattern(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (BooleanUtils.getValue(notificationDetails.enableVibration)) { - if (notificationDetails.vibrationPattern != null && notificationDetails.vibrationPattern.length > 0) { - builder.setVibrate(notificationDetails.vibrationPattern); - } - } else { - builder.setVibrate(new long[]{0}); - } - } - - private static void setLights(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (BooleanUtils.getValue(notificationDetails.enableLights) && notificationDetails.ledOnMs != null && notificationDetails.ledOffMs != null) { - builder.setLights(notificationDetails.ledColor, notificationDetails.ledOnMs, notificationDetails.ledOffMs); - } - } - - private static void setSound(Context context, NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (BooleanUtils.getValue(notificationDetails.playSound)) { - Uri uri = retrieveSoundResourceUri(context, notificationDetails.sound, notificationDetails.soundSource); - builder.setSound(uri); - } else { - builder.setSound(null); - } - } - - private static void setCategory(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (notificationDetails.category == null) { - return; - } - builder.setCategory(notificationDetails.category); - } - - private static void setTimeoutAfter(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (notificationDetails.timeoutAfter == null) { - return; - } - builder.setTimeoutAfter(notificationDetails.timeoutAfter); - } - - private static Intent getLaunchIntent(Context context) { - String packageName = context.getPackageName(); - PackageManager packageManager = context.getPackageManager(); - return packageManager.getLaunchIntentForPackage(packageName); - } - - private static void setStyle(Context context, NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - switch (notificationDetails.style) { - case BigPicture: - setBigPictureStyle(context, notificationDetails, builder); - break; - case BigText: - setBigTextStyle(notificationDetails, builder); - break; - case Inbox: - setInboxStyle(notificationDetails, builder); - break; - case Messaging: - setMessagingStyle(context, notificationDetails, builder); - break; - case Media: - setMediaStyle(builder); - break; - default: - break; - } - } - - private static void setProgress(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - if (BooleanUtils.getValue(notificationDetails.showProgress)) { - builder.setProgress(notificationDetails.maxProgress, notificationDetails.progress, notificationDetails.indeterminate); - } - } - - private static void setBigPictureStyle(Context context, NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - BigPictureStyleInformation bigPictureStyleInformation = (BigPictureStyleInformation) notificationDetails.styleInformation; - NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); - if (bigPictureStyleInformation.contentTitle != null) { - CharSequence contentTitle = bigPictureStyleInformation.htmlFormatContentTitle ? fromHtml(bigPictureStyleInformation.contentTitle) : bigPictureStyleInformation.contentTitle; - bigPictureStyle.setBigContentTitle(contentTitle); - } - if (bigPictureStyleInformation.summaryText != null) { - CharSequence summaryText = bigPictureStyleInformation.htmlFormatSummaryText ? fromHtml(bigPictureStyleInformation.summaryText) : bigPictureStyleInformation.summaryText; - bigPictureStyle.setSummaryText(summaryText); + if (!TextUtils.isEmpty(action.icon)) { + icon = getIconFromSource(context, action.icon, action.iconSource); } - if (bigPictureStyleInformation.hideExpandedLargeIcon) { - bigPictureStyle.bigLargeIcon(null); - } else { - if (bigPictureStyleInformation.largeIcon != null) { - bigPictureStyle.bigLargeIcon(getBitmapFromSource(context, bigPictureStyleInformation.largeIcon, bigPictureStyleInformation.largeIconBitmapSource)); - } - } - bigPictureStyle.bigPicture(getBitmapFromSource(context, bigPictureStyleInformation.bigPicture, bigPictureStyleInformation.bigPictureBitmapSource)); - builder.setStyle(bigPictureStyle); - } + Intent actionIntent = + new Intent(context, ActionBroadcastReceiver.class) + .setAction(ActionBroadcastReceiver.ACTION_TAPPED) + .putExtra("id", action.id) + .putExtra(PAYLOAD, notificationDetails.payload); + PendingIntent actionPendingIntent = + PendingIntent.getBroadcast(context, requestCode++, actionIntent, 0); + Builder actionBuilder = new Builder(icon, action.title, actionPendingIntent); - private static void setInboxStyle(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - InboxStyleInformation inboxStyleInformation = (InboxStyleInformation) notificationDetails.styleInformation; - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - if (inboxStyleInformation.contentTitle != null) { - CharSequence contentTitle = inboxStyleInformation.htmlFormatContentTitle ? fromHtml(inboxStyleInformation.contentTitle) : inboxStyleInformation.contentTitle; - inboxStyle.setBigContentTitle(contentTitle); + if (action.contextual != null) { + actionBuilder.setContextual(action.contextual); } - if (inboxStyleInformation.summaryText != null) { - CharSequence summaryText = inboxStyleInformation.htmlFormatSummaryText ? fromHtml(inboxStyleInformation.summaryText) : inboxStyleInformation.summaryText; - inboxStyle.setSummaryText(summaryText); + if (action.showsUserInterface != null) { + actionBuilder.setShowsUserInterface(action.showsUserInterface); } - if (inboxStyleInformation.lines != null) { - for (String line : inboxStyleInformation.lines) { - inboxStyle.addLine(inboxStyleInformation.htmlFormatLines ? fromHtml(line) : line); - } + if (action.allowGeneratedReplies != null) { + actionBuilder.setAllowGeneratedReplies(action.allowGeneratedReplies); } - builder.setStyle(inboxStyle); - } - private static void setMediaStyle(NotificationCompat.Builder builder) { - androidx.media.app.NotificationCompat.MediaStyle mediaStyle = new androidx.media.app.NotificationCompat.MediaStyle(); - builder.setStyle(mediaStyle); - } + for (NotificationActionInput input : action.inputs) { + RemoteInput.Builder remoteInput = + new RemoteInput.Builder("FlutterLocalNotificationsPluginInputResult") + .setLabel(input.label); + if (input.allowFreeFormInput != null) { + remoteInput.setAllowFreeFormInput(input.allowFreeFormInput); + } - private static void setMessagingStyle(Context context, NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - MessagingStyleInformation messagingStyleInformation = (MessagingStyleInformation) notificationDetails.styleInformation; - Person person = buildPerson(context, messagingStyleInformation.person); - NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person); - messagingStyle.setGroupConversation(BooleanUtils.getValue(messagingStyleInformation.groupConversation)); - if (messagingStyleInformation.conversationTitle != null) { - messagingStyle.setConversationTitle(messagingStyleInformation.conversationTitle); - } - if (messagingStyleInformation.messages != null && !messagingStyleInformation.messages.isEmpty()) { - for (MessageDetails messageDetails : messagingStyleInformation.messages) { - NotificationCompat.MessagingStyle.Message message = createMessage(context, messageDetails); - messagingStyle.addMessage(message); + if (input.allowedMimeTypes != null) { + for (String mimeType : input.allowedMimeTypes) { + remoteInput.setAllowDataType(mimeType, true); } - } - builder.setStyle(messagingStyle); - } - - private static NotificationCompat.MessagingStyle.Message createMessage(Context context, MessageDetails messageDetails) { - NotificationCompat.MessagingStyle.Message message = new NotificationCompat.MessagingStyle.Message(messageDetails.text, messageDetails.timestamp, buildPerson(context, messageDetails.person)); - if (messageDetails.dataUri != null && messageDetails.dataMimeType != null) { - message.setData(messageDetails.dataMimeType, Uri.parse(messageDetails.dataUri)); - } - return message; - } - - private static Person buildPerson(Context context, PersonDetails personDetails) { - if (personDetails == null) { - return null; - } - - Person.Builder personBuilder = new Person.Builder(); - personBuilder.setBot(BooleanUtils.getValue(personDetails.bot)); - if (personDetails.icon != null && personDetails.iconBitmapSource != null) { - personBuilder.setIcon(getIconFromSource(context, personDetails.icon, personDetails.iconBitmapSource)); - } - personBuilder.setImportant(BooleanUtils.getValue(personDetails.important)); - if (personDetails.key != null) { - personBuilder.setKey(personDetails.key); - } - if (personDetails.name != null) { - personBuilder.setName(personDetails.name); - } - if (personDetails.uri != null) { - personBuilder.setUri(personDetails.uri); - } - return personBuilder.build(); - } - - private static void setBigTextStyle(NotificationDetails notificationDetails, NotificationCompat.Builder builder) { - BigTextStyleInformation bigTextStyleInformation = (BigTextStyleInformation) notificationDetails.styleInformation; - NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); - if (bigTextStyleInformation.bigText != null) { - CharSequence bigText = bigTextStyleInformation.htmlFormatBigText ? fromHtml(bigTextStyleInformation.bigText) : bigTextStyleInformation.bigText; - bigTextStyle.bigText(bigText); - } - if (bigTextStyleInformation.contentTitle != null) { - CharSequence contentTitle = bigTextStyleInformation.htmlFormatContentTitle ? fromHtml(bigTextStyleInformation.contentTitle) : bigTextStyleInformation.contentTitle; - bigTextStyle.setBigContentTitle(contentTitle); - } - if (bigTextStyleInformation.summaryText != null) { - CharSequence summaryText = bigTextStyleInformation.htmlFormatSummaryText ? fromHtml(bigTextStyleInformation.summaryText) : bigTextStyleInformation.summaryText; - bigTextStyle.setSummaryText(summaryText); - } - builder.setStyle(bigTextStyle); - } - - private static void setupNotificationChannel(Context context, NotificationChannelDetails notificationChannelDetails) { - 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 - // created on older versions of the plugin where channel management options weren't available back then - if ((notificationChannel == null && (notificationChannelDetails.channelAction == null || notificationChannelDetails.channelAction == NotificationChannelAction.CreateIfNotExists)) || (notificationChannel != null && notificationChannelDetails.channelAction == NotificationChannelAction.Update)) { - notificationChannel = new NotificationChannel(notificationChannelDetails.id, notificationChannelDetails.name, notificationChannelDetails.importance); - notificationChannel.setDescription(notificationChannelDetails.description); - notificationChannel.setGroup(notificationChannelDetails.groupId); - if (notificationChannelDetails.playSound) { - AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build(); - Uri uri = retrieveSoundResourceUri(context, notificationChannelDetails.sound, notificationChannelDetails.soundSource); - notificationChannel.setSound(uri, audioAttributes); - } else { - notificationChannel.setSound(null, null); - } - notificationChannel.enableVibration(BooleanUtils.getValue(notificationChannelDetails.enableVibration)); - if (notificationChannelDetails.vibrationPattern != null && notificationChannelDetails.vibrationPattern.length > 0) { - notificationChannel.setVibrationPattern(notificationChannelDetails.vibrationPattern); - } - boolean enableLights = BooleanUtils.getValue(notificationChannelDetails.enableLights); - notificationChannel.enableLights(enableLights); - if (enableLights && notificationChannelDetails.ledColor != null) { - notificationChannel.setLightColor(notificationChannelDetails.ledColor); + } + if (input.choices != null) { + remoteInput.setChoices(input.choices.toArray(new CharSequence[] {})); + } + actionBuilder.addRemoteInput(remoteInput.build()); + } + builder.addAction(actionBuilder.build()); + } + } + + setSmallIcon(context, notificationDetails, builder); + builder.setLargeIcon( + getBitmapFromSource( + context, notificationDetails.largeIcon, notificationDetails.largeIconBitmapSource)); + if (notificationDetails.color != null) { + builder.setColor(notificationDetails.color.intValue()); + } + + if (notificationDetails.showWhen != null) { + builder.setShowWhen(BooleanUtils.getValue(notificationDetails.showWhen)); + } + + if (notificationDetails.when != null) { + builder.setWhen(notificationDetails.when); + } + + if (notificationDetails.usesChronometer != null) { + builder.setUsesChronometer(notificationDetails.usesChronometer); + } + + if (BooleanUtils.getValue(notificationDetails.fullScreenIntent)) { + builder.setFullScreenIntent(pendingIntent, true); + } + + if (!StringUtils.isNullOrEmpty(notificationDetails.shortcutId)) { + builder.setShortcutId(notificationDetails.shortcutId); + } + + if (!StringUtils.isNullOrEmpty(notificationDetails.subText)) { + builder.setSubText(notificationDetails.subText); + } + + setVisibility(notificationDetails, builder); + applyGrouping(notificationDetails, builder); + setSound(context, notificationDetails, builder); + setVibrationPattern(notificationDetails, builder); + setLights(notificationDetails, builder); + setStyle(context, notificationDetails, builder); + setProgress(notificationDetails, builder); + setCategory(notificationDetails, builder); + setTimeoutAfter(notificationDetails, builder); + Notification notification = builder.build(); + if (notificationDetails.additionalFlags != null + && notificationDetails.additionalFlags.length > 0) { + for (int additionalFlag : notificationDetails.additionalFlags) { + notification.flags |= additionalFlag; + } + } + return notification; + } + + private static Boolean canCreateNotificationChannel( + Context context, NotificationChannelDetails notificationChannelDetails) { + 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 + // created on older versions of the plugin where channel management options weren't available + // back then + return ((notificationChannel == null + && (notificationChannelDetails.channelAction == null + || notificationChannelDetails.channelAction + == NotificationChannelAction.CreateIfNotExists)) + || (notificationChannel != null + && notificationChannelDetails.channelAction == NotificationChannelAction.Update)); + } + return false; + } + + private static void setSmallIcon( + Context context, + NotificationDetails notificationDetails, + NotificationCompat.Builder builder) { + if (!StringUtils.isNullOrEmpty(notificationDetails.icon)) { + builder.setSmallIcon(getDrawableResourceId(context, notificationDetails.icon)); + } else { + SharedPreferences sharedPreferences = + context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); + String defaultIcon = sharedPreferences.getString(DEFAULT_ICON, null); + if (StringUtils.isNullOrEmpty(defaultIcon)) { + // for backwards compatibility: this is for handling the old way references to the icon used + // to be kept but should be removed in future + builder.setSmallIcon(notificationDetails.iconResourceId); + + } else { + builder.setSmallIcon(getDrawableResourceId(context, defaultIcon)); + } + } + } + + @NonNull + static Gson buildGson() { + if (gson == null) { + RuntimeTypeAdapterFactory styleInformationAdapter = + RuntimeTypeAdapterFactory.of(StyleInformation.class) + .registerSubtype(DefaultStyleInformation.class) + .registerSubtype(BigTextStyleInformation.class) + .registerSubtype(BigPictureStyleInformation.class) + .registerSubtype(InboxStyleInformation.class) + .registerSubtype(MessagingStyleInformation.class); + GsonBuilder builder = new GsonBuilder().registerTypeAdapterFactory(styleInformationAdapter); + gson = builder.create(); + } + return gson; + } + + private static ArrayList loadScheduledNotifications(Context context) { + ArrayList scheduledNotifications = new ArrayList<>(); + SharedPreferences sharedPreferences = + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); + String json = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS, null); + if (json != null) { + Gson gson = buildGson(); + Type type = new TypeToken>() {}.getType(); + scheduledNotifications = gson.fromJson(json, type); + } + return scheduledNotifications; + } + + private static void saveScheduledNotifications( + Context context, ArrayList scheduledNotifications) { + Gson gson = buildGson(); + String json = gson.toJson(scheduledNotifications); + SharedPreferences sharedPreferences = + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(SCHEDULED_NOTIFICATIONS, json); + tryCommittingInBackground(editor, 3); + } + + private static void tryCommittingInBackground( + final SharedPreferences.Editor editor, final int tries) { + if (tries == 0) { + return; + } + new Thread( + new Runnable() { + @Override + public void run() { + final boolean isCommitted = editor.commit(); + if (!isCommitted) { + tryCommittingInBackground(editor, tries - 1); } - notificationChannel.setShowBadge(BooleanUtils.getValue(notificationChannelDetails.showBadge)); - notificationManager.createNotificationChannel(notificationChannel); - } - } - } - - private static Uri retrieveSoundResourceUri(Context context, String sound, SoundSource soundSource) { - Uri uri = null; - if (StringUtils.isNullOrEmpty(sound)) { - uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - } else { - // allow null as soundSource was added later and prior to that, it was assumed to be a raw resource - if (soundSource == null || soundSource == SoundSource.RawResource) { - int soundResourceId = context.getResources().getIdentifier(sound, "raw", context.getPackageName()); - uri = Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId); - } else if (soundSource == SoundSource.Uri) { - uri = Uri.parse(sound); - } - } - return uri; - } - - private static AlarmManager getAlarmManager(Context context) { - return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - } - - private static boolean isValidDrawableResource(Context context, String name, Result result, String errorCode) { - int resourceId = context.getResources().getIdentifier(name, DRAWABLE, context.getPackageName()); - if (resourceId == 0) { - result.error(errorCode, String.format(INVALID_DRAWABLE_RESOURCE_ERROR_MESSAGE, name), null); - return false; - } + } + }) + .start(); + } + + static void removeNotificationFromCache(Context context, Integer notificationId) { + ArrayList scheduledNotifications = loadScheduledNotifications(context); + for (Iterator it = scheduledNotifications.iterator(); it.hasNext(); ) { + NotificationDetails notificationDetails = it.next(); + if (notificationDetails.id.equals(notificationId)) { + it.remove(); + break; + } + } + saveScheduledNotifications(context, scheduledNotifications); + } + + @SuppressWarnings("deprecation") + private static Spanned fromHtml(String html) { + if (html == null) { + return null; + } + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + } else { + return Html.fromHtml(html); + } + } + + private static void scheduleNotification( + 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); + PendingIntent pendingIntent = + getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); + + AlarmManager alarmManager = getAlarmManager(context); + if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) { + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, + AlarmManager.RTC_WAKEUP, + notificationDetails.millisecondsSinceEpoch, + pendingIntent); + } else { + AlarmManagerCompat.setExact( + alarmManager, + AlarmManager.RTC_WAKEUP, + notificationDetails.millisecondsSinceEpoch, + pendingIntent); + } + + if (updateScheduledNotificationsCache) { + saveScheduledNotification(context, notificationDetails); + } + } + + 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); + PendingIntent pendingIntent = + getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); + AlarmManager alarmManager = getAlarmManager(context); + 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 = + getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); + AlarmManager alarmManager = getAlarmManager(context); + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent); + saveScheduledNotification(context, notificationDetails); + } + + private static PendingIntent getActivityPendingIntent(Context context, int id, Intent intent) { + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (VERSION.SDK_INT >= VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + return PendingIntent.getActivity(context, id, intent, flags); + } + + private static PendingIntent getBroadcastPendingIntent(Context context, int id, Intent intent) { + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (VERSION.SDK_INT >= VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + return PendingIntent.getBroadcast(context, id, intent, flags); + } + + 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()); + calendar.set(Calendar.HOUR_OF_DAY, notificationDetails.repeatTime.hour); + calendar.set(Calendar.MINUTE, notificationDetails.repeatTime.minute); + calendar.set(Calendar.SECOND, notificationDetails.repeatTime.second); + if (notificationDetails.day != null) { + calendar.set(Calendar.DAY_OF_WEEK, notificationDetails.day); + } + + notificationTriggerTime = calendar.getTimeInMillis(); + } + + notificationTriggerTime = + calculateNextNotificationTrigger(notificationTriggerTime, repeatInterval); + + Gson gson = buildGson(); + String notificationDetailsJson = gson.toJson(notificationDetails); + Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); + notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); + PendingIntent pendingIntent = + getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); + 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<>(); + for (NotificationDetails scheduledNotification : scheduledNotifications) { + if (scheduledNotification.id.equals(notificationDetails.id)) { + continue; + } + scheduledNotificationsToSave.add(scheduledNotification); + } + scheduledNotificationsToSave.add(notificationDetails); + saveScheduledNotifications(context, scheduledNotificationsToSave); + } + + private static int getDrawableResourceId(Context context, String name) { + return context.getResources().getIdentifier(name, DRAWABLE, context.getPackageName()); + } + + @SuppressWarnings("unchecked") + private static byte[] castObjectToByteArray(Object data) { + byte[] byteArray; + // if data is deserialized by gson, it is of the wrong type and we have to convert it + if (data instanceof ArrayList) { + List l = (ArrayList) data; + byteArray = new byte[l.size()]; + for (int i = 0; i < l.size(); i++) { + byteArray[i] = (byte) l.get(i).intValue(); + } + } else { + byteArray = (byte[]) data; + } + return byteArray; + } + + private static Bitmap getBitmapFromSource( + Context context, Object data, BitmapSource bitmapSource) { + Bitmap bitmap = null; + if (bitmapSource == BitmapSource.DrawableResource) { + bitmap = + BitmapFactory.decodeResource( + context.getResources(), getDrawableResourceId(context, (String) data)); + } else if (bitmapSource == BitmapSource.FilePath) { + bitmap = BitmapFactory.decodeFile((String) data); + } else if (bitmapSource == BitmapSource.ByteArray) { + byte[] byteArray = castObjectToByteArray(data); + bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + } + + return bitmap; + } + + private static IconCompat getIconFromSource(Context context, Object data, IconSource iconSource) { + IconCompat icon = null; + switch (iconSource) { + case DrawableResource: + icon = + IconCompat.createWithResource(context, getDrawableResourceId(context, (String) data)); + break; + case BitmapFilePath: + icon = IconCompat.createWithBitmap(BitmapFactory.decodeFile((String) data)); + break; + case ContentUri: + icon = IconCompat.createWithContentUri((String) data); + break; + case FlutterBitmapAsset: + try { + FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader(); + AssetFileDescriptor assetFileDescriptor = + context.getAssets().openFd(flutterLoader.getLookupKeyForAsset((String) data)); + FileInputStream fileInputStream = assetFileDescriptor.createInputStream(); + icon = IconCompat.createWithBitmap(BitmapFactory.decodeStream(fileInputStream)); + fileInputStream.close(); + assetFileDescriptor.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + break; + case ByteArray: + byte[] byteArray = castObjectToByteArray(data); + icon = IconCompat.createWithData(byteArray, 0, byteArray.length); + default: + break; + } + return icon; + } + + /** + * Sets the visibility property to the input Notification Builder + * + * @throws IllegalArgumentException If `notificationDetails.visibility` is not null but also not + * matches any known index. + */ + private static void setVisibility( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + if (notificationDetails.visibility == null) { + return; + } + + int visibility; + switch (notificationDetails.visibility) { + case 0: // Private + visibility = NotificationCompat.VISIBILITY_PRIVATE; + break; + case 1: // Public + visibility = NotificationCompat.VISIBILITY_PUBLIC; + break; + case 2: // Secret + visibility = NotificationCompat.VISIBILITY_SECRET; + break; + + default: + throw new IllegalArgumentException("Unknown index: " + notificationDetails.visibility); + } + + builder.setVisibility(visibility); + } + + private static void applyGrouping( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + boolean isGrouped = false; + if (!StringUtils.isNullOrEmpty(notificationDetails.groupKey)) { + builder.setGroup(notificationDetails.groupKey); + isGrouped = true; + } + + if (isGrouped) { + if (BooleanUtils.getValue(notificationDetails.setAsGroupSummary)) { + builder.setGroupSummary(true); + } + + builder.setGroupAlertBehavior(notificationDetails.groupAlertBehavior); + } + } + + private static void setVibrationPattern( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + if (BooleanUtils.getValue(notificationDetails.enableVibration)) { + if (notificationDetails.vibrationPattern != null + && notificationDetails.vibrationPattern.length > 0) { + builder.setVibrate(notificationDetails.vibrationPattern); + } + } else { + builder.setVibrate(new long[] {0}); + } + } + + private static void setLights( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + if (BooleanUtils.getValue(notificationDetails.enableLights) + && notificationDetails.ledOnMs != null + && notificationDetails.ledOffMs != null) { + builder.setLights( + notificationDetails.ledColor, notificationDetails.ledOnMs, notificationDetails.ledOffMs); + } + } + + private static void setSound( + Context context, + NotificationDetails notificationDetails, + NotificationCompat.Builder builder) { + if (BooleanUtils.getValue(notificationDetails.playSound)) { + Uri uri = + retrieveSoundResourceUri( + context, notificationDetails.sound, notificationDetails.soundSource); + builder.setSound(uri); + } else { + builder.setSound(null); + } + } + + private static void setCategory( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + if (notificationDetails.category == null) { + return; + } + builder.setCategory(notificationDetails.category); + } + + private static void setTimeoutAfter( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + if (notificationDetails.timeoutAfter == null) { + return; + } + builder.setTimeoutAfter(notificationDetails.timeoutAfter); + } + + private static Intent getLaunchIntent(Context context) { + String packageName = context.getPackageName(); + PackageManager packageManager = context.getPackageManager(); + return packageManager.getLaunchIntentForPackage(packageName); + } + + private static void setStyle( + Context context, + NotificationDetails notificationDetails, + NotificationCompat.Builder builder) { + switch (notificationDetails.style) { + case BigPicture: + setBigPictureStyle(context, notificationDetails, builder); + break; + case BigText: + setBigTextStyle(notificationDetails, builder); + break; + case Inbox: + setInboxStyle(notificationDetails, builder); + break; + case Messaging: + setMessagingStyle(context, notificationDetails, builder); + break; + case Media: + setMediaStyle(builder); + break; + default: + break; + } + } + + private static void setProgress( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + if (BooleanUtils.getValue(notificationDetails.showProgress)) { + builder.setProgress( + notificationDetails.maxProgress, + notificationDetails.progress, + notificationDetails.indeterminate); + } + } + + private static void setBigPictureStyle( + Context context, + NotificationDetails notificationDetails, + NotificationCompat.Builder builder) { + BigPictureStyleInformation bigPictureStyleInformation = + (BigPictureStyleInformation) notificationDetails.styleInformation; + NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); + if (bigPictureStyleInformation.contentTitle != null) { + CharSequence contentTitle = + bigPictureStyleInformation.htmlFormatContentTitle + ? fromHtml(bigPictureStyleInformation.contentTitle) + : bigPictureStyleInformation.contentTitle; + bigPictureStyle.setBigContentTitle(contentTitle); + } + if (bigPictureStyleInformation.summaryText != null) { + CharSequence summaryText = + bigPictureStyleInformation.htmlFormatSummaryText + ? fromHtml(bigPictureStyleInformation.summaryText) + : bigPictureStyleInformation.summaryText; + bigPictureStyle.setSummaryText(summaryText); + } + + if (bigPictureStyleInformation.hideExpandedLargeIcon) { + bigPictureStyle.bigLargeIcon(null); + } else { + if (bigPictureStyleInformation.largeIcon != null) { + bigPictureStyle.bigLargeIcon( + getBitmapFromSource( + context, + bigPictureStyleInformation.largeIcon, + bigPictureStyleInformation.largeIconBitmapSource)); + } + } + bigPictureStyle.bigPicture( + getBitmapFromSource( + context, + bigPictureStyleInformation.bigPicture, + bigPictureStyleInformation.bigPictureBitmapSource)); + builder.setStyle(bigPictureStyle); + } + + private static void setInboxStyle( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + InboxStyleInformation inboxStyleInformation = + (InboxStyleInformation) notificationDetails.styleInformation; + NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + if (inboxStyleInformation.contentTitle != null) { + CharSequence contentTitle = + inboxStyleInformation.htmlFormatContentTitle + ? fromHtml(inboxStyleInformation.contentTitle) + : inboxStyleInformation.contentTitle; + inboxStyle.setBigContentTitle(contentTitle); + } + if (inboxStyleInformation.summaryText != null) { + CharSequence summaryText = + inboxStyleInformation.htmlFormatSummaryText + ? fromHtml(inboxStyleInformation.summaryText) + : inboxStyleInformation.summaryText; + inboxStyle.setSummaryText(summaryText); + } + if (inboxStyleInformation.lines != null) { + for (String line : inboxStyleInformation.lines) { + inboxStyle.addLine(inboxStyleInformation.htmlFormatLines ? fromHtml(line) : line); + } + } + builder.setStyle(inboxStyle); + } + + private static void setMediaStyle(NotificationCompat.Builder builder) { + androidx.media.app.NotificationCompat.MediaStyle mediaStyle = + new androidx.media.app.NotificationCompat.MediaStyle(); + builder.setStyle(mediaStyle); + } + + private static void setMessagingStyle( + Context context, + NotificationDetails notificationDetails, + NotificationCompat.Builder builder) { + MessagingStyleInformation messagingStyleInformation = + (MessagingStyleInformation) notificationDetails.styleInformation; + Person person = buildPerson(context, messagingStyleInformation.person); + NotificationCompat.MessagingStyle messagingStyle = + new NotificationCompat.MessagingStyle(person); + messagingStyle.setGroupConversation( + BooleanUtils.getValue(messagingStyleInformation.groupConversation)); + if (messagingStyleInformation.conversationTitle != null) { + messagingStyle.setConversationTitle(messagingStyleInformation.conversationTitle); + } + if (messagingStyleInformation.messages != null + && !messagingStyleInformation.messages.isEmpty()) { + for (MessageDetails messageDetails : messagingStyleInformation.messages) { + NotificationCompat.MessagingStyle.Message message = createMessage(context, messageDetails); + messagingStyle.addMessage(message); + } + } + builder.setStyle(messagingStyle); + } + + private static NotificationCompat.MessagingStyle.Message createMessage( + Context context, MessageDetails messageDetails) { + NotificationCompat.MessagingStyle.Message message = + new NotificationCompat.MessagingStyle.Message( + messageDetails.text, + messageDetails.timestamp, + buildPerson(context, messageDetails.person)); + if (messageDetails.dataUri != null && messageDetails.dataMimeType != null) { + message.setData(messageDetails.dataMimeType, Uri.parse(messageDetails.dataUri)); + } + return message; + } + + private static Person buildPerson(Context context, PersonDetails personDetails) { + if (personDetails == null) { + return null; + } + + Person.Builder personBuilder = new Person.Builder(); + personBuilder.setBot(BooleanUtils.getValue(personDetails.bot)); + if (personDetails.icon != null && personDetails.iconBitmapSource != null) { + personBuilder.setIcon( + getIconFromSource(context, personDetails.icon, personDetails.iconBitmapSource)); + } + personBuilder.setImportant(BooleanUtils.getValue(personDetails.important)); + if (personDetails.key != null) { + personBuilder.setKey(personDetails.key); + } + if (personDetails.name != null) { + personBuilder.setName(personDetails.name); + } + if (personDetails.uri != null) { + personBuilder.setUri(personDetails.uri); + } + return personBuilder.build(); + } + + private static void setBigTextStyle( + NotificationDetails notificationDetails, NotificationCompat.Builder builder) { + BigTextStyleInformation bigTextStyleInformation = + (BigTextStyleInformation) notificationDetails.styleInformation; + NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); + if (bigTextStyleInformation.bigText != null) { + CharSequence bigText = + bigTextStyleInformation.htmlFormatBigText + ? fromHtml(bigTextStyleInformation.bigText) + : bigTextStyleInformation.bigText; + bigTextStyle.bigText(bigText); + } + if (bigTextStyleInformation.contentTitle != null) { + CharSequence contentTitle = + bigTextStyleInformation.htmlFormatContentTitle + ? fromHtml(bigTextStyleInformation.contentTitle) + : bigTextStyleInformation.contentTitle; + bigTextStyle.setBigContentTitle(contentTitle); + } + if (bigTextStyleInformation.summaryText != null) { + CharSequence summaryText = + bigTextStyleInformation.htmlFormatSummaryText + ? fromHtml(bigTextStyleInformation.summaryText) + : bigTextStyleInformation.summaryText; + bigTextStyle.setSummaryText(summaryText); + } + builder.setStyle(bigTextStyle); + } + + private static void setupNotificationChannel( + Context context, NotificationChannelDetails notificationChannelDetails) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel notificationChannel = + new NotificationChannel( + notificationChannelDetails.id, + notificationChannelDetails.name, + notificationChannelDetails.importance); + notificationChannel.setDescription(notificationChannelDetails.description); + notificationChannel.setGroup(notificationChannelDetails.groupId); + if (notificationChannelDetails.playSound) { + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build(); + Uri uri = + retrieveSoundResourceUri( + context, notificationChannelDetails.sound, notificationChannelDetails.soundSource); + notificationChannel.setSound(uri, audioAttributes); + } else { + notificationChannel.setSound(null, null); + } + notificationChannel.enableVibration( + BooleanUtils.getValue(notificationChannelDetails.enableVibration)); + if (notificationChannelDetails.vibrationPattern != null + && notificationChannelDetails.vibrationPattern.length > 0) { + notificationChannel.setVibrationPattern(notificationChannelDetails.vibrationPattern); + } + boolean enableLights = BooleanUtils.getValue(notificationChannelDetails.enableLights); + notificationChannel.enableLights(enableLights); + if (enableLights && notificationChannelDetails.ledColor != null) { + notificationChannel.setLightColor(notificationChannelDetails.ledColor); + } + notificationChannel.setShowBadge(BooleanUtils.getValue(notificationChannelDetails.showBadge)); + notificationManager.createNotificationChannel(notificationChannel); + } + } + + private static Uri retrieveSoundResourceUri( + Context context, String sound, SoundSource soundSource) { + Uri uri = null; + if (StringUtils.isNullOrEmpty(sound)) { + uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + } else { + // allow null as soundSource was added later and prior to that, it was assumed to be a raw + // resource + if (soundSource == null || soundSource == SoundSource.RawResource) { + uri = Uri.parse("android.resource://" + context.getPackageName() + "/raw/" + sound); + } else if (soundSource == SoundSource.Uri) { + uri = Uri.parse(sound); + } + } + return uri; + } + + private static AlarmManager getAlarmManager(Context context) { + return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + private static boolean isValidDrawableResource( + Context context, String name, Result result, String errorCode) { + int resourceId = context.getResources().getIdentifier(name, DRAWABLE, context.getPackageName()); + if (resourceId == 0) { + result.error(errorCode, String.format(INVALID_DRAWABLE_RESOURCE_ERROR_MESSAGE, name), null); + return false; + } + return true; + } + + static void showNotification(Context context, NotificationDetails notificationDetails) { + Notification notification = createNotification(context, notificationDetails); + NotificationManagerCompat notificationManagerCompat = getNotificationManager(context); + + if (notificationDetails.tag != null) { + notificationManagerCompat.notify( + notificationDetails.tag, notificationDetails.id, notification); + } else { + notificationManagerCompat.notify(notificationDetails.id, notification); + } + } + + static void zonedScheduleNextNotification( + Context context, NotificationDetails notificationDetails) { + initAndroidThreeTen(context); + String nextFireDate = getNextFireDate(notificationDetails); + if (nextFireDate == null) { + return; + } + notificationDetails.scheduledDateTime = nextFireDate; + zonedScheduleNotification(context, notificationDetails, true); + } + + static void zonedScheduleNextNotificationMatchingDateComponents( + Context context, NotificationDetails notificationDetails) { + initAndroidThreeTen(context); + String nextFireDate = getNextFireDateMatchingDateTimeComponents(notificationDetails); + if (nextFireDate == null) { + return; + } + notificationDetails.scheduledDateTime = nextFireDate; + 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; + } + + static String getNextFireDateMatchingDateTimeComponents(NotificationDetails notificationDetails) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + ZoneId zoneId = ZoneId.of(notificationDetails.timeZoneName); + ZonedDateTime scheduledDateTime = + ZonedDateTime.of(LocalDateTime.parse(notificationDetails.scheduledDateTime), zoneId); + ZonedDateTime now = ZonedDateTime.now(zoneId); + ZonedDateTime nextFireDate = + ZonedDateTime.of( + now.getYear(), + now.getMonthValue(), + now.getDayOfMonth(), + scheduledDateTime.getHour(), + scheduledDateTime.getMinute(), + scheduledDateTime.getSecond(), + scheduledDateTime.getNano(), + zoneId); + while (nextFireDate.isBefore(now)) { + // adjust to be a date in the future that matches the time + nextFireDate = nextFireDate.plusDays(1); + } + if (notificationDetails.matchDateTimeComponents == DateTimeComponents.Time) { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } else if (notificationDetails.matchDateTimeComponents + == DateTimeComponents.DayOfWeekAndTime) { + while (nextFireDate.getDayOfWeek() != scheduledDateTime.getDayOfWeek()) { + nextFireDate = nextFireDate.plusDays(1); + } + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } else if (notificationDetails.matchDateTimeComponents + == DateTimeComponents.DayOfMonthAndTime) { + while (nextFireDate.getDayOfMonth() != scheduledDateTime.getDayOfMonth()) { + nextFireDate = nextFireDate.plusDays(1); + } + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } else if (notificationDetails.matchDateTimeComponents == DateTimeComponents.DateAndTime) { + while (nextFireDate.getMonthValue() != scheduledDateTime.getMonthValue() + || nextFireDate.getDayOfMonth() != scheduledDateTime.getDayOfMonth()) { + nextFireDate = nextFireDate.plusDays(1); + } + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } + } else { + org.threeten.bp.ZoneId zoneId = org.threeten.bp.ZoneId.of(notificationDetails.timeZoneName); + org.threeten.bp.ZonedDateTime scheduledDateTime = + org.threeten.bp.ZonedDateTime.of( + org.threeten.bp.LocalDateTime.parse(notificationDetails.scheduledDateTime), zoneId); + org.threeten.bp.ZonedDateTime now = org.threeten.bp.ZonedDateTime.now(zoneId); + org.threeten.bp.ZonedDateTime nextFireDate = + org.threeten.bp.ZonedDateTime.of( + now.getYear(), + now.getMonthValue(), + now.getDayOfMonth(), + scheduledDateTime.getHour(), + scheduledDateTime.getMinute(), + scheduledDateTime.getSecond(), + scheduledDateTime.getNano(), + zoneId); + while (nextFireDate.isBefore(now)) { + // adjust to be a date in the future that matches the time + nextFireDate = nextFireDate.plusDays(1); + } + if (notificationDetails.matchDateTimeComponents == DateTimeComponents.Time) { + return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } else if (notificationDetails.matchDateTimeComponents + == DateTimeComponents.DayOfWeekAndTime) { + while (nextFireDate.getDayOfWeek() != scheduledDateTime.getDayOfWeek()) { + nextFireDate = nextFireDate.plusDays(1); + } + return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } else if (notificationDetails.matchDateTimeComponents + == DateTimeComponents.DayOfMonthAndTime) { + while (nextFireDate.getDayOfMonth() != scheduledDateTime.getDayOfMonth()) { + nextFireDate = nextFireDate.plusDays(1); + } + return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } else if (notificationDetails.matchDateTimeComponents == DateTimeComponents.DateAndTime) { + while (nextFireDate.getMonthValue() != scheduledDateTime.getMonthValue() + || nextFireDate.getDayOfMonth() != scheduledDateTime.getDayOfMonth()) { + nextFireDate = nextFireDate.plusDays(1); + } + return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); + } + } + return null; + } + + private static NotificationManagerCompat getNotificationManager(Context context) { + return NotificationManagerCompat.from(context); + } + + private static boolean launchedActivityFromHistory(Intent intent) { + return intent != null + && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) + == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY; + } + + private void setActivity(Activity flutterActivity) { + this.mainActivity = flutterActivity; + if (mainActivity != null) { + launchIntent = mainActivity.getIntent(); + } + } + + private void onAttachedToEngine(Context context, BinaryMessenger binaryMessenger) { + this.applicationContext = context; + this.channel = new MethodChannel(binaryMessenger, METHOD_CHANNEL); + this.channel.setMethodCallHandler(this); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) {} + + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + binding.addOnNewIntentListener(this); + mainActivity = binding.getActivity(); + launchIntent = mainActivity.getIntent(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + this.mainActivity = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + binding.addOnNewIntentListener(this); + mainActivity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivity() { + this.mainActivity = null; + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + switch (call.method) { + case INITIALIZE_METHOD: + { + initialize(call, result); + break; + } + case GET_CALLBACK_HANDLE_METHOD: + { + getCallbackHandle(result); + break; + } + case GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD: + { + getNotificationAppLaunchDetails(result); + break; + } + case SHOW_METHOD: + { + show(call, result); + break; + } + case SCHEDULE_METHOD: + { + 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: + { + repeat(call, result); + break; + } + case CANCEL_METHOD: + cancel(call, result); + break; + case CANCEL_ALL_METHOD: + cancelAllNotifications(result); + break; + case PENDING_NOTIFICATION_REQUESTS_METHOD: + pendingNotificationRequests(result); + break; + case CREATE_NOTIFICATION_CHANNEL_GROUP_METHOD: + createNotificationChannelGroup(call, result); + break; + case DELETE_NOTIFICATION_CHANNEL_GROUP_METHOD: + deleteNotificationChannelGroup(call, result); + break; + case CREATE_NOTIFICATION_CHANNEL_METHOD: + createNotificationChannel(call, result); + break; + case DELETE_NOTIFICATION_CHANNEL_METHOD: + deleteNotificationChannel(call, result); + break; + case GET_ACTIVE_NOTIFICATIONS_METHOD: + getActiveNotifications(result); + break; + case GET_NOTIFICATION_CHANNELS_METHOD: + getNotificationChannels(result); + break; + case START_FOREGROUND_SERVICE: + startForegroundService(call, result); + break; + case STOP_FOREGROUND_SERVICE: + stopForegroundService(result); + break; + default: + result.notImplemented(); + break; + } + } + + private void pendingNotificationRequests(Result result) { + ArrayList scheduledNotifications = + loadScheduledNotifications(applicationContext); + List> pendingNotifications = new ArrayList<>(); + + for (NotificationDetails scheduledNotification : scheduledNotifications) { + HashMap pendingNotification = new HashMap<>(); + pendingNotification.put("id", scheduledNotification.id); + pendingNotification.put("title", scheduledNotification.title); + pendingNotification.put("body", scheduledNotification.body); + pendingNotification.put("payload", scheduledNotification.payload); + pendingNotifications.add(pendingNotification); + } + result.success(pendingNotifications); + } + + private void cancel(MethodCall call, Result result) { + Map arguments = call.arguments(); + Integer id = (Integer) arguments.get(CANCEL_ID); + String tag = (String) arguments.get(CANCEL_TAG); + cancelNotification(id, tag); + result.success(null); + } + + private void repeat(MethodCall call, Result result) { + Map arguments = call.arguments(); + NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + if (notificationDetails != null) { + repeatNotification(applicationContext, notificationDetails, true); + result.success(null); + } + } + + private void schedule(MethodCall call, Result result) { + Map arguments = call.arguments(); + NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + if (notificationDetails != null) { + scheduleNotification(applicationContext, notificationDetails, true); + result.success(null); + } + } + + private void zonedSchedule(MethodCall call, Result result) { + Map arguments = call.arguments(); + NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + if (notificationDetails != null) { + if (notificationDetails.matchDateTimeComponents != null) { + notificationDetails.scheduledDateTime = + getNextFireDateMatchingDateTimeComponents(notificationDetails); + } + zonedScheduleNotification(applicationContext, notificationDetails, true); + result.success(null); + } + } + + private void show(MethodCall call, Result result) { + Map arguments = call.arguments(); + NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + if (notificationDetails != null) { + showNotification(applicationContext, notificationDetails); + result.success(null); + } + } + + private void getNotificationAppLaunchDetails(Result result) { + Map notificationAppLaunchDetails = new HashMap<>(); + String payload = null; + Boolean notificationLaunchedApp = + mainActivity != null + && SELECT_NOTIFICATION.equals(mainActivity.getIntent().getAction()) + && !launchedActivityFromHistory(mainActivity.getIntent()); + notificationAppLaunchDetails.put(NOTIFICATION_LAUNCHED_APP, notificationLaunchedApp); + if (notificationLaunchedApp) { + payload = launchIntent.getStringExtra(PAYLOAD); + } + notificationAppLaunchDetails.put(PAYLOAD, payload); + result.success(notificationAppLaunchDetails); + } + + private void initialize(MethodCall call, Result result) { + Map arguments = call.arguments(); + String defaultIcon = (String) arguments.get(DEFAULT_ICON); + if (!isValidDrawableResource( + applicationContext, defaultIcon, result, INVALID_ICON_ERROR_CODE)) { + return; + } + + Long dispatcherHandle = call.argument(DISPATCHER_HANDLE); + Long callbackHandle = call.argument(CALLBACK_HANDLE); + if (dispatcherHandle != null && callbackHandle != null) { + IsolatePreferences.saveCallbackKeys(applicationContext, dispatcherHandle, callbackHandle); + } + + initAndroidThreeTen(applicationContext); + + SharedPreferences sharedPreferences = + applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(DEFAULT_ICON, defaultIcon); + tryCommittingInBackground(editor, 3); + result.success(true); + } + + private void getCallbackHandle(Result result) { + final Long handle = IsolatePreferences.getCallbackHandle(applicationContext); + result.success(handle); + } + + /// Extracts the details of the notifications passed from the Flutter side and also validates that + // some of the details (especially resources) passed are valid + private NotificationDetails extractNotificationDetails( + Result result, Map arguments) { + NotificationDetails notificationDetails = NotificationDetails.from(arguments); + if (hasInvalidIcon(result, notificationDetails.icon) + || hasInvalidLargeIcon( + result, notificationDetails.largeIcon, notificationDetails.largeIconBitmapSource) + || hasInvalidBigPictureResources(result, notificationDetails) + || hasInvalidRawSoundResource(result, notificationDetails) + || hasInvalidLedDetails(result, notificationDetails)) { + return null; + } + + return notificationDetails; + } + + private boolean hasInvalidLedDetails(Result result, NotificationDetails notificationDetails) { + if (notificationDetails.ledColor != null + && (notificationDetails.ledOnMs == null || notificationDetails.ledOffMs == null)) { + result.error(INVALID_LED_DETAILS_ERROR_CODE, INVALID_LED_DETAILS_ERROR_MESSAGE, null); + return true; + } + return false; + } + + private boolean hasInvalidRawSoundResource( + Result result, NotificationDetails notificationDetails) { + 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, + String.format(INVALID_RAW_RESOURCE_ERROR_MESSAGE, notificationDetails.sound), + null); return true; - } - - static void showNotification(Context context, NotificationDetails notificationDetails) { - Notification notification = createNotification(context, notificationDetails); - NotificationManagerCompat notificationManagerCompat = getNotificationManager(context); - notificationManagerCompat.notify(notificationDetails.id, notification); - } - - static void zonedScheduleNextNotification(Context context, NotificationDetails notificationDetails) { - initAndroidThreeTen(context); - String nextFireDate = getNextFireDate(notificationDetails); - if (nextFireDate == null) { - return; - } - notificationDetails.scheduledDateTime = nextFireDate; - zonedScheduleNotification(context, notificationDetails, true); - } - - static void zonedScheduleNextNotificationMatchingDateComponents(Context context, NotificationDetails notificationDetails) { - initAndroidThreeTen(context); - String nextFireDate = getNextFireDateMatchingDateTimeComponents(notificationDetails); - if (nextFireDate == null) { - return; - } - notificationDetails.scheduledDateTime = nextFireDate; - zonedScheduleNotification(context, notificationDetails, true); - } - - static String getNextFireDate(NotificationDetails notificationDetails) { + } + } + return false; + } + + private boolean hasInvalidBigPictureResources( + Result result, NotificationDetails notificationDetails) { + if (notificationDetails.style == NotificationStyle.BigPicture) { + BigPictureStyleInformation bigPictureStyleInformation = + (BigPictureStyleInformation) notificationDetails.styleInformation; + if (hasInvalidLargeIcon( + result, + bigPictureStyleInformation.largeIcon, + bigPictureStyleInformation.largeIconBitmapSource)) return true; + + if (bigPictureStyleInformation.bigPictureBitmapSource == BitmapSource.DrawableResource) { + String bigPictureResourceName = (String) bigPictureStyleInformation.bigPicture; + return StringUtils.isNullOrEmpty(bigPictureResourceName) + && !isValidDrawableResource( + applicationContext, bigPictureResourceName, result, INVALID_BIG_PICTURE_ERROR_CODE); + } else if (bigPictureStyleInformation.bigPictureBitmapSource == BitmapSource.FilePath) { + String largeIconPath = (String) bigPictureStyleInformation.bigPicture; + return StringUtils.isNullOrEmpty(largeIconPath); + } else if (bigPictureStyleInformation.bigPictureBitmapSource == BitmapSource.ByteArray) { + byte[] byteArray = (byte[]) bigPictureStyleInformation.bigPicture; + return byteArray == null || byteArray.length == 0; + } + } + return false; + } + + private boolean hasInvalidLargeIcon( + Result result, Object largeIcon, BitmapSource largeIconBitmapSource) { + if (largeIconBitmapSource == BitmapSource.DrawableResource + || largeIconBitmapSource == BitmapSource.FilePath) { + String largeIconPath = (String) largeIcon; + return !StringUtils.isNullOrEmpty(largeIconPath) + && largeIconBitmapSource == BitmapSource.DrawableResource + && !isValidDrawableResource( + applicationContext, largeIconPath, result, INVALID_LARGE_ICON_ERROR_CODE); + } else if (largeIconBitmapSource == BitmapSource.ByteArray) { + byte[] byteArray = (byte[]) largeIcon; + return byteArray.length == 0; + } + return false; + } + + private boolean hasInvalidIcon(Result result, String icon) { + return !StringUtils.isNullOrEmpty(icon) + && !isValidDrawableResource(applicationContext, icon, result, INVALID_ICON_ERROR_CODE); + } + + private void cancelNotification(Integer id, String tag) { + Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); + PendingIntent pendingIntent = getBroadcastPendingIntent(applicationContext, id, intent); + AlarmManager alarmManager = getAlarmManager(applicationContext); + alarmManager.cancel(pendingIntent); + NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); + if (tag == null) { + notificationManager.cancel(id); + } else { + notificationManager.cancel(tag, id); + } + removeNotificationFromCache(applicationContext, id); + } + + private void cancelAllNotifications(Result result) { + NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); + notificationManager.cancelAll(); + ArrayList scheduledNotifications = + loadScheduledNotifications(applicationContext); + if (scheduledNotifications == null || scheduledNotifications.isEmpty()) { + result.success(null); + return; + } + + Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); + for (NotificationDetails scheduledNotification : scheduledNotifications) { + PendingIntent pendingIntent = + getBroadcastPendingIntent(applicationContext, scheduledNotification.id, intent); + AlarmManager alarmManager = getAlarmManager(applicationContext); + alarmManager.cancel(pendingIntent); + } + + saveScheduledNotifications(applicationContext, new ArrayList()); + result.success(null); + } + + @Override + public boolean onNewIntent(Intent intent) { + boolean res = sendNotificationPayloadMessage(intent); + if (res && mainActivity != null) { + mainActivity.setIntent(intent); + } + return res; + } + + private Boolean sendNotificationPayloadMessage(Intent intent) { + if (SELECT_NOTIFICATION.equals(intent.getAction())) { + String payload = intent.getStringExtra(PAYLOAD); + channel.invokeMethod("selectNotification", payload); + return true; + } + return false; + } + + private void createNotificationChannelGroup(MethodCall call, Result result) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + Map arguments = call.arguments(); + NotificationChannelGroupDetails notificationChannelGroupDetails = + NotificationChannelGroupDetails.from(arguments); + NotificationManager notificationManager = + (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannelGroup notificationChannelGroup = + new NotificationChannelGroup( + notificationChannelGroupDetails.id, notificationChannelGroupDetails.name); + if (VERSION.SDK_INT >= VERSION_CODES.P) { + notificationChannelGroup.setDescription(notificationChannelGroupDetails.description); + } + notificationManager.createNotificationChannelGroup(notificationChannelGroup); + } + result.success(null); + } + + private void deleteNotificationChannelGroup(MethodCall call, Result result) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + NotificationManager notificationManager = + (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); + String groupId = call.arguments(); + notificationManager.deleteNotificationChannelGroup(groupId); + } + result.success(null); + } + + private void createNotificationChannel(MethodCall call, Result result) { + Map arguments = call.arguments(); + NotificationChannelDetails notificationChannelDetails = + NotificationChannelDetails.from(arguments); + setupNotificationChannel(applicationContext, notificationChannelDetails); + result.success(null); + } + + private void deleteNotificationChannel(MethodCall call, Result result) { + 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) { - 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); + activeNotificationPayload.put("channelId", notification.getChannelId()); + } + + activeNotificationPayload.put("groupKey", notification.getGroup()); + activeNotificationPayload.put( + "title", notification.extras.getCharSequence("android.title")); + activeNotificationPayload.put("body", notification.extras.getCharSequence("android.text")); + activeNotificationsPayload.add(activeNotificationPayload); + } + result.success(activeNotificationsPayload); + } catch (Throwable e) { + result.error(GET_ACTIVE_NOTIFICATIONS_ERROR_CODE, e.getMessage(), e.getStackTrace()); + } + } + + private void getNotificationChannels(Result result) { + try { + NotificationManagerCompat notificationManagerCompat = + getNotificationManager(applicationContext); + List channels = notificationManagerCompat.getNotificationChannels(); + List> channelsPayload = new ArrayList<>(); + for (NotificationChannel channel : channels) { + HashMap channelPayload = getMappedNotificationChannel(channel); + channelsPayload.add(channelPayload); + } + result.success(channelsPayload); + } catch (Throwable e) { + result.error(GET_NOTIFICATION_CHANNELS_ERROR_CODE, e.getMessage(), e.getStackTrace()); + } + } + + private HashMap getMappedNotificationChannel(NotificationChannel channel) { + HashMap channelPayload = new HashMap<>(); + if (VERSION.SDK_INT >= VERSION_CODES.O) { + channelPayload.put("id", channel.getId()); + channelPayload.put("name", channel.getName()); + channelPayload.put("description", channel.getDescription()); + channelPayload.put("groupId", channel.getGroup()); + channelPayload.put("showBadge", channel.canShowBadge()); + channelPayload.put("importance", channel.getImportance()); + Uri soundUri = channel.getSound(); + if (soundUri == null) { + channelPayload.put("sound", null); + channelPayload.put("playSound", false); + } else { + channelPayload.put("playSound", true); + List soundSources = Arrays.asList(SoundSource.values()); + if (soundUri.getScheme().equals("android.resource")) { + String[] splitUri = soundUri.toString().split("/"); + String resource = splitUri[splitUri.length - 1]; + Integer resourceId = tryParseInt(resource); + if (resourceId == null) { + channelPayload.put("soundSource", soundSources.indexOf(SoundSource.RawResource)); + channelPayload.put("sound", resource); + } else { + // Kept for backwards compatibility when the source resource used to be based on id + String resourceName = + applicationContext.getResources().getResourceEntryName(resourceId); + if (resourceName != null) { + channelPayload.put("soundSource", soundSources.indexOf(SoundSource.RawResource)); + channelPayload.put("sound", resourceName); } + } } 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; - } - - static String getNextFireDateMatchingDateTimeComponents(NotificationDetails notificationDetails) { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - ZoneId zoneId = ZoneId.of(notificationDetails.timeZoneName); - ZonedDateTime scheduledDateTime = ZonedDateTime.of(LocalDateTime.parse(notificationDetails.scheduledDateTime), zoneId); - ZonedDateTime now = ZonedDateTime.now(zoneId); - ZonedDateTime nextFireDate = ZonedDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), scheduledDateTime.getHour(), scheduledDateTime.getMinute(), scheduledDateTime.getSecond(), scheduledDateTime.getNano(), zoneId); - while (nextFireDate.isBefore(now)) { - // adjust to be a date in the future that matches the time - nextFireDate = nextFireDate.plusDays(1); - } - if (notificationDetails.matchDateTimeComponents == DateTimeComponents.Time) { - return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); - } else if (notificationDetails.matchDateTimeComponents == DateTimeComponents.DayOfWeekAndTime) { - while (nextFireDate.getDayOfWeek() != scheduledDateTime.getDayOfWeek()) { - nextFireDate = nextFireDate.plusDays(1); - } - return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); - } - } else { - org.threeten.bp.ZoneId zoneId = org.threeten.bp.ZoneId.of(notificationDetails.timeZoneName); - org.threeten.bp.ZonedDateTime scheduledDateTime = org.threeten.bp.ZonedDateTime.of(org.threeten.bp.LocalDateTime.parse(notificationDetails.scheduledDateTime), zoneId); - org.threeten.bp.ZonedDateTime now = org.threeten.bp.ZonedDateTime.now(zoneId); - org.threeten.bp.ZonedDateTime nextFireDate = org.threeten.bp.ZonedDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), scheduledDateTime.getHour(), scheduledDateTime.getMinute(), scheduledDateTime.getSecond(), scheduledDateTime.getNano(), zoneId); - while (nextFireDate.isBefore(now)) { - // adjust to be a date in the future that matches the time - nextFireDate = nextFireDate.plusDays(1); - } - if (notificationDetails.matchDateTimeComponents == DateTimeComponents.Time) { - return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); - } else if (notificationDetails.matchDateTimeComponents == DateTimeComponents.DayOfWeekAndTime) { - while (nextFireDate.getDayOfWeek() != scheduledDateTime.getDayOfWeek()) { - nextFireDate = nextFireDate.plusDays(1); - } - return org.threeten.bp.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(nextFireDate); - } - } - 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) { - this.applicationContext = context; - this.channel = new MethodChannel(binaryMessenger, METHOD_CHANNEL); - this.channel.setMethodCallHandler(this); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - } - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - binding.addOnNewIntentListener(this); - mainActivity = binding.getActivity(); - launchIntent = mainActivity.getIntent(); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - this.mainActivity = null; - } - - @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - binding.addOnNewIntentListener(this); - mainActivity = binding.getActivity(); - } - - @Override - public void onDetachedFromActivity() { - this.mainActivity = null; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - switch (call.method) { - case INITIALIZE_METHOD: { - initialize(call, result); - break; - } - case GET_CALLBACK_HANDLE_METHOD: { - getCallbackHandle(result); - break; - } - case GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD: { - getNotificationAppLaunchDetails(result); - break; - } - case SHOW_METHOD: { - show(call, result); - break; - } - case SCHEDULE_METHOD: { - 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: { - repeat(call, result); - break; - } - case CANCEL_METHOD: - cancel(call, result); - break; - case CANCEL_ALL_METHOD: - cancelAllNotifications(result); - break; - case PENDING_NOTIFICATION_REQUESTS_METHOD: - pendingNotificationRequests(result); - break; - case CREATE_NOTIFICATION_CHANNEL_GROUP_METHOD: - createNotificationChannelGroup(call, result); - break; - case DELETE_NOTIFICATION_CHANNEL_GROUP_METHOD: - deleteNotificationChannelGroup(call, result); - break; - case CREATE_NOTIFICATION_CHANNEL_METHOD: - createNotificationChannel(call, result); - break; - case DELETE_NOTIFICATION_CHANNEL_METHOD: - deleteNotificationChannel(call, result); - break; - case GET_ACTIVE_NOTIFICATIONS_METHOD: - getActiveNotifications(result); - break; - default: - result.notImplemented(); - break; - } - } - - private void pendingNotificationRequests(Result result) { - ArrayList scheduledNotifications = loadScheduledNotifications(applicationContext); - List> pendingNotifications = new ArrayList<>(); - - for (NotificationDetails scheduledNotification : scheduledNotifications) { - HashMap pendingNotification = new HashMap<>(); - pendingNotification.put("id", scheduledNotification.id); - pendingNotification.put("title", scheduledNotification.title); - pendingNotification.put("body", scheduledNotification.body); - pendingNotification.put("payload", scheduledNotification.payload); - pendingNotifications.add(pendingNotification); - } - result.success(pendingNotifications); - } - - private void cancel(MethodCall call, Result result) { - Integer id = call.arguments(); - cancelNotification(id); - result.success(null); - } - - private void repeat(MethodCall call, Result result) { - Map arguments = call.arguments(); - NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); - if (notificationDetails != null) { - repeatNotification(applicationContext, notificationDetails, true); - result.success(null); - } - } - - private void schedule(MethodCall call, Result result) { - Map arguments = call.arguments(); - NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); - if (notificationDetails != null) { - scheduleNotification(applicationContext, notificationDetails, true); - result.success(null); - } - } - - private void zonedSchedule(MethodCall call, Result result) { - Map arguments = call.arguments(); - NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + channelPayload.put("soundSource", soundSources.indexOf(SoundSource.Uri)); + channelPayload.put("sound", soundUri.toString()); + } + } + channelPayload.put("enableVibration", channel.shouldVibrate()); + channelPayload.put("vibrationPattern", channel.getVibrationPattern()); + channelPayload.put("enableLights", channel.shouldShowLights()); + channelPayload.put("ledColor", channel.getLightColor()); + } + return channelPayload; + } + + private Integer tryParseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return null; + } + } + + private void startForegroundService(MethodCall call, Result result) { + Map notificationData = call.>argument("notificationData"); + Integer startType = call.argument("startType"); + ArrayList foregroundServiceTypes = call.argument("foregroundServiceTypes"); + if (foregroundServiceTypes == null || foregroundServiceTypes.size() != 0) { + if (notificationData != null && startType != null) { + NotificationDetails notificationDetails = + extractNotificationDetails(result, notificationData); if (notificationDetails != null) { - if (notificationDetails.matchDateTimeComponents != null) { - notificationDetails.scheduledDateTime = getNextFireDateMatchingDateTimeComponents(notificationDetails); - } - zonedScheduleNotification(applicationContext, notificationDetails, true); - result.success(null); - } - } - - private void show(MethodCall call, Result result) { - Map arguments = call.arguments(); - NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); - if (notificationDetails != null) { - showNotification(applicationContext, notificationDetails); - result.success(null); - } - } - - private void getNotificationAppLaunchDetails(Result result) { - Map notificationAppLaunchDetails = new HashMap<>(); - String payload = null; - Boolean notificationLaunchedApp = mainActivity != null && SELECT_NOTIFICATION.equals(mainActivity.getIntent().getAction()) && !launchedActivityFromHistory(mainActivity.getIntent()); - notificationAppLaunchDetails.put(NOTIFICATION_LAUNCHED_APP, notificationLaunchedApp); - if (notificationLaunchedApp) { - payload = launchIntent.getStringExtra(PAYLOAD); - } - notificationAppLaunchDetails.put(PAYLOAD, payload); - result.success(notificationAppLaunchDetails); - } - - private void initialize(MethodCall call, Result result) { - Map arguments = call.arguments(); - String defaultIcon = (String) arguments.get(DEFAULT_ICON); - if (!isValidDrawableResource(applicationContext, defaultIcon, result, INVALID_ICON_ERROR_CODE)) { - return; - } - - Long dispatcherHandle = call.argument(DISPATCHER_HANDLE); - Long callbackHandle = call.argument(CALLBACK_HANDLE); - if (dispatcherHandle != null && callbackHandle != null) { - IsolatePreferences.saveCallbackKeys(applicationContext, dispatcherHandle, callbackHandle); - } - - initAndroidThreeTen(applicationContext); - - SharedPreferences sharedPreferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(DEFAULT_ICON, defaultIcon); - editor.commit(); - - if (mainActivity != null && !launchedActivityFromHistory(mainActivity.getIntent())) { - sendNotificationPayloadMessage(mainActivity.getIntent()); - } - result.success(true); - } - - private void getCallbackHandle( Result result) { - final Long handle = IsolatePreferences.getCallbackHandle(applicationContext); - result.success(handle); - } - - private static boolean launchedActivityFromHistory(Intent intent) { - return intent != null && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY; - } - - - /// Extracts the details of the notifications passed from the Flutter side and also validates that some of the details (especially resources) passed are valid - private NotificationDetails extractNotificationDetails(Result result, Map arguments) { - NotificationDetails notificationDetails = NotificationDetails.from(arguments); - if (hasInvalidIcon(result, notificationDetails.icon) || - hasInvalidLargeIcon(result, notificationDetails.largeIcon, notificationDetails.largeIconBitmapSource) || - hasInvalidBigPictureResources(result, notificationDetails) || - hasInvalidRawSoundResource(result, notificationDetails) || - hasInvalidLedDetails(result, notificationDetails)) { - return null; - } - - return notificationDetails; - } - - private boolean hasInvalidLedDetails(Result result, NotificationDetails notificationDetails) { - if (notificationDetails.ledColor != null && (notificationDetails.ledOnMs == null || notificationDetails.ledOffMs == null)) { - result.error(INVALID_LED_DETAILS_ERROR_CODE, INVALID_LED_DETAILS_ERROR_MESSAGE, null); - return true; - } - return false; - } - - private boolean hasInvalidRawSoundResource(Result result, NotificationDetails notificationDetails) { - 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, String.format(INVALID_RAW_RESOURCE_ERROR_MESSAGE, notificationDetails.sound), null); - return true; - } - } - return false; - } - - private boolean hasInvalidBigPictureResources(Result result, NotificationDetails notificationDetails) { - if (notificationDetails.style == NotificationStyle.BigPicture) { - BigPictureStyleInformation bigPictureStyleInformation = (BigPictureStyleInformation) notificationDetails.styleInformation; - if (hasInvalidLargeIcon(result, bigPictureStyleInformation.largeIcon, bigPictureStyleInformation.largeIconBitmapSource)) - return true; - return bigPictureStyleInformation.bigPictureBitmapSource == BitmapSource.DrawableResource && !isValidDrawableResource(applicationContext, bigPictureStyleInformation.bigPicture, result, INVALID_BIG_PICTURE_ERROR_CODE); - } - return false; - } - - private boolean hasInvalidLargeIcon(Result result, String largeIcon, BitmapSource largeIconBitmapSource) { - return !StringUtils.isNullOrEmpty(largeIcon) && largeIconBitmapSource == BitmapSource.DrawableResource && !isValidDrawableResource(applicationContext, largeIcon, result, INVALID_LARGE_ICON_ERROR_CODE); - } - - private boolean hasInvalidIcon(Result result, String icon) { - return !StringUtils.isNullOrEmpty(icon) && !isValidDrawableResource(applicationContext, icon, result, INVALID_ICON_ERROR_CODE); - } - - private void cancelNotification(Integer id) { - Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(applicationContext, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); - AlarmManager alarmManager = getAlarmManager(applicationContext); - alarmManager.cancel(pendingIntent); - NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); - notificationManager.cancel(id); - removeNotificationFromCache(applicationContext, id); - } - - private void cancelAllNotifications(Result result) { - NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); - notificationManager.cancelAll(); - ArrayList scheduledNotifications = loadScheduledNotifications(applicationContext); - if (scheduledNotifications == null || scheduledNotifications.isEmpty()) { + if (notificationDetails.id != 0) { + ForegroundServiceStartParameter parameter = + new ForegroundServiceStartParameter( + notificationDetails, startType, foregroundServiceTypes); + Intent intent = new Intent(applicationContext, ForegroundService.class); + intent.putExtra(ForegroundServiceStartParameter.EXTRA, parameter); + ContextCompat.startForegroundService(applicationContext, intent); result.success(null); - return; - } - - Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); - for (NotificationDetails scheduledNotification : - scheduledNotifications) { - PendingIntent pendingIntent = PendingIntent.getBroadcast(applicationContext, scheduledNotification.id, intent, PendingIntent.FLAG_UPDATE_CURRENT); - AlarmManager alarmManager = getAlarmManager(applicationContext); - alarmManager.cancel(pendingIntent); - } - - saveScheduledNotifications(applicationContext, new ArrayList()); - result.success(null); - } - - @Override - public boolean onNewIntent(Intent intent) { - boolean res = sendNotificationPayloadMessage(intent); - if (res && mainActivity != null) { - mainActivity.setIntent(intent); - } - return res; - } - - private Boolean sendNotificationPayloadMessage(Intent intent) { - if (SELECT_NOTIFICATION.equals(intent.getAction())) { - String payload = intent.getStringExtra(PAYLOAD); - channel.invokeMethod("selectNotification", payload); - return true; - } - return false; - } - - private void createNotificationChannelGroup(MethodCall call, Result result) { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - Map arguments = call.arguments(); - NotificationChannelGroupDetails notificationChannelGroupDetails = NotificationChannelGroupDetails.from(arguments); - NotificationManager notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationChannelGroup notificationChannelGroup = new NotificationChannelGroup(notificationChannelGroupDetails.id, notificationChannelGroupDetails.name); - if (VERSION.SDK_INT >= VERSION_CODES.P) { - notificationChannelGroup.setDescription(notificationChannelGroupDetails.description); - } - notificationManager.createNotificationChannelGroup(notificationChannelGroup); - } - result.success(null); - } - - private void deleteNotificationChannelGroup(MethodCall call, Result result) { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - NotificationManager notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); - String groupId = call.arguments(); - notificationManager.deleteNotificationChannelGroup(groupId); - } - result.success(null); - } - - private void createNotificationChannel(MethodCall call, Result result) { - Map arguments = call.arguments(); - NotificationChannelDetails notificationChannelDetails = NotificationChannelDetails.from(arguments); - setupNotificationChannel(applicationContext, notificationChannelDetails); - result.success(null); - } - - private void deleteNotificationChannel(MethodCall call, Result result) { - 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()); - } - } + } else { + result.error( + "ARGUMENT_ERROR", + "The id of the notification for a foreground service must not be 0!", + null); + } + } + } else { + result.error( + "ARGUMENT_ERROR", "An argument passed to startForegroundService was null!", null); + } + } else { + result.error( + "ARGUMENT_ERROR", "If foregroundServiceTypes is non-null it must not be empty!", null); + } + } + + private void stopForegroundService(Result result) { + applicationContext.stopService(new Intent(applicationContext, ForegroundService.class)); + result.success(null); + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java new file mode 100644 index 000000000..cb646ca26 --- /dev/null +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java @@ -0,0 +1,44 @@ +package com.dexterous.flutterlocalnotifications; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; + +import java.util.ArrayList; + +public class ForegroundService extends Service { + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + ForegroundServiceStartParameter parameter = + (ForegroundServiceStartParameter) + intent.getSerializableExtra(ForegroundServiceStartParameter.EXTRA); + Notification notification = + FlutterLocalNotificationsPlugin.createNotification(this, parameter.notificationData); + if (parameter.foregroundServiceTypes != null + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + parameter.notificationData.id, + notification, + orCombineFlags(parameter.foregroundServiceTypes)); + } else { + startForeground(parameter.notificationData.id, notification); + } + return parameter.startMode; + } + + private static int orCombineFlags(ArrayList flags) { + int flag = flags.get(0); + for (int i = 1; i < flags.size(); i++) { + flag |= flags.get(i); + } + return flag; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundServiceStartParameter.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundServiceStartParameter.java new file mode 100644 index 000000000..c0becb23c --- /dev/null +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundServiceStartParameter.java @@ -0,0 +1,36 @@ +package com.dexterous.flutterlocalnotifications; + +import com.dexterous.flutterlocalnotifications.models.NotificationDetails; + +import java.io.Serializable; +import java.util.ArrayList; + +public class ForegroundServiceStartParameter implements Serializable { + public static final String EXTRA = + "com.dexterous.flutterlocalnotifications.ForegroundServiceStartParameter"; + + public final NotificationDetails notificationData; + public final int startMode; + public final ArrayList foregroundServiceTypes; + + public ForegroundServiceStartParameter( + NotificationDetails notificationData, + int startMode, + ArrayList foregroundServiceTypes) { + this.notificationData = notificationData; + this.startMode = startMode; + this.foregroundServiceTypes = foregroundServiceTypes; + } + + @Override + public String toString() { + return "ForegroundServiceStartParameter{" + + "notificationData=" + + notificationData + + ", startMode=" + + startMode + + ", foregroundServiceTypes=" + + foregroundServiceTypes + + '}'; + } +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java index 08beb04f3..829d62645 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java @@ -3,12 +3,11 @@ import androidx.annotation.Keep; @Keep -public enum NotificationStyle{ - Default, - BigPicture, - BigText, - Inbox, - Messaging, - Media +public enum NotificationStyle { + Default, + BigPicture, + BigText, + Inbox, + Messaging, + Media } - diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java index b7a8ffd38..490e74737 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java @@ -4,8 +4,8 @@ @Keep public enum RepeatInterval { - EveryMinute, - Hourly, - Daily, - Weekly + EveryMinute, + Hourly, + Daily, + Weekly } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RuntimeTypeAdapterFactory.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RuntimeTypeAdapterFactory.java index a1e40201b..db59fc52f 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RuntimeTypeAdapterFactory.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/RuntimeTypeAdapterFactory.java @@ -51,209 +51,238 @@ import com.google.gson.stream.JsonWriter; /** - * Adapts values whose runtime type may differ from their declaration type. This - * is necessary when a field's type is not the same type that GSON should create - * when deserializing that field. For example, consider these types: - *
   {@code
- *   abstract class Shape {
- *     int x;
- *     int y;
- *   }
- *   class Circle extends Shape {
- *     int radius;
- *   }
- *   class Rectangle extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Diamond extends Shape {
- *     int width;
- *     int height;
+ * Adapts values whose runtime type may differ from their declaration type. This is necessary when a
+ * field's type is not the same type that GSON should create when deserializing that field. For
+ * example, consider these types:
+ *
+ * 
{@code
+ * abstract class Shape {
+ *   int x;
+ *   int y;
+ * }
+ * class Circle extends Shape {
+ *   int radius;
+ * }
+ * class Rectangle extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Diamond extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Drawing {
+ *   Shape bottomShape;
+ *   Shape topShape;
+ * }
+ * }
+ * + *

Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in + * this drawing a rectangle or a diamond? + * + *

{@code
+ * {
+ *   "bottomShape": {
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
  *   }
- *   class Drawing {
- *     Shape bottomShape;
- *     Shape topShape;
+ * }
+ * }
+ * + * This class addresses this problem by adding type information to the serialized JSON and honoring + * that type information when the JSON is deserialized: + * + *
{@code
+ * {
+ *   "bottomShape": {
+ *     "type": "Diamond",
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "type": "Circle",
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
  *   }
+ * }
  * }
- *

Without additional type information, the serialized JSON is ambiguous. Is - * the bottom shape in this drawing a rectangle or a diamond?

   {@code
- *   {
- *     "bottomShape": {
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * This class addresses this problem by adding type information to the - * serialized JSON and honoring that type information when the JSON is - * deserialized:
   {@code
- *   {
- *     "bottomShape": {
- *       "type": "Diamond",
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "type": "Circle",
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * Both the type field name ({@code "type"}) and the type labels ({@code - * "Rectangle"}) are configurable. + * + * Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are + * configurable. * *

Registering Types

- * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field - * name to the {@link #of} factory method. If you don't supply an explicit type - * field name, {@code "type"} will be used.
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory
- *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ *
+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the
+ * {@link #of} factory method. If you don't supply an explicit type field name, {@code "type"} will
+ * be used.
+ *
+ * 
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory
+ *     = RuntimeTypeAdapterFactory.of(Shape.class, "type");
  * }
- * Next register all of your subtypes. Every subtype must be explicitly - * registered. This protects your application from injection attacks. If you - * don't supply an explicit type label, the type's simple name will be used. - *
   {@code
- *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
- *   shapeAdapter.registerSubtype(Circle.class, "Circle");
- *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
+ *
+ * Next register all of your subtypes. Every subtype must be explicitly registered. This protects
+ * your application from injection attacks. If you don't supply an explicit type label, the type's
+ * simple name will be used.
+ *
+ * 
{@code
+ * shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapter.registerSubtype(Circle.class, "Circle");
+ * shapeAdapter.registerSubtype(Diamond.class, "Diamond");
  * }
+ * * Finally, register the type adapter factory in your application's GSON builder: - *
   {@code
- *   Gson gson = new GsonBuilder()
- *       .registerTypeAdapterFactory(shapeAdapterFactory)
- *       .create();
+ *
+ * 
{@code
+ * Gson gson = new GsonBuilder()
+ *     .registerTypeAdapterFactory(shapeAdapterFactory)
+ *     .create();
  * }
- * Like {@code GsonBuilder}, this API supports chaining:
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
- *       .registerSubtype(Rectangle.class)
- *       .registerSubtype(Circle.class)
- *       .registerSubtype(Diamond.class);
+ *
+ * Like {@code GsonBuilder}, this API supports chaining:
+ *
+ * 
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *     .registerSubtype(Rectangle.class)
+ *     .registerSubtype(Circle.class)
+ *     .registerSubtype(Diamond.class);
  * }
*/ @Keep public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap>(); - private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); - private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName); - } + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code typeFieldName} as + * the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type field + * name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type"); + } - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory(baseType, "type"); + /** + * Registers {@code type} identified by {@code label}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} have already been + * registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are + * case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name have already been + * registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; } - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); + final Map> labelToDelegate = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate = + new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); } - public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() != baseType) { - return null; + return new TypeAdapter() { + @Override + public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + if (labelJsonElement == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " because it does not define a field named " + + typeFieldName); } - - final Map> labelToDelegate - = new LinkedHashMap>(); - final Map, TypeAdapter> subtypeToDelegate - = new LinkedHashMap, TypeAdapter>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " subtype named " + + label + + "; did you forget to register a subtype?"); } + return delegate.fromJsonTree(jsonElement); + } - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = Streams.parse(in); - JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - throw new JsonParseException("cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype?"); - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } - JsonObject clone = new JsonObject(); - clone.add(typeFieldName, new JsonPrimitive(label)); - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - Streams.write(clone, out); - } - }.nullSafe(); - } -} \ No newline at end of file + @Override + public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException( + "cannot serialize " + srcType.getName() + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException( + "cannot serialize " + + srcType.getName() + + " because it already defines a field named " + + typeFieldName); + } + JsonObject clone = new JsonObject(); + clone.add(typeFieldName, new JsonPrimitive(label)); + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationBootReceiver.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationBootReceiver.java index 51c824964..cb619a708 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationBootReceiver.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationBootReceiver.java @@ -7,19 +7,17 @@ import androidx.annotation.Keep; @Keep - -public class ScheduledNotificationBootReceiver extends BroadcastReceiver -{ - @Override - public void onReceive(final Context context, Intent intent) { - String action = intent.getAction(); - if (action != null) { - if (action.equals(android.content.Intent.ACTION_BOOT_COMPLETED) - || action.equals(Intent.ACTION_MY_PACKAGE_REPLACED) - || action.equals("android.intent.action.QUICKBOOT_POWERON") - || action.equals("com.htc.intent.action.QUICKBOOT_POWERON")) { - FlutterLocalNotificationsPlugin.rescheduleNotifications(context); - } - } +public class ScheduledNotificationBootReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, Intent intent) { + String action = intent.getAction(); + if (action != null) { + if (action.equals(android.content.Intent.ACTION_BOOT_COMPLETED) + || action.equals(Intent.ACTION_MY_PACKAGE_REPLACED) + || action.equals("android.intent.action.QUICKBOOT_POWERON") + || action.equals("com.htc.intent.action.QUICKBOOT_POWERON")) { + FlutterLocalNotificationsPlugin.rescheduleNotifications(context); + } } + } } 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 eaf3e7da8..92e4d3758 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 @@ -15,46 +15,42 @@ import java.lang.reflect.Type; -/** - * Created by michaelbui on 24/3/18. - */ - +/** Created by michaelbui on 24/3/18. */ @Keep public class ScheduledNotificationReceiver extends BroadcastReceiver { - - @Override - public void onReceive(final Context context, Intent intent) { - String notificationDetailsJson = intent.getStringExtra(FlutterLocalNotificationsPlugin.NOTIFICATION_DETAILS); - 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("notification_id", - 0); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - notificationManager.notify(notificationId, notification); - boolean repeat = intent.getBooleanExtra("repeat", false); - if (!repeat) { - FlutterLocalNotificationsPlugin.removeNotificationFromCache(context, notificationId); - } - } else { - Gson gson = FlutterLocalNotificationsPlugin.buildGson(); - Type type = new TypeToken() { - }.getType(); - NotificationDetails notificationDetails = gson.fromJson(notificationDetailsJson, type); - FlutterLocalNotificationsPlugin.showNotification(context, notificationDetails); - if (notificationDetails.scheduledNotificationRepeatFrequency != null) { - FlutterLocalNotificationsPlugin.zonedScheduleNextNotification(context, notificationDetails); - } else if (notificationDetails.matchDateTimeComponents != null) { - FlutterLocalNotificationsPlugin.zonedScheduleNextNotificationMatchingDateComponents(context, notificationDetails); - } else if (notificationDetails.repeatInterval != null) { - FlutterLocalNotificationsPlugin.scheduleNextRepeatingNotification(context, notificationDetails); - } else { - FlutterLocalNotificationsPlugin.removeNotificationFromCache(context, notificationDetails.id); - } - } - + @Override + public void onReceive(final Context context, Intent intent) { + String notificationDetailsJson = + intent.getStringExtra(FlutterLocalNotificationsPlugin.NOTIFICATION_DETAILS); + 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("notification_id", 0); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.notify(notificationId, notification); + boolean repeat = intent.getBooleanExtra("repeat", false); + if (!repeat) { + FlutterLocalNotificationsPlugin.removeNotificationFromCache(context, notificationId); + } + } else { + Gson gson = FlutterLocalNotificationsPlugin.buildGson(); + Type type = new TypeToken() {}.getType(); + NotificationDetails notificationDetails = gson.fromJson(notificationDetailsJson, type); + FlutterLocalNotificationsPlugin.showNotification(context, notificationDetails); + if (notificationDetails.scheduledNotificationRepeatFrequency != null) { + FlutterLocalNotificationsPlugin.zonedScheduleNextNotification(context, notificationDetails); + } else if (notificationDetails.matchDateTimeComponents != null) { + FlutterLocalNotificationsPlugin.zonedScheduleNextNotificationMatchingDateComponents( + context, notificationDetails); + } else if (notificationDetails.repeatInterval != null) { + FlutterLocalNotificationsPlugin.scheduleNextRepeatingNotification( + context, notificationDetails); + } else { + FlutterLocalNotificationsPlugin.removeNotificationFromCache( + context, notificationDetails.id); + } } - + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/SoundSource.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/SoundSource.java deleted file mode 100644 index 14b5c644a..000000000 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/SoundSource.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.dexterous.flutterlocalnotifications; - -import androidx.annotation.Keep; - -@Keep -public enum SoundSource { - RawResource, - Uri -} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/isolate/IsolatePreferences.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/isolate/IsolatePreferences.java index af043062f..4492e9f91 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/isolate/IsolatePreferences.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/isolate/IsolatePreferences.java @@ -5,31 +5,31 @@ public class IsolatePreferences { - private static final String SHARED_PREFS_FILE_NAME = "flutter_local_notifications_plugin"; - private static final String CALLBACK_DISPATCHER_HANDLE_KEY = - "com.dexterous.flutterlocalnotifications.CALLBACK_DISPATCHER_HANDLE_KEY"; - private static final String CALLBACK_HANDLE_KEY = - "com.dexterous.flutterlocalnotifications.CALLBACK_HANDLE_KEY"; - - public static SharedPreferences get(Context context) { - return context.getSharedPreferences(SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE); - } - - public static void saveCallbackKeys(Context context, Long dispatcherCallbackHandle, - Long callbackHandle) { - get(context).edit().putLong(CALLBACK_DISPATCHER_HANDLE_KEY, dispatcherCallbackHandle).apply(); - get(context).edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply(); - } - - public static Long getCallbackDispatcherHandle(Context context) { - return get(context).getLong(CALLBACK_DISPATCHER_HANDLE_KEY, -1L); - } - - public static Long getCallbackHandle(Context context) { - return get(context).getLong(CALLBACK_HANDLE_KEY, -1L); - } - - public static boolean hasCallbackHandle(Context context) { - return get(context).contains(CALLBACK_DISPATCHER_HANDLE_KEY); - } + private static final String SHARED_PREFS_FILE_NAME = "flutter_local_notifications_plugin"; + private static final String CALLBACK_DISPATCHER_HANDLE_KEY = + "com.dexterous.flutterlocalnotifications.CALLBACK_DISPATCHER_HANDLE_KEY"; + private static final String CALLBACK_HANDLE_KEY = + "com.dexterous.flutterlocalnotifications.CALLBACK_HANDLE_KEY"; + + public static SharedPreferences get(Context context) { + return context.getSharedPreferences(SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE); + } + + public static void saveCallbackKeys( + Context context, Long dispatcherCallbackHandle, Long callbackHandle) { + get(context).edit().putLong(CALLBACK_DISPATCHER_HANDLE_KEY, dispatcherCallbackHandle).apply(); + get(context).edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply(); + } + + public static Long getCallbackDispatcherHandle(Context context) { + return get(context).getLong(CALLBACK_DISPATCHER_HANDLE_KEY, -1L); + } + + public static Long getCallbackHandle(Context context) { + return get(context).getLong(CALLBACK_HANDLE_KEY, -1L); + } + + public static boolean hasCallbackHandle(Context context) { + return get(context).contains(CALLBACK_DISPATCHER_HANDLE_KEY); + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/BitmapSource.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/BitmapSource.java new file mode 100644 index 000000000..14bcf7a5a --- /dev/null +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/BitmapSource.java @@ -0,0 +1,10 @@ +package com.dexterous.flutterlocalnotifications.models; + +import androidx.annotation.Keep; + +@Keep +public enum BitmapSource { + DrawableResource, + FilePath, + ByteArray +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/DateTimeComponents.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/DateTimeComponents.java index 9de0e2865..ef4c03f98 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/DateTimeComponents.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/DateTimeComponents.java @@ -4,6 +4,8 @@ @Keep public enum DateTimeComponents { - Time, - DayOfWeekAndTime + Time, + DayOfWeekAndTime, + DayOfMonthAndTime, + DateAndTime } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/IconSource.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/IconSource.java index f37613c51..2fde7012d 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/IconSource.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/IconSource.java @@ -4,8 +4,9 @@ @Keep public enum IconSource { - DrawableResource, - BitmapFilePath, - ContentUri, - FlutterBitmapAsset + DrawableResource, + BitmapFilePath, + ContentUri, + FlutterBitmapAsset, + ByteArray } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/MessageDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/MessageDetails.java index bbdd6f1bb..754901aa5 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/MessageDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/MessageDetails.java @@ -2,19 +2,22 @@ import androidx.annotation.Keep; +import java.io.Serializable; + @Keep -public class MessageDetails { - public String text; - public Long timestamp; - public PersonDetails person; - public String dataMimeType; - public String dataUri; +public class MessageDetails implements Serializable { + public String text; + public Long timestamp; + public PersonDetails person; + public String dataMimeType; + public String dataUri; - public MessageDetails(String text, Long timestamp, PersonDetails person, String dataMimeType, String dataUri) { - this.text = text; - this.timestamp = timestamp; - this.person = person; - this.dataMimeType = dataMimeType; - this.dataUri = dataUri; - } + public MessageDetails( + String text, Long timestamp, PersonDetails person, String dataMimeType, String dataUri) { + this.text = text; + this.timestamp = timestamp; + this.person = person; + this.dataMimeType = dataMimeType; + this.dataUri = dataUri; + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationAction.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationAction.java index 16a12e401..54ee0dc9c 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationAction.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationAction.java @@ -6,62 +6,64 @@ import java.util.Map; public class NotificationAction { - public static class NotificationActionInput { + public static class NotificationActionInput { - public NotificationActionInput(@Nullable List choices, - Boolean allowFreeFormInput, - String label, @Nullable List allowedMimeTypes) { - this.choices = choices; - this.allowFreeFormInput = allowFreeFormInput; - this.label = label; - this.allowedMimeTypes = allowedMimeTypes; - } + public NotificationActionInput( + @Nullable List choices, + Boolean allowFreeFormInput, + String label, + @Nullable List allowedMimeTypes) { + this.choices = choices; + this.allowFreeFormInput = allowFreeFormInput; + this.label = label; + this.allowedMimeTypes = allowedMimeTypes; + } - @Nullable public List choices; - public Boolean allowFreeFormInput; - public String label; - @Nullable public List allowedMimeTypes; - } + @Nullable public List choices; + public Boolean allowFreeFormInput; + public String label; + @Nullable public List allowedMimeTypes; + } - private static final String ID = "id"; - private static final String TITLE = "title"; - private static final String ICON = "icon"; - private static final String ICON_SOURCE = "iconBitmapSource"; + private static final String ID = "id"; + private static final String TITLE = "title"; + private static final String ICON = "icon"; + private static final String ICON_SOURCE = "iconBitmapSource"; - public String id; - public String title; - public String icon; - public Boolean contextual; - public Boolean showsUserInterface; - public Boolean allowGeneratedReplies; - public IconSource iconSource; - public List inputs; + public String id; + public String title; + public String icon; + public Boolean contextual; + public Boolean showsUserInterface; + public Boolean allowGeneratedReplies; + public IconSource iconSource; + public List inputs; - public static NotificationAction from(Map arguments) { - NotificationAction action = new NotificationAction(); - action.id = (String) arguments.get(ID); - action.title = (String) arguments.get(TITLE); - action.icon = (String) arguments.get(ICON); - action.contextual = (Boolean) arguments.get("contextual"); - action.showsUserInterface = (Boolean) arguments.get("showsUserInterface"); - action.allowGeneratedReplies = (Boolean) arguments.get("allowGeneratedReplies"); - Integer iconSourceIndex = (Integer) arguments.get(ICON_SOURCE); - if (iconSourceIndex != null) { - action.iconSource = IconSource.values()[iconSourceIndex]; - } - action.inputs = new ArrayList<>(); - if( arguments.get("inputs") != null) { - List> inputs = (List>) arguments.get("inputs"); - for (Map input : inputs) { - action.inputs.add(new NotificationActionInput( - (List) input.get("choices"), - (Boolean) input.get("allowFreeFormInput"), - (String) input.get("label"), - (List) input.get("allowedMimeTypes") - )); - } - } + public static NotificationAction from(Map arguments) { + NotificationAction action = new NotificationAction(); + action.id = (String) arguments.get(ID); + action.title = (String) arguments.get(TITLE); + action.icon = (String) arguments.get(ICON); + action.contextual = (Boolean) arguments.get("contextual"); + action.showsUserInterface = (Boolean) arguments.get("showsUserInterface"); + action.allowGeneratedReplies = (Boolean) arguments.get("allowGeneratedReplies"); + Integer iconSourceIndex = (Integer) arguments.get(ICON_SOURCE); + if (iconSourceIndex != null) { + action.iconSource = IconSource.values()[iconSourceIndex]; + } + action.inputs = new ArrayList<>(); + if (arguments.get("inputs") != null) { + List> inputs = (List>) arguments.get("inputs"); + for (Map input : inputs) { + action.inputs.add( + new NotificationActionInput( + (List) input.get("choices"), + (Boolean) input.get("allowFreeFormInput"), + (String) input.get("label"), + (List) input.get("allowedMimeTypes"))); + } + } - return action; - } -} \ No newline at end of file + return action; + } +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelAction.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelAction.java index 293957658..ceb49b925 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelAction.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelAction.java @@ -4,6 +4,6 @@ @Keep public enum NotificationChannelAction { - CreateIfNotExists, - Update + CreateIfNotExists, + Update } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelDetails.java index 3771b95f3..cd80a1cd1 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelDetails.java @@ -4,96 +4,96 @@ import androidx.annotation.Keep; -import com.dexterous.flutterlocalnotifications.SoundSource; - +import java.io.Serializable; import java.util.Map; @Keep -public class NotificationChannelDetails { - private static final String ID = "id"; - private static final String NAME = "name"; - private static final String DESCRIPTION = "description"; - private static final String GROUP_ID = "groupId"; - private static final String SHOW_BADGE = "showBadge"; - private static final String IMPORTANCE = "importance"; - private static final String PLAY_SOUND = "playSound"; - private static final String SOUND = "sound"; - private static final String SOUND_SOURCE = "soundSource"; - private static final String ENABLE_VIBRATION = "enableVibration"; - private static final String VIBRATION_PATTERN = "vibrationPattern"; - private static final String CHANNEL_ACTION = "channelAction"; - private static final String ENABLE_LIGHTS = "enableLights"; - private static final String LED_COLOR_ALPHA = "ledColorAlpha"; - private static final String LED_COLOR_RED = "ledColorRed"; - private static final String LED_COLOR_GREEN = "ledColorGreen"; - private static final String LED_COLOR_BLUE = "ledColorBlue"; - - public String id; - public String name; - public String description; - public String groupId; - public Boolean showBadge; - public Integer importance; - public Boolean playSound; - public String sound; - public SoundSource soundSource; - public Boolean enableVibration; - public long[] vibrationPattern; - public NotificationChannelAction channelAction; - public Boolean enableLights; - public Integer ledColor; - +public class NotificationChannelDetails implements Serializable { + private static final String ID = "id"; + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String GROUP_ID = "groupId"; + private static final String SHOW_BADGE = "showBadge"; + private static final String IMPORTANCE = "importance"; + private static final String PLAY_SOUND = "playSound"; + private static final String SOUND = "sound"; + private static final String SOUND_SOURCE = "soundSource"; + private static final String ENABLE_VIBRATION = "enableVibration"; + private static final String VIBRATION_PATTERN = "vibrationPattern"; + private static final String CHANNEL_ACTION = "channelAction"; + private static final String ENABLE_LIGHTS = "enableLights"; + private static final String LED_COLOR_ALPHA = "ledColorAlpha"; + private static final String LED_COLOR_RED = "ledColorRed"; + private static final String LED_COLOR_GREEN = "ledColorGreen"; + private static final String LED_COLOR_BLUE = "ledColorBlue"; - public static NotificationChannelDetails from(Map arguments) { - NotificationChannelDetails notificationChannel = new NotificationChannelDetails(); - notificationChannel.id = (String) arguments.get(ID); - notificationChannel.name = (String) arguments.get(NAME); - notificationChannel.description = (String) arguments.get(DESCRIPTION); - notificationChannel.groupId = (String) arguments.get(GROUP_ID); - notificationChannel.importance = (Integer) arguments.get(IMPORTANCE); - notificationChannel.showBadge = (Boolean) arguments.get(SHOW_BADGE); - notificationChannel.channelAction = NotificationChannelAction.values()[(Integer) arguments.get(CHANNEL_ACTION)]; - notificationChannel.enableVibration = (Boolean) arguments.get(ENABLE_VIBRATION); - notificationChannel.vibrationPattern = (long[]) arguments.get(VIBRATION_PATTERN); + public String id; + public String name; + public String description; + public String groupId; + public Boolean showBadge; + public Integer importance; + public Boolean playSound; + public String sound; + public SoundSource soundSource; + public Boolean enableVibration; + public long[] vibrationPattern; + public NotificationChannelAction channelAction; + public Boolean enableLights; + public Integer ledColor; - notificationChannel.playSound = (Boolean) arguments.get(PLAY_SOUND); - notificationChannel.sound = (String) arguments.get(SOUND); - Integer soundSourceIndex = (Integer) arguments.get(SOUND_SOURCE); - if (soundSourceIndex != null) { - notificationChannel.soundSource = SoundSource.values()[soundSourceIndex]; - } + public static NotificationChannelDetails from(Map arguments) { + NotificationChannelDetails notificationChannel = new NotificationChannelDetails(); + notificationChannel.id = (String) arguments.get(ID); + notificationChannel.name = (String) arguments.get(NAME); + notificationChannel.description = (String) arguments.get(DESCRIPTION); + notificationChannel.groupId = (String) arguments.get(GROUP_ID); + notificationChannel.importance = (Integer) arguments.get(IMPORTANCE); + notificationChannel.showBadge = (Boolean) arguments.get(SHOW_BADGE); + notificationChannel.channelAction = + NotificationChannelAction.values()[(Integer) arguments.get(CHANNEL_ACTION)]; + notificationChannel.enableVibration = (Boolean) arguments.get(ENABLE_VIBRATION); + notificationChannel.vibrationPattern = (long[]) arguments.get(VIBRATION_PATTERN); - Integer a = (Integer) arguments.get(LED_COLOR_ALPHA); - Integer r = (Integer) arguments.get(LED_COLOR_RED); - Integer g = (Integer) arguments.get(LED_COLOR_GREEN); - Integer b = (Integer) arguments.get(LED_COLOR_BLUE); - if (a != null && r != null && g != null && b != null) { - notificationChannel.ledColor = Color.argb(a, r, g, b); - } + notificationChannel.playSound = (Boolean) arguments.get(PLAY_SOUND); + notificationChannel.sound = (String) arguments.get(SOUND); + Integer soundSourceIndex = (Integer) arguments.get(SOUND_SOURCE); + if (soundSourceIndex != null) { + notificationChannel.soundSource = SoundSource.values()[soundSourceIndex]; + } - notificationChannel.enableLights = (Boolean) arguments.get(ENABLE_LIGHTS); - return notificationChannel; + Integer a = (Integer) arguments.get(LED_COLOR_ALPHA); + Integer r = (Integer) arguments.get(LED_COLOR_RED); + Integer g = (Integer) arguments.get(LED_COLOR_GREEN); + Integer b = (Integer) arguments.get(LED_COLOR_BLUE); + if (a != null && r != null && g != null && b != null) { + notificationChannel.ledColor = Color.argb(a, r, g, b); } - public static NotificationChannelDetails fromNotificationDetails(NotificationDetails notificationDetails) { - NotificationChannelDetails notificationChannel = new NotificationChannelDetails(); - notificationChannel.id = notificationDetails.channelId; - notificationChannel.name = notificationDetails.channelName; - notificationChannel.description = notificationDetails.channelDescription; - notificationChannel.importance = notificationDetails.importance; - notificationChannel.showBadge = notificationDetails.channelShowBadge; - if (notificationDetails.channelAction == null) { - notificationChannel.channelAction = NotificationChannelAction.CreateIfNotExists; - } else { - notificationChannel.channelAction = notificationDetails.channelAction; - } - notificationChannel.enableVibration = notificationDetails.enableVibration; - notificationChannel.vibrationPattern = notificationDetails.vibrationPattern; - notificationChannel.playSound = notificationDetails.playSound; - notificationChannel.sound = notificationDetails.sound; - notificationChannel.soundSource = notificationDetails.soundSource; - notificationChannel.ledColor = notificationDetails.ledColor; - notificationChannel.enableLights = notificationDetails.enableLights; - return notificationChannel; + notificationChannel.enableLights = (Boolean) arguments.get(ENABLE_LIGHTS); + return notificationChannel; + } + + public static NotificationChannelDetails fromNotificationDetails( + NotificationDetails notificationDetails) { + NotificationChannelDetails notificationChannel = new NotificationChannelDetails(); + notificationChannel.id = notificationDetails.channelId; + notificationChannel.name = notificationDetails.channelName; + notificationChannel.description = notificationDetails.channelDescription; + notificationChannel.importance = notificationDetails.importance; + notificationChannel.showBadge = notificationDetails.channelShowBadge; + if (notificationDetails.channelAction == null) { + notificationChannel.channelAction = NotificationChannelAction.CreateIfNotExists; + } else { + notificationChannel.channelAction = notificationDetails.channelAction; } + notificationChannel.enableVibration = notificationDetails.enableVibration; + notificationChannel.vibrationPattern = notificationDetails.vibrationPattern; + notificationChannel.playSound = notificationDetails.playSound; + notificationChannel.sound = notificationDetails.sound; + notificationChannel.soundSource = notificationDetails.soundSource; + notificationChannel.ledColor = notificationDetails.ledColor; + notificationChannel.enableLights = notificationDetails.enableLights; + return notificationChannel; + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelGroupDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelGroupDetails.java index b51d5e78f..67d767eff 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelGroupDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationChannelGroupDetails.java @@ -2,24 +2,25 @@ import androidx.annotation.Keep; +import java.io.Serializable; import java.util.Map; @Keep -public class NotificationChannelGroupDetails { - private static final String ID = "id"; - private static final String NAME = "name"; - private static final String DESCRIPTION = "description"; +public class NotificationChannelGroupDetails implements Serializable { + private static final String ID = "id"; + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + public String id; + public String name; + public String description; - public String id; - public String name; - public String description; - - public static NotificationChannelGroupDetails from(Map arguments) { - NotificationChannelGroupDetails notificationChannelGroupDetails = new NotificationChannelGroupDetails(); - notificationChannelGroupDetails.id = (String) arguments.get(ID); - notificationChannelGroupDetails.name = (String) arguments.get(NAME); - notificationChannelGroupDetails.description = (String) arguments.get(DESCRIPTION); - return notificationChannelGroupDetails; - } + public static NotificationChannelGroupDetails from(Map arguments) { + NotificationChannelGroupDetails notificationChannelGroupDetails = + new NotificationChannelGroupDetails(); + notificationChannelGroupDetails.id = (String) arguments.get(ID); + notificationChannelGroupDetails.name = (String) arguments.get(NAME); + notificationChannelGroupDetails.description = (String) arguments.get(DESCRIPTION); + return notificationChannelGroupDetails; + } } 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 80d253ab7..bea4e36fc 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 @@ -6,10 +6,8 @@ import androidx.annotation.Keep; -import com.dexterous.flutterlocalnotifications.BitmapSource; import com.dexterous.flutterlocalnotifications.NotificationStyle; import com.dexterous.flutterlocalnotifications.RepeatInterval; -import com.dexterous.flutterlocalnotifications.SoundSource; import com.dexterous.flutterlocalnotifications.models.styles.BigPictureStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.BigTextStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.DefaultStyleInformation; @@ -17,438 +15,530 @@ import com.dexterous.flutterlocalnotifications.models.styles.MessagingStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.StyleInformation; -import java.lang.reflect.Array; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; @Keep -public class NotificationDetails { - private static final String ID = "id"; - private static final String TITLE = "title"; - private static final String BODY = "body"; - private static final String PAYLOAD = "payload"; - private static final String MILLISECONDS_SINCE_EPOCH = "millisecondsSinceEpoch"; - private static final String CALLED_AT = "calledAt"; - private static final String REPEAT_INTERVAL = "repeatInterval"; - private static final String REPEAT_TIME = "repeatTime"; - private static final String PLATFORM_SPECIFICS = "platformSpecifics"; - private static final String AUTO_CANCEL = "autoCancel"; - private static final String ONGOING = "ongoing"; - private static final String STYLE = "style"; - private static final String ICON = "icon"; - private static final String PRIORITY = "priority"; - private static final String PLAY_SOUND = "playSound"; - private static final String SOUND = "sound"; - private static final String SOUND_SOURCE = "soundSource"; - private static final String ENABLE_VIBRATION = "enableVibration"; - private static final String VIBRATION_PATTERN = "vibrationPattern"; - private static final String GROUP_KEY = "groupKey"; - private static final String SET_AS_GROUP_SUMMARY = "setAsGroupSummary"; - private static final String GROUP_ALERT_BEHAVIOR = "groupAlertBehavior"; - private static final String ONLY_ALERT_ONCE = "onlyAlertOnce"; - private static final String CHANNEL_ID = "channelId"; - private static final String CHANNEL_NAME = "channelName"; - private static final String CHANNEL_DESCRIPTION = "channelDescription"; - private static final String CHANNEL_SHOW_BADGE = "channelShowBadge"; - private static final String IMPORTANCE = "importance"; - private static final String STYLE_INFORMATION = "styleInformation"; - private static final String BIG_TEXT = "bigText"; - private static final String HTML_FORMAT_BIG_TEXT = "htmlFormatBigText"; - private static final String CONTENT_TITLE = "contentTitle"; - private static final String HTML_FORMAT_CONTENT_TITLE = "htmlFormatContentTitle"; - private static final String SUMMARY_TEXT = "summaryText"; - private static final String HTML_FORMAT_SUMMARY_TEXT = "htmlFormatSummaryText"; - private static final String LINES = "lines"; - private static final String HTML_FORMAT_LINES = "htmlFormatLines"; - private static final String HTML_FORMAT_TITLE = "htmlFormatTitle"; - private static final String HTML_FORMAT_CONTENT = "htmlFormatContent"; - private static final String DAY = "day"; - private static final String COLOR_ALPHA = "colorAlpha"; - private static final String COLOR_RED = "colorRed"; - private static final String COLOR_GREEN = "colorGreen"; - private static final String COLOR_BLUE = "colorBlue"; - private static final String LARGE_ICON = "largeIcon"; - private static final String LARGE_ICON_BITMAP_SOURCE = "largeIconBitmapSource"; - private static final String BIG_PICTURE = "bigPicture"; - private static final String BIG_PICTURE_BITMAP_SOURCE = "bigPictureBitmapSource"; - private static final String HIDE_EXPANDED_LARGE_ICON = "hideExpandedLargeIcon"; - private static final String SHOW_PROGRESS = "showProgress"; - private static final String MAX_PROGRESS = "maxProgress"; - private static final String PROGRESS = "progress"; - private static final String INDETERMINATE = "indeterminate"; - private static final String PERSON = "person"; - private static final String CONVERSATION_TITLE = "conversationTitle"; - private static final String GROUP_CONVERSATION = "groupConversation"; - private static final String MESSAGES = "messages"; - private static final String TEXT = "text"; - private static final String TIMESTAMP = "timestamp"; - private static final String BOT = "bot"; - private static final String ICON_SOURCE = "iconSource"; - private static final String IMPORTANT = "important"; - private static final String KEY = "key"; - private static final String NAME = "name"; - private static final String URI = "uri"; - private static final String DATA_MIME_TYPE = "dataMimeType"; - private static final String DATA_URI = "dataUri"; - private static final String CHANNEL_ACTION = "channelAction"; - private static final String ENABLE_LIGHTS = "enableLights"; - private static final String LED_COLOR_ALPHA = "ledColorAlpha"; - private static final String LED_COLOR_RED = "ledColorRed"; - private static final String LED_COLOR_GREEN = "ledColorGreen"; - private static final String LED_COLOR_BLUE = "ledColorBlue"; - - private static final String LED_ON_MS = "ledOnMs"; - private static final String LED_OFF_MS = "ledOffMs"; - private static final String VISIBILITY = "visibility"; - - private static final String TICKER = "ticker"; - private static final String ALLOW_WHILE_IDLE = "allowWhileIdle"; - private static final String CATEGORY = "category"; - private static final String TIMEOUT_AFTER = "timeoutAfter"; - private static final String SHOW_WHEN = "showWhen"; - private static final String WHEN = "when"; - private static final String USES_CHRONOMETER = "usesChronometer"; - 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 MATCH_DATE_TIME_COMPONENTS = "matchDateTimeComponents"; - - private static final String FULL_SCREEN_INTENT = "fullScreenIntent"; - private static final String SHORTCUT_ID = "shortcutId"; - - private static final String ACTIONS = "actions"; - - public Integer id; - public String title; - public String body; - public String icon; - public String channelId = "Default_Channel_Id"; - public String channelName; - public String channelDescription; - public Boolean channelShowBadge; - public Integer importance; - public Integer priority; - public Boolean playSound; - public String sound; - public SoundSource soundSource; - public Boolean enableVibration; - public long[] vibrationPattern; - public NotificationStyle style; - public StyleInformation styleInformation; - public RepeatInterval repeatInterval; - public Time repeatTime; - public Long millisecondsSinceEpoch; - public Long calledAt; - public String payload; - public String groupKey; - public Boolean setAsGroupSummary; - public Integer groupAlertBehavior; - public Boolean autoCancel; - public Boolean ongoing; - public Integer day; - public Integer color; - public String largeIcon; - public BitmapSource largeIconBitmapSource; - public Boolean onlyAlertOnce; - public Boolean showProgress; - public Integer maxProgress; - public Integer progress; - public Boolean indeterminate; - public NotificationChannelAction channelAction; - public Boolean enableLights; - public Integer ledColor; - public Integer ledOnMs; - public Integer ledOffMs; - public String ticker; - public Integer visibility; - public Boolean allowWhileIdle; - public Long timeoutAfter; - public String category; - public int[] additionalFlags; - public Boolean showWhen; - public Boolean usesChronometer; - public String scheduledDateTime; - public String timeZoneName; - public ScheduledNotificationRepeatFrequency scheduledNotificationRepeatFrequency; - public DateTimeComponents matchDateTimeComponents; - public Long when; - public Boolean fullScreenIntent; - public String shortcutId; - public List actions; - - - // Note: this is set on the Android to save details about the icon that should be used when re-hydrating scheduled notifications when a device has been restarted. - public Integer iconResourceId; - - public static NotificationDetails from(Map arguments) { - NotificationDetails notificationDetails = new NotificationDetails(); - notificationDetails.payload = (String) arguments.get(PAYLOAD); - 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(MATCH_DATE_TIME_COMPONENTS)) { - notificationDetails.matchDateTimeComponents = DateTimeComponents.values()[(Integer) arguments.get(MATCH_DATE_TIME_COMPONENTS)]; - } - if (arguments.containsKey(MILLISECONDS_SINCE_EPOCH)) { - notificationDetails.millisecondsSinceEpoch = (Long) arguments.get(MILLISECONDS_SINCE_EPOCH); - } - if (arguments.containsKey(CALLED_AT)) { - notificationDetails.calledAt = (Long) arguments.get(CALLED_AT); - } - if (arguments.containsKey(REPEAT_INTERVAL)) { - notificationDetails.repeatInterval = RepeatInterval.values()[(Integer) arguments.get(REPEAT_INTERVAL)]; - } - if (arguments.containsKey(REPEAT_TIME)) { - @SuppressWarnings("unchecked") - Map repeatTimeParams = (Map) arguments.get(REPEAT_TIME); - notificationDetails.repeatTime = Time.from(repeatTimeParams); - } - if (arguments.containsKey(DAY)) { - notificationDetails.day = (Integer) arguments.get(DAY); - } - - readPlatformSpecifics(arguments, notificationDetails); - return notificationDetails; +public class NotificationDetails implements Serializable { + private static final String ID = "id"; + private static final String TITLE = "title"; + private static final String BODY = "body"; + private static final String PAYLOAD = "payload"; + private static final String MILLISECONDS_SINCE_EPOCH = "millisecondsSinceEpoch"; + private static final String CALLED_AT = "calledAt"; + private static final String REPEAT_INTERVAL = "repeatInterval"; + private static final String REPEAT_TIME = "repeatTime"; + private static final String PLATFORM_SPECIFICS = "platformSpecifics"; + private static final String AUTO_CANCEL = "autoCancel"; + private static final String ONGOING = "ongoing"; + private static final String STYLE = "style"; + private static final String ICON = "icon"; + private static final String PRIORITY = "priority"; + private static final String PLAY_SOUND = "playSound"; + private static final String SOUND = "sound"; + private static final String SOUND_SOURCE = "soundSource"; + private static final String ENABLE_VIBRATION = "enableVibration"; + private static final String VIBRATION_PATTERN = "vibrationPattern"; + private static final String TAG = "tag"; + private static final String GROUP_KEY = "groupKey"; + private static final String SET_AS_GROUP_SUMMARY = "setAsGroupSummary"; + private static final String GROUP_ALERT_BEHAVIOR = "groupAlertBehavior"; + private static final String ONLY_ALERT_ONCE = "onlyAlertOnce"; + private static final String CHANNEL_ID = "channelId"; + private static final String CHANNEL_NAME = "channelName"; + private static final String CHANNEL_DESCRIPTION = "channelDescription"; + private static final String CHANNEL_SHOW_BADGE = "channelShowBadge"; + private static final String IMPORTANCE = "importance"; + private static final String STYLE_INFORMATION = "styleInformation"; + private static final String BIG_TEXT = "bigText"; + private static final String HTML_FORMAT_BIG_TEXT = "htmlFormatBigText"; + private static final String CONTENT_TITLE = "contentTitle"; + private static final String HTML_FORMAT_CONTENT_TITLE = "htmlFormatContentTitle"; + private static final String SUMMARY_TEXT = "summaryText"; + private static final String HTML_FORMAT_SUMMARY_TEXT = "htmlFormatSummaryText"; + private static final String LINES = "lines"; + private static final String HTML_FORMAT_LINES = "htmlFormatLines"; + private static final String HTML_FORMAT_TITLE = "htmlFormatTitle"; + private static final String HTML_FORMAT_CONTENT = "htmlFormatContent"; + private static final String DAY = "day"; + private static final String COLOR_ALPHA = "colorAlpha"; + private static final String COLOR_RED = "colorRed"; + private static final String COLOR_GREEN = "colorGreen"; + private static final String COLOR_BLUE = "colorBlue"; + private static final String LARGE_ICON = "largeIcon"; + private static final String LARGE_ICON_BITMAP_SOURCE = "largeIconBitmapSource"; + private static final String BIG_PICTURE = "bigPicture"; + private static final String BIG_PICTURE_BITMAP_SOURCE = "bigPictureBitmapSource"; + private static final String HIDE_EXPANDED_LARGE_ICON = "hideExpandedLargeIcon"; + private static final String SHOW_PROGRESS = "showProgress"; + private static final String MAX_PROGRESS = "maxProgress"; + private static final String PROGRESS = "progress"; + private static final String INDETERMINATE = "indeterminate"; + private static final String PERSON = "person"; + private static final String CONVERSATION_TITLE = "conversationTitle"; + private static final String GROUP_CONVERSATION = "groupConversation"; + private static final String MESSAGES = "messages"; + private static final String TEXT = "text"; + private static final String TIMESTAMP = "timestamp"; + private static final String BOT = "bot"; + private static final String ICON_SOURCE = "iconSource"; + private static final String IMPORTANT = "important"; + private static final String KEY = "key"; + private static final String NAME = "name"; + private static final String URI = "uri"; + private static final String DATA_MIME_TYPE = "dataMimeType"; + private static final String DATA_URI = "dataUri"; + private static final String CHANNEL_ACTION = "channelAction"; + private static final String ENABLE_LIGHTS = "enableLights"; + private static final String LED_COLOR_ALPHA = "ledColorAlpha"; + private static final String LED_COLOR_RED = "ledColorRed"; + private static final String LED_COLOR_GREEN = "ledColorGreen"; + private static final String LED_COLOR_BLUE = "ledColorBlue"; + + private static final String LED_ON_MS = "ledOnMs"; + private static final String LED_OFF_MS = "ledOffMs"; + private static final String VISIBILITY = "visibility"; + + private static final String TICKER = "ticker"; + private static final String ALLOW_WHILE_IDLE = "allowWhileIdle"; + private static final String CATEGORY = "category"; + private static final String TIMEOUT_AFTER = "timeoutAfter"; + private static final String SHOW_WHEN = "showWhen"; + private static final String WHEN = "when"; + private static final String USES_CHRONOMETER = "usesChronometer"; + 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 MATCH_DATE_TIME_COMPONENTS = "matchDateTimeComponents"; + + private static final String FULL_SCREEN_INTENT = "fullScreenIntent"; + private static final String SHORTCUT_ID = "shortcutId"; + private static final String SUB_TEXT = "subText"; + private static final String ACTIONS = "actions"; + + public Integer id; + public String title; + public String body; + public String icon; + public String channelId = "Default_Channel_Id"; + public String channelName; + public String channelDescription; + public Boolean channelShowBadge; + public Integer importance; + public Integer priority; + public Boolean playSound; + public String sound; + public SoundSource soundSource; + public Boolean enableVibration; + public long[] vibrationPattern; + public NotificationStyle style; + public StyleInformation styleInformation; + public RepeatInterval repeatInterval; + public Time repeatTime; + public Long millisecondsSinceEpoch; + public Long calledAt; + public String payload; + public String groupKey; + public Boolean setAsGroupSummary; + public Integer groupAlertBehavior; + public Boolean autoCancel; + public Boolean ongoing; + public Integer day; + public Integer color; + public Object largeIcon; + public BitmapSource largeIconBitmapSource; + public Boolean onlyAlertOnce; + public Boolean showProgress; + public Integer maxProgress; + public Integer progress; + public Boolean indeterminate; + public NotificationChannelAction channelAction; + public Boolean enableLights; + public Integer ledColor; + public Integer ledOnMs; + public Integer ledOffMs; + public String ticker; + public Integer visibility; + public Boolean allowWhileIdle; + public Long timeoutAfter; + public String category; + public int[] additionalFlags; + public Boolean showWhen; + public Boolean usesChronometer; + public String scheduledDateTime; + public String timeZoneName; + public ScheduledNotificationRepeatFrequency scheduledNotificationRepeatFrequency; + public DateTimeComponents matchDateTimeComponents; + public Long when; + public Boolean fullScreenIntent; + public String shortcutId; + public String subText; + public List actions; + public String tag; + + // Note: this is set on the Android to save details about the icon that should be used when + // re-hydrating scheduled notifications when a device has been restarted. + public Integer iconResourceId; + + public static NotificationDetails from(Map arguments) { + NotificationDetails notificationDetails = new NotificationDetails(); + notificationDetails.payload = (String) arguments.get(PAYLOAD); + 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)]; } - - private static void readPlatformSpecifics(Map arguments, NotificationDetails notificationDetails) { - @SuppressWarnings("unchecked") - Map platformChannelSpecifics = (Map) arguments.get(PLATFORM_SPECIFICS); - if (platformChannelSpecifics != null) { - notificationDetails.autoCancel = (Boolean) platformChannelSpecifics.get(AUTO_CANCEL); - notificationDetails.ongoing = (Boolean) platformChannelSpecifics.get(ONGOING); - notificationDetails.style = NotificationStyle.values()[(Integer) platformChannelSpecifics.get(STYLE)]; - readStyleInformation(notificationDetails, platformChannelSpecifics); - notificationDetails.icon = (String) platformChannelSpecifics.get(ICON); - notificationDetails.priority = (Integer) platformChannelSpecifics.get(PRIORITY); - readSoundInformation(notificationDetails, platformChannelSpecifics); - notificationDetails.enableVibration = (Boolean) platformChannelSpecifics.get(ENABLE_VIBRATION); - notificationDetails.vibrationPattern = (long[]) platformChannelSpecifics.get(VIBRATION_PATTERN); - readGroupingInformation(notificationDetails, platformChannelSpecifics); - notificationDetails.onlyAlertOnce = (Boolean) platformChannelSpecifics.get(ONLY_ALERT_ONCE); - notificationDetails.showWhen = (Boolean) platformChannelSpecifics.get(SHOW_WHEN); - notificationDetails.when = parseLong(platformChannelSpecifics.get(WHEN)); - notificationDetails.usesChronometer = (Boolean) platformChannelSpecifics.get(USES_CHRONOMETER); - readProgressInformation(notificationDetails, platformChannelSpecifics); - readColor(notificationDetails, platformChannelSpecifics); - readChannelInformation(notificationDetails, platformChannelSpecifics); - readLedInformation(notificationDetails, platformChannelSpecifics); - readLargeIconInformation(notificationDetails, platformChannelSpecifics); - notificationDetails.ticker = (String) platformChannelSpecifics.get(TICKER); - notificationDetails.visibility = (Integer) platformChannelSpecifics.get(VISIBILITY); - 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); - - if(platformChannelSpecifics.containsKey(ACTIONS)) { - List> inputActions = (List>) platformChannelSpecifics.get(ACTIONS); - if(!inputActions.isEmpty()) { - for (Map input : inputActions) { - final NotificationAction action = NotificationAction.from(input); - if(action != null) { - if(notificationDetails.actions == null) { - notificationDetails.actions = new ArrayList<>(); - } - notificationDetails.actions.add(action); - } - } - } - } - - } + if (arguments.containsKey(MATCH_DATE_TIME_COMPONENTS)) { + notificationDetails.matchDateTimeComponents = + DateTimeComponents.values()[(Integer) arguments.get(MATCH_DATE_TIME_COMPONENTS)]; } - - private static Long parseLong(Object object) { - if (object instanceof Integer) { - return ((Integer) object).longValue(); - } - if (object instanceof Long) { - return (Long) object; - } - return null; + if (arguments.containsKey(MILLISECONDS_SINCE_EPOCH)) { + notificationDetails.millisecondsSinceEpoch = (Long) arguments.get(MILLISECONDS_SINCE_EPOCH); } - - private static void readProgressInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - notificationDetails.showProgress = (Boolean) platformChannelSpecifics.get(SHOW_PROGRESS); - if (platformChannelSpecifics.containsKey(MAX_PROGRESS)) { - notificationDetails.maxProgress = (Integer) platformChannelSpecifics.get(MAX_PROGRESS); - } - - if (platformChannelSpecifics.containsKey(PROGRESS)) { - notificationDetails.progress = (Integer) platformChannelSpecifics.get(PROGRESS); - } - - if (platformChannelSpecifics.containsKey(INDETERMINATE)) { - notificationDetails.indeterminate = (Boolean) platformChannelSpecifics.get(INDETERMINATE); - } + if (arguments.containsKey(CALLED_AT)) { + notificationDetails.calledAt = (Long) arguments.get(CALLED_AT); + } + if (arguments.containsKey(REPEAT_INTERVAL)) { + notificationDetails.repeatInterval = + RepeatInterval.values()[(Integer) arguments.get(REPEAT_INTERVAL)]; + } + if (arguments.containsKey(REPEAT_TIME)) { + @SuppressWarnings("unchecked") + Map repeatTimeParams = (Map) arguments.get(REPEAT_TIME); + notificationDetails.repeatTime = Time.from(repeatTimeParams); + } + if (arguments.containsKey(DAY)) { + notificationDetails.day = (Integer) arguments.get(DAY); } - private static void readLargeIconInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - notificationDetails.largeIcon = (String) platformChannelSpecifics.get(LARGE_ICON); - if (platformChannelSpecifics.containsKey(LARGE_ICON_BITMAP_SOURCE)) { - Integer argumentValue = (Integer) platformChannelSpecifics.get(LARGE_ICON_BITMAP_SOURCE); - if (argumentValue != null) { - notificationDetails.largeIconBitmapSource = BitmapSource.values()[argumentValue]; + readPlatformSpecifics(arguments, notificationDetails); + return notificationDetails; + } + + private static void readPlatformSpecifics( + Map arguments, NotificationDetails notificationDetails) { + @SuppressWarnings("unchecked") + Map platformChannelSpecifics = + (Map) arguments.get(PLATFORM_SPECIFICS); + if (platformChannelSpecifics != null) { + notificationDetails.autoCancel = (Boolean) platformChannelSpecifics.get(AUTO_CANCEL); + notificationDetails.ongoing = (Boolean) platformChannelSpecifics.get(ONGOING); + notificationDetails.style = + NotificationStyle.values()[(Integer) platformChannelSpecifics.get(STYLE)]; + readStyleInformation(notificationDetails, platformChannelSpecifics); + notificationDetails.icon = (String) platformChannelSpecifics.get(ICON); + notificationDetails.priority = (Integer) platformChannelSpecifics.get(PRIORITY); + readSoundInformation(notificationDetails, platformChannelSpecifics); + notificationDetails.enableVibration = + (Boolean) platformChannelSpecifics.get(ENABLE_VIBRATION); + notificationDetails.vibrationPattern = + (long[]) platformChannelSpecifics.get(VIBRATION_PATTERN); + readGroupingInformation(notificationDetails, platformChannelSpecifics); + notificationDetails.onlyAlertOnce = (Boolean) platformChannelSpecifics.get(ONLY_ALERT_ONCE); + notificationDetails.showWhen = (Boolean) platformChannelSpecifics.get(SHOW_WHEN); + notificationDetails.when = parseLong(platformChannelSpecifics.get(WHEN)); + notificationDetails.usesChronometer = + (Boolean) platformChannelSpecifics.get(USES_CHRONOMETER); + readProgressInformation(notificationDetails, platformChannelSpecifics); + readColor(notificationDetails, platformChannelSpecifics); + readChannelInformation(notificationDetails, platformChannelSpecifics); + readLedInformation(notificationDetails, platformChannelSpecifics); + readLargeIconInformation(notificationDetails, platformChannelSpecifics); + notificationDetails.ticker = (String) platformChannelSpecifics.get(TICKER); + notificationDetails.visibility = (Integer) platformChannelSpecifics.get(VISIBILITY); + 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); + notificationDetails.subText = (String) platformChannelSpecifics.get(SUB_TEXT); + notificationDetails.tag = (String) platformChannelSpecifics.get(TAG); + + if (platformChannelSpecifics.containsKey(ACTIONS)) { + List> inputActions = + (List>) platformChannelSpecifics.get(ACTIONS); + if (!inputActions.isEmpty()) { + for (Map input : inputActions) { + final NotificationAction action = NotificationAction.from(input); + if (action != null) { + if (notificationDetails.actions == null) { + notificationDetails.actions = new ArrayList<>(); + } + notificationDetails.actions.add(action); } + } } + } } + } - private static void readGroupingInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - notificationDetails.groupKey = (String) platformChannelSpecifics.get(GROUP_KEY); - notificationDetails.setAsGroupSummary = (Boolean) platformChannelSpecifics.get(SET_AS_GROUP_SUMMARY); - notificationDetails.groupAlertBehavior = (Integer) platformChannelSpecifics.get(GROUP_ALERT_BEHAVIOR); + private static Long parseLong(Object object) { + if (object instanceof Integer) { + return ((Integer) object).longValue(); } - - private static void readSoundInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - notificationDetails.playSound = (Boolean) platformChannelSpecifics.get(PLAY_SOUND); - notificationDetails.sound = (String) platformChannelSpecifics.get(SOUND); - Integer soundSourceIndex = (Integer)platformChannelSpecifics.get(SOUND_SOURCE); - if(soundSourceIndex != null) { - notificationDetails.soundSource = SoundSource.values()[soundSourceIndex]; - } + if (object instanceof Long) { + return (Long) object; } - - private static void readColor(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - Integer a = (Integer) platformChannelSpecifics.get(COLOR_ALPHA); - Integer r = (Integer) platformChannelSpecifics.get(COLOR_RED); - Integer g = (Integer) platformChannelSpecifics.get(COLOR_GREEN); - Integer b = (Integer) platformChannelSpecifics.get(COLOR_BLUE); - if (a != null && r != null && g != null && b != null) { - notificationDetails.color = Color.argb(a, r, g, b); - } + return null; + } + + private static void readProgressInformation( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + notificationDetails.showProgress = (Boolean) platformChannelSpecifics.get(SHOW_PROGRESS); + if (platformChannelSpecifics.containsKey(MAX_PROGRESS)) { + notificationDetails.maxProgress = (Integer) platformChannelSpecifics.get(MAX_PROGRESS); } - private static void readLedInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - Integer a = (Integer) platformChannelSpecifics.get(LED_COLOR_ALPHA); - Integer r = (Integer) platformChannelSpecifics.get(LED_COLOR_RED); - Integer g = (Integer) platformChannelSpecifics.get(LED_COLOR_GREEN); - Integer b = (Integer) platformChannelSpecifics.get(LED_COLOR_BLUE); - if (a != null && r != null && g != null && b != null) { - notificationDetails.ledColor = Color.argb(a, r, g, b); - } - notificationDetails.enableLights = (Boolean) platformChannelSpecifics.get(ENABLE_LIGHTS); - notificationDetails.ledOnMs = (Integer) platformChannelSpecifics.get(LED_ON_MS); - notificationDetails.ledOffMs = (Integer) platformChannelSpecifics.get(LED_OFF_MS); + if (platformChannelSpecifics.containsKey(PROGRESS)) { + notificationDetails.progress = (Integer) platformChannelSpecifics.get(PROGRESS); } - private static void readChannelInformation(NotificationDetails notificationDetails, Map platformChannelSpecifics) { - 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); - notificationDetails.importance = (Integer) platformChannelSpecifics.get(IMPORTANCE); - notificationDetails.channelShowBadge = (Boolean) platformChannelSpecifics.get(CHANNEL_SHOW_BADGE); - notificationDetails.channelAction = NotificationChannelAction.values()[(Integer) platformChannelSpecifics.get(CHANNEL_ACTION)]; - } + if (platformChannelSpecifics.containsKey(INDETERMINATE)) { + notificationDetails.indeterminate = (Boolean) platformChannelSpecifics.get(INDETERMINATE); } - - @SuppressWarnings("unchecked") - private static void readStyleInformation(NotificationDetails notificationDetails, Map platformSpecifics) { - Map styleInformation = (Map) platformSpecifics.get(STYLE_INFORMATION); - DefaultStyleInformation defaultStyleInformation = getDefaultStyleInformation(styleInformation); - if (notificationDetails.style == NotificationStyle.Default) { - notificationDetails.styleInformation = defaultStyleInformation; - } else if (notificationDetails.style == NotificationStyle.BigPicture) { - readBigPictureStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); - } else if (notificationDetails.style == NotificationStyle.BigText) { - readBigTextStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); - } else if (notificationDetails.style == NotificationStyle.Inbox) { - readInboxStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); - } else if (notificationDetails.style == NotificationStyle.Messaging) { - readMessagingStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); - } else if (notificationDetails.style == NotificationStyle.Media) { - notificationDetails.styleInformation = defaultStyleInformation; - } + } + + private static void readLargeIconInformation( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + notificationDetails.largeIcon = platformChannelSpecifics.get(LARGE_ICON); + if (platformChannelSpecifics.containsKey(LARGE_ICON_BITMAP_SOURCE)) { + Integer argumentValue = (Integer) platformChannelSpecifics.get(LARGE_ICON_BITMAP_SOURCE); + if (argumentValue != null) { + notificationDetails.largeIconBitmapSource = BitmapSource.values()[argumentValue]; + } } - - @SuppressWarnings("unchecked") - private static void readMessagingStyleInformation(NotificationDetails notificationDetails, Map styleInformation, DefaultStyleInformation defaultStyleInformation) { - String conversationTitle = (String) styleInformation.get(CONVERSATION_TITLE); - Boolean groupConversation = (Boolean) styleInformation.get(GROUP_CONVERSATION); - PersonDetails person = readPersonDetails((Map) styleInformation.get(PERSON)); - ArrayList messages = readMessages((ArrayList>) styleInformation.get(MESSAGES)); - notificationDetails.styleInformation = new MessagingStyleInformation(person, conversationTitle, groupConversation, messages, defaultStyleInformation.htmlFormatTitle, defaultStyleInformation.htmlFormatBody); + } + + private static void readGroupingInformation( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + notificationDetails.groupKey = (String) platformChannelSpecifics.get(GROUP_KEY); + notificationDetails.setAsGroupSummary = + (Boolean) platformChannelSpecifics.get(SET_AS_GROUP_SUMMARY); + notificationDetails.groupAlertBehavior = + (Integer) platformChannelSpecifics.get(GROUP_ALERT_BEHAVIOR); + } + + private static void readSoundInformation( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + notificationDetails.playSound = (Boolean) platformChannelSpecifics.get(PLAY_SOUND); + notificationDetails.sound = (String) platformChannelSpecifics.get(SOUND); + Integer soundSourceIndex = (Integer) platformChannelSpecifics.get(SOUND_SOURCE); + if (soundSourceIndex != null) { + notificationDetails.soundSource = SoundSource.values()[soundSourceIndex]; } - - private static PersonDetails readPersonDetails(Map person) { - if (person == null) { - return null; - } - Boolean bot = (Boolean) person.get(BOT); - String icon = (String) person.get(ICON); - Integer iconSourceIndex = (Integer) person.get(ICON_SOURCE); - IconSource iconSource = iconSourceIndex == null ? null : IconSource.values()[iconSourceIndex]; - Boolean important = (Boolean) person.get(IMPORTANT); - String key = (String) person.get(KEY); - String name = (String) person.get(NAME); - String uri = (String) person.get(URI); - return new PersonDetails(bot, icon, iconSource, important, key, name, uri); + } + + private static void readColor( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + Integer a = (Integer) platformChannelSpecifics.get(COLOR_ALPHA); + Integer r = (Integer) platformChannelSpecifics.get(COLOR_RED); + Integer g = (Integer) platformChannelSpecifics.get(COLOR_GREEN); + Integer b = (Integer) platformChannelSpecifics.get(COLOR_BLUE); + if (a != null && r != null && g != null && b != null) { + notificationDetails.color = Color.argb(a, r, g, b); } - - @SuppressWarnings("unchecked") - private static ArrayList readMessages(ArrayList> messages) { - ArrayList result = new ArrayList<>(); - if (messages != null) { - for (Map messageData : messages) { - result.add(new MessageDetails((String) messageData.get(TEXT), (Long) messageData.get(TIMESTAMP), readPersonDetails((Map) messageData.get(PERSON)), (String) messageData.get(DATA_MIME_TYPE), (String) messageData.get(DATA_URI))); - } - } - return result; + } + + private static void readLedInformation( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + Integer a = (Integer) platformChannelSpecifics.get(LED_COLOR_ALPHA); + Integer r = (Integer) platformChannelSpecifics.get(LED_COLOR_RED); + Integer g = (Integer) platformChannelSpecifics.get(LED_COLOR_GREEN); + Integer b = (Integer) platformChannelSpecifics.get(LED_COLOR_BLUE); + if (a != null && r != null && g != null && b != null) { + notificationDetails.ledColor = Color.argb(a, r, g, b); } - - private static void readInboxStyleInformation(NotificationDetails notificationDetails, Map styleInformation, DefaultStyleInformation defaultStyleInformation) { - String contentTitle = (String) styleInformation.get(CONTENT_TITLE); - Boolean htmlFormatContentTitle = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT_TITLE); - String summaryText = (String) styleInformation.get(SUMMARY_TEXT); - Boolean htmlFormatSummaryText = (Boolean) styleInformation.get(HTML_FORMAT_SUMMARY_TEXT); - @SuppressWarnings("unchecked") - ArrayList lines = (ArrayList) styleInformation.get(LINES); - Boolean htmlFormatLines = (Boolean) styleInformation.get(HTML_FORMAT_LINES); - notificationDetails.styleInformation = new InboxStyleInformation(defaultStyleInformation.htmlFormatTitle, defaultStyleInformation.htmlFormatBody, contentTitle, htmlFormatContentTitle, summaryText, htmlFormatSummaryText, lines, htmlFormatLines); + notificationDetails.enableLights = (Boolean) platformChannelSpecifics.get(ENABLE_LIGHTS); + notificationDetails.ledOnMs = (Integer) platformChannelSpecifics.get(LED_ON_MS); + notificationDetails.ledOffMs = (Integer) platformChannelSpecifics.get(LED_OFF_MS); + } + + private static void readChannelInformation( + NotificationDetails notificationDetails, Map platformChannelSpecifics) { + 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); + notificationDetails.importance = (Integer) platformChannelSpecifics.get(IMPORTANCE); + notificationDetails.channelShowBadge = + (Boolean) platformChannelSpecifics.get(CHANNEL_SHOW_BADGE); + notificationDetails.channelAction = + NotificationChannelAction.values()[ + (Integer) platformChannelSpecifics.get(CHANNEL_ACTION)]; } - - private static void readBigTextStyleInformation(NotificationDetails notificationDetails, Map styleInformation, DefaultStyleInformation defaultStyleInformation) { - String bigText = (String) styleInformation.get(BIG_TEXT); - Boolean htmlFormatBigText = (Boolean) styleInformation.get(HTML_FORMAT_BIG_TEXT); - String contentTitle = (String) styleInformation.get(CONTENT_TITLE); - Boolean htmlFormatContentTitle = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT_TITLE); - String summaryText = (String) styleInformation.get(SUMMARY_TEXT); - Boolean htmlFormatSummaryText = (Boolean) styleInformation.get(HTML_FORMAT_SUMMARY_TEXT); - notificationDetails.styleInformation = new BigTextStyleInformation(defaultStyleInformation.htmlFormatTitle, defaultStyleInformation.htmlFormatBody, bigText, htmlFormatBigText, contentTitle, htmlFormatContentTitle, summaryText, htmlFormatSummaryText); + } + + @SuppressWarnings("unchecked") + private static void readStyleInformation( + NotificationDetails notificationDetails, Map platformSpecifics) { + Map styleInformation = + (Map) platformSpecifics.get(STYLE_INFORMATION); + DefaultStyleInformation defaultStyleInformation = getDefaultStyleInformation(styleInformation); + if (notificationDetails.style == NotificationStyle.Default) { + notificationDetails.styleInformation = defaultStyleInformation; + } else if (notificationDetails.style == NotificationStyle.BigPicture) { + readBigPictureStyleInformation( + notificationDetails, styleInformation, defaultStyleInformation); + } else if (notificationDetails.style == NotificationStyle.BigText) { + readBigTextStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); + } else if (notificationDetails.style == NotificationStyle.Inbox) { + readInboxStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); + } else if (notificationDetails.style == NotificationStyle.Messaging) { + readMessagingStyleInformation(notificationDetails, styleInformation, defaultStyleInformation); + } else if (notificationDetails.style == NotificationStyle.Media) { + notificationDetails.styleInformation = defaultStyleInformation; } - - private static void readBigPictureStyleInformation(NotificationDetails notificationDetails, Map styleInformation, DefaultStyleInformation defaultStyleInformation) { - String contentTitle = (String) styleInformation.get(CONTENT_TITLE); - Boolean htmlFormatContentTitle = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT_TITLE); - String summaryText = (String) styleInformation.get(SUMMARY_TEXT); - Boolean htmlFormatSummaryText = (Boolean) styleInformation.get(HTML_FORMAT_SUMMARY_TEXT); - String largeIcon = (String) styleInformation.get(LARGE_ICON); - BitmapSource largeIconBitmapSource = null; - if (styleInformation.containsKey(LARGE_ICON_BITMAP_SOURCE)) { - Integer largeIconBitmapSourceArgument = (Integer) styleInformation.get(LARGE_ICON_BITMAP_SOURCE); - largeIconBitmapSource = BitmapSource.values()[largeIconBitmapSourceArgument]; - } - String bigPicture = (String) styleInformation.get(BIG_PICTURE); - Integer bigPictureBitmapSourceArgument = (Integer) styleInformation.get(BIG_PICTURE_BITMAP_SOURCE); - BitmapSource bigPictureBitmapSource = BitmapSource.values()[bigPictureBitmapSourceArgument]; - Boolean showThumbnail = (Boolean) styleInformation.get(HIDE_EXPANDED_LARGE_ICON); - notificationDetails.styleInformation = new BigPictureStyleInformation(defaultStyleInformation.htmlFormatTitle, defaultStyleInformation.htmlFormatBody, contentTitle, htmlFormatContentTitle, summaryText, htmlFormatSummaryText, largeIcon, largeIconBitmapSource, bigPicture, bigPictureBitmapSource, showThumbnail); + } + + @SuppressWarnings("unchecked") + private static void readMessagingStyleInformation( + NotificationDetails notificationDetails, + Map styleInformation, + DefaultStyleInformation defaultStyleInformation) { + String conversationTitle = (String) styleInformation.get(CONVERSATION_TITLE); + Boolean groupConversation = (Boolean) styleInformation.get(GROUP_CONVERSATION); + PersonDetails person = readPersonDetails((Map) styleInformation.get(PERSON)); + ArrayList messages = + readMessages((ArrayList>) styleInformation.get(MESSAGES)); + notificationDetails.styleInformation = + new MessagingStyleInformation( + person, + conversationTitle, + groupConversation, + messages, + defaultStyleInformation.htmlFormatTitle, + defaultStyleInformation.htmlFormatBody); + } + + private static PersonDetails readPersonDetails(Map person) { + if (person == null) { + return null; } - - private static DefaultStyleInformation getDefaultStyleInformation(Map styleInformation) { - Boolean htmlFormatTitle = (Boolean) styleInformation.get(HTML_FORMAT_TITLE); - Boolean htmlFormatBody = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT); - return new DefaultStyleInformation(htmlFormatTitle, htmlFormatBody); + Boolean bot = (Boolean) person.get(BOT); + Object icon = person.get(ICON); + Integer iconSourceIndex = (Integer) person.get(ICON_SOURCE); + IconSource iconSource = iconSourceIndex == null ? null : IconSource.values()[iconSourceIndex]; + Boolean important = (Boolean) person.get(IMPORTANT); + String key = (String) person.get(KEY); + String name = (String) person.get(NAME); + String uri = (String) person.get(URI); + return new PersonDetails(bot, icon, iconSource, important, key, name, uri); + } + + @SuppressWarnings("unchecked") + private static ArrayList readMessages(ArrayList> messages) { + ArrayList result = new ArrayList<>(); + if (messages != null) { + for (Map messageData : messages) { + result.add( + new MessageDetails( + (String) messageData.get(TEXT), + (Long) messageData.get(TIMESTAMP), + readPersonDetails((Map) messageData.get(PERSON)), + (String) messageData.get(DATA_MIME_TYPE), + (String) messageData.get(DATA_URI))); + } + } + return result; + } + + private static void readInboxStyleInformation( + NotificationDetails notificationDetails, + Map styleInformation, + DefaultStyleInformation defaultStyleInformation) { + String contentTitle = (String) styleInformation.get(CONTENT_TITLE); + Boolean htmlFormatContentTitle = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT_TITLE); + String summaryText = (String) styleInformation.get(SUMMARY_TEXT); + Boolean htmlFormatSummaryText = (Boolean) styleInformation.get(HTML_FORMAT_SUMMARY_TEXT); + @SuppressWarnings("unchecked") + ArrayList lines = (ArrayList) styleInformation.get(LINES); + Boolean htmlFormatLines = (Boolean) styleInformation.get(HTML_FORMAT_LINES); + notificationDetails.styleInformation = + new InboxStyleInformation( + defaultStyleInformation.htmlFormatTitle, + defaultStyleInformation.htmlFormatBody, + contentTitle, + htmlFormatContentTitle, + summaryText, + htmlFormatSummaryText, + lines, + htmlFormatLines); + } + + private static void readBigTextStyleInformation( + NotificationDetails notificationDetails, + Map styleInformation, + DefaultStyleInformation defaultStyleInformation) { + String bigText = (String) styleInformation.get(BIG_TEXT); + Boolean htmlFormatBigText = (Boolean) styleInformation.get(HTML_FORMAT_BIG_TEXT); + String contentTitle = (String) styleInformation.get(CONTENT_TITLE); + Boolean htmlFormatContentTitle = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT_TITLE); + String summaryText = (String) styleInformation.get(SUMMARY_TEXT); + Boolean htmlFormatSummaryText = (Boolean) styleInformation.get(HTML_FORMAT_SUMMARY_TEXT); + notificationDetails.styleInformation = + new BigTextStyleInformation( + defaultStyleInformation.htmlFormatTitle, + defaultStyleInformation.htmlFormatBody, + bigText, + htmlFormatBigText, + contentTitle, + htmlFormatContentTitle, + summaryText, + htmlFormatSummaryText); + } + + private static void readBigPictureStyleInformation( + NotificationDetails notificationDetails, + Map styleInformation, + DefaultStyleInformation defaultStyleInformation) { + String contentTitle = (String) styleInformation.get(CONTENT_TITLE); + Boolean htmlFormatContentTitle = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT_TITLE); + String summaryText = (String) styleInformation.get(SUMMARY_TEXT); + Boolean htmlFormatSummaryText = (Boolean) styleInformation.get(HTML_FORMAT_SUMMARY_TEXT); + Object largeIcon = styleInformation.get(LARGE_ICON); + BitmapSource largeIconBitmapSource = null; + if (styleInformation.containsKey(LARGE_ICON_BITMAP_SOURCE)) { + Integer largeIconBitmapSourceArgument = + (Integer) styleInformation.get(LARGE_ICON_BITMAP_SOURCE); + largeIconBitmapSource = BitmapSource.values()[largeIconBitmapSourceArgument]; } + Object bigPicture = styleInformation.get(BIG_PICTURE); + Integer bigPictureBitmapSourceArgument = + (Integer) styleInformation.get(BIG_PICTURE_BITMAP_SOURCE); + BitmapSource bigPictureBitmapSource = BitmapSource.values()[bigPictureBitmapSourceArgument]; + Boolean showThumbnail = (Boolean) styleInformation.get(HIDE_EXPANDED_LARGE_ICON); + notificationDetails.styleInformation = + new BigPictureStyleInformation( + defaultStyleInformation.htmlFormatTitle, + defaultStyleInformation.htmlFormatBody, + contentTitle, + htmlFormatContentTitle, + summaryText, + htmlFormatSummaryText, + largeIcon, + largeIconBitmapSource, + bigPicture, + bigPictureBitmapSource, + showThumbnail); + } + + private static DefaultStyleInformation getDefaultStyleInformation( + Map styleInformation) { + Boolean htmlFormatTitle = (Boolean) styleInformation.get(HTML_FORMAT_TITLE); + Boolean htmlFormatBody = (Boolean) styleInformation.get(HTML_FORMAT_CONTENT); + return new DefaultStyleInformation(htmlFormatTitle, htmlFormatBody); + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/PersonDetails.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/PersonDetails.java index eef9e1653..04c923830 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/PersonDetails.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/PersonDetails.java @@ -2,23 +2,32 @@ import androidx.annotation.Keep; +import java.io.Serializable; + @Keep -public class PersonDetails { - public Boolean bot; - public String icon; - public IconSource iconBitmapSource; - public Boolean important; - public String key; - public String name; - public String uri; +public class PersonDetails implements Serializable { + public Boolean bot; + public Object icon; + public IconSource iconBitmapSource; + public Boolean important; + public String key; + public String name; + public String uri; - public PersonDetails(Boolean bot, String icon, IconSource iconSource, Boolean important, String key, String name, String uri) { - this.bot = bot; - this.icon = icon; - this.iconBitmapSource = iconSource; - this.important = important; - this.key = key; - this.name = name; - this.uri = uri; - } + public PersonDetails( + Boolean bot, + Object icon, + IconSource iconSource, + Boolean important, + String key, + String name, + String uri) { + this.bot = bot; + this.icon = icon; + this.iconBitmapSource = iconSource; + this.important = important; + this.key = key; + this.name = name; + this.uri = uri; + } } 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 index 953493cc4..3e4df1933 100644 --- 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 @@ -4,6 +4,6 @@ @Keep public enum ScheduledNotificationRepeatFrequency { - Daily, - Weekly + Daily, + Weekly } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/SoundSource.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/SoundSource.java new file mode 100644 index 000000000..3d7f3be88 --- /dev/null +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/SoundSource.java @@ -0,0 +1,9 @@ +package com.dexterous.flutterlocalnotifications.models; + +import androidx.annotation.Keep; + +@Keep +public enum SoundSource { + RawResource, + Uri +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/Time.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/Time.java index 4025b20d0..d2b79fcdd 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/Time.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/Time.java @@ -2,23 +2,24 @@ import androidx.annotation.Keep; +import java.io.Serializable; import java.util.Map; @Keep -public class Time { - private static final String HOUR = "hour"; - private static final String MINUTE = "minute"; - private static final String SECOND = "second"; +public class Time implements Serializable { + private static final String HOUR = "hour"; + private static final String MINUTE = "minute"; + private static final String SECOND = "second"; - public Integer hour = 0; - public Integer minute = 0; - public Integer second = 0; + public Integer hour = 0; + public Integer minute = 0; + public Integer second = 0; - public static Time from(Map arguments) { - Time time = new Time(); - time.hour = (Integer) arguments.get(HOUR); - time.minute = (Integer) arguments.get(MINUTE); - time.second = (Integer) arguments.get(SECOND); - return time; - } -} \ No newline at end of file + public static Time from(Map arguments) { + Time time = new Time(); + time.hour = (Integer) arguments.get(HOUR); + time.minute = (Integer) arguments.get(MINUTE); + time.second = (Integer) arguments.get(SECOND); + return time; + } +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigPictureStyleInformation.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigPictureStyleInformation.java index 5c7f307fa..3c433677a 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigPictureStyleInformation.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigPictureStyleInformation.java @@ -2,30 +2,41 @@ import androidx.annotation.Keep; -import com.dexterous.flutterlocalnotifications.BitmapSource; +import com.dexterous.flutterlocalnotifications.models.BitmapSource; @Keep public class BigPictureStyleInformation extends DefaultStyleInformation { - public String contentTitle; - public Boolean htmlFormatContentTitle; - public String summaryText; - public Boolean htmlFormatSummaryText; - public String largeIcon; - public BitmapSource largeIconBitmapSource; - public String bigPicture; - public BitmapSource bigPictureBitmapSource; - public Boolean hideExpandedLargeIcon; + public String contentTitle; + public Boolean htmlFormatContentTitle; + public String summaryText; + public Boolean htmlFormatSummaryText; + public Object largeIcon; + public BitmapSource largeIconBitmapSource; + public Object bigPicture; + public BitmapSource bigPictureBitmapSource; + public Boolean hideExpandedLargeIcon; - public BigPictureStyleInformation(Boolean htmlFormatTitle, Boolean htmlFormatBody, String contentTitle, Boolean htmlFormatContentTitle, String summaryText, Boolean htmlFormatSummaryText, String largeIcon, BitmapSource largeIconBitmapSource, String bigPicture, BitmapSource bigPictureBitmapSource, Boolean hideExpandedLargeIcon) { - super(htmlFormatTitle, htmlFormatBody); - this.contentTitle = contentTitle; - this.htmlFormatContentTitle = htmlFormatContentTitle; - this.summaryText = summaryText; - this.htmlFormatSummaryText = htmlFormatSummaryText; - this.largeIcon = largeIcon; - this.largeIconBitmapSource = largeIconBitmapSource; - this.bigPicture = bigPicture; - this.bigPictureBitmapSource = bigPictureBitmapSource; - this.hideExpandedLargeIcon = hideExpandedLargeIcon; - } + public BigPictureStyleInformation( + Boolean htmlFormatTitle, + Boolean htmlFormatBody, + String contentTitle, + Boolean htmlFormatContentTitle, + String summaryText, + Boolean htmlFormatSummaryText, + Object largeIcon, + BitmapSource largeIconBitmapSource, + Object bigPicture, + BitmapSource bigPictureBitmapSource, + Boolean hideExpandedLargeIcon) { + super(htmlFormatTitle, htmlFormatBody); + this.contentTitle = contentTitle; + this.htmlFormatContentTitle = htmlFormatContentTitle; + this.summaryText = summaryText; + this.htmlFormatSummaryText = htmlFormatSummaryText; + this.largeIcon = largeIcon; + this.largeIconBitmapSource = largeIconBitmapSource; + this.bigPicture = bigPicture; + this.bigPictureBitmapSource = bigPictureBitmapSource; + this.hideExpandedLargeIcon = hideExpandedLargeIcon; + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigTextStyleInformation.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigTextStyleInformation.java index 027edc085..63012c566 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigTextStyleInformation.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/BigTextStyleInformation.java @@ -4,20 +4,28 @@ @Keep public class BigTextStyleInformation extends DefaultStyleInformation { - public String bigText; - public Boolean htmlFormatBigText; - public String contentTitle; - public Boolean htmlFormatContentTitle; - public String summaryText; - public Boolean htmlFormatSummaryText; + public String bigText; + public Boolean htmlFormatBigText; + public String contentTitle; + public Boolean htmlFormatContentTitle; + public String summaryText; + public Boolean htmlFormatSummaryText; - public BigTextStyleInformation(Boolean htmlFormatTitle, Boolean htmlFormatBody, String bigText, Boolean htmlFormatBigText, String contentTitle, Boolean htmlFormatContentTitle, String summaryText, Boolean htmlFormatSummaryText) { - super(htmlFormatTitle, htmlFormatBody); - this.bigText = bigText; - this.htmlFormatBigText = htmlFormatBigText; - this.contentTitle = contentTitle; - this.htmlFormatContentTitle = htmlFormatContentTitle; - this.summaryText = summaryText; - this.htmlFormatSummaryText = htmlFormatSummaryText; - } + public BigTextStyleInformation( + Boolean htmlFormatTitle, + Boolean htmlFormatBody, + String bigText, + Boolean htmlFormatBigText, + String contentTitle, + Boolean htmlFormatContentTitle, + String summaryText, + Boolean htmlFormatSummaryText) { + super(htmlFormatTitle, htmlFormatBody); + this.bigText = bigText; + this.htmlFormatBigText = htmlFormatBigText; + this.contentTitle = contentTitle; + this.htmlFormatContentTitle = htmlFormatContentTitle; + this.summaryText = summaryText; + this.htmlFormatSummaryText = htmlFormatSummaryText; + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/DefaultStyleInformation.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/DefaultStyleInformation.java index 4d196b207..d9ea5fa15 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/DefaultStyleInformation.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/DefaultStyleInformation.java @@ -4,11 +4,11 @@ @Keep public class DefaultStyleInformation extends StyleInformation { - public Boolean htmlFormatTitle; - public Boolean htmlFormatBody; + public Boolean htmlFormatTitle; + public Boolean htmlFormatBody; - public DefaultStyleInformation(Boolean htmlFormatTitle, Boolean htmlFormatBody) { - this.htmlFormatTitle = htmlFormatTitle; - this.htmlFormatBody = htmlFormatBody; - } + public DefaultStyleInformation(Boolean htmlFormatTitle, Boolean htmlFormatBody) { + this.htmlFormatTitle = htmlFormatTitle; + this.htmlFormatBody = htmlFormatBody; + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/InboxStyleInformation.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/InboxStyleInformation.java index 1d7559394..5fd8dac9b 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/InboxStyleInformation.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/InboxStyleInformation.java @@ -6,20 +6,28 @@ @Keep public class InboxStyleInformation extends DefaultStyleInformation { - public Boolean htmlFormatLines; - public ArrayList lines; - public String contentTitle; - public Boolean htmlFormatContentTitle; - public String summaryText; - public Boolean htmlFormatSummaryText; + public Boolean htmlFormatLines; + public ArrayList lines; + public String contentTitle; + public Boolean htmlFormatContentTitle; + public String summaryText; + public Boolean htmlFormatSummaryText; - public InboxStyleInformation(Boolean htmlFormatTitle, Boolean htmlFormatBody, String contentTitle, Boolean htmlFormatContentTitle, String summaryText, Boolean htmlFormatSummaryText, ArrayList lines, Boolean htmlFormatLines) { - super(htmlFormatTitle, htmlFormatBody); - this.contentTitle = contentTitle; - this.htmlFormatContentTitle = htmlFormatContentTitle; - this.summaryText = summaryText; - this.htmlFormatSummaryText = htmlFormatSummaryText; - this.lines = lines; - this.htmlFormatLines = htmlFormatLines; - } -} \ No newline at end of file + public InboxStyleInformation( + Boolean htmlFormatTitle, + Boolean htmlFormatBody, + String contentTitle, + Boolean htmlFormatContentTitle, + String summaryText, + Boolean htmlFormatSummaryText, + ArrayList lines, + Boolean htmlFormatLines) { + super(htmlFormatTitle, htmlFormatBody); + this.contentTitle = contentTitle; + this.htmlFormatContentTitle = htmlFormatContentTitle; + this.summaryText = summaryText; + this.htmlFormatSummaryText = htmlFormatSummaryText; + this.lines = lines; + this.htmlFormatLines = htmlFormatLines; + } +} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/MessagingStyleInformation.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/MessagingStyleInformation.java index e3c43f0cb..3c6eeaeac 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/MessagingStyleInformation.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/MessagingStyleInformation.java @@ -9,16 +9,22 @@ @Keep public class MessagingStyleInformation extends DefaultStyleInformation { - public PersonDetails person; - public String conversationTitle; - public Boolean groupConversation; - public ArrayList messages; + public PersonDetails person; + public String conversationTitle; + public Boolean groupConversation; + public ArrayList messages; - public MessagingStyleInformation(PersonDetails person, String conversationTitle, Boolean groupConversation, ArrayList messages, Boolean htmlFormatTitle, Boolean htmlFormatBody) { - super(htmlFormatTitle, htmlFormatBody); - this.person = person; - this.conversationTitle = conversationTitle; - this.groupConversation = groupConversation; - this.messages = messages; - } + public MessagingStyleInformation( + PersonDetails person, + String conversationTitle, + Boolean groupConversation, + ArrayList messages, + Boolean htmlFormatTitle, + Boolean htmlFormatBody) { + super(htmlFormatTitle, htmlFormatBody); + this.person = person; + this.conversationTitle = conversationTitle; + this.groupConversation = groupConversation; + this.messages = messages; + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/StyleInformation.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/StyleInformation.java index 5f98e2030..592b30742 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/StyleInformation.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/styles/StyleInformation.java @@ -2,6 +2,7 @@ import androidx.annotation.Keep; +import java.io.Serializable; + @Keep -public abstract class StyleInformation { -} +public abstract class StyleInformation implements Serializable {} diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/BooleanUtils.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/BooleanUtils.java index 7a06cd495..3de60da38 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/BooleanUtils.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/BooleanUtils.java @@ -4,7 +4,7 @@ @Keep public class BooleanUtils { - public static boolean getValue(Boolean booleanObject){ - return booleanObject != null && booleanObject.booleanValue(); - } + public static boolean getValue(Boolean booleanObject) { + return booleanObject != null && booleanObject.booleanValue(); + } } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/StringUtils.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/StringUtils.java index fed74ebd6..e7c9d4986 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/StringUtils.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/utils/StringUtils.java @@ -4,7 +4,7 @@ @Keep public class StringUtils { - public static Boolean isNullOrEmpty(String string){ - return string == null || string.isEmpty(); - } + public static Boolean isNullOrEmpty(String string) { + return string == null || string.isEmpty(); + } } diff --git a/flutter_local_notifications/example/analysis_options.yaml b/flutter_local_notifications/example/analysis_options.yaml index 8df560e25..14ccfc93f 100644 --- a/flutter_local_notifications/example/analysis_options.yaml +++ b/flutter_local_notifications/example/analysis_options.yaml @@ -1,3 +1,6 @@ +analyzer: + exclude: + - lib/generated_plugin_registrant.dart linter: rules: - always_declare_return_types @@ -7,7 +10,7 @@ linter: - always_specify_types - annotate_overrides - avoid_annotating_with_dynamic - - avoid_as + # - avoid_as - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses - avoid_catching_errors @@ -128,7 +131,7 @@ linter: - slash_for_doc_comments - sort_child_properties_last - sort_constructors_first - - sort_pub_dependencies + # - sort_pub_dependencies - sort_unnamed_constructors_first - test_types_in_equals - throw_in_finally @@ -161,4 +164,4 @@ linter: - use_string_buffers - use_to_and_as_if_applicable - valid_regexps - - void_checks \ No newline at end of file + - void_checks diff --git a/flutter_local_notifications/example/android/.gitignore b/flutter_local_notifications/example/android/.gitignore index 65b7315af..0a741cb43 100644 --- a/flutter_local_notifications/example/android/.gitignore +++ b/flutter_local_notifications/example/android/.gitignore @@ -1,10 +1,11 @@ -*.iml -*.class -.gradle +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat /local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/flutter_local_notifications/example/android/app/build.gradle b/flutter_local_notifications/example/android/app/build.gradle index 5ba544458..2bae4d49b 100644 --- a/flutter_local_notifications/example/android/app/build.gradle +++ b/flutter_local_notifications/example/android/app/build.gradle @@ -11,22 +11,34 @@ if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - applicationId "com.dexterous.flutterlocalnotificationsexample" + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.dexterous.flutter_local_notifications_example" minSdkVersion 16 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -35,23 +47,13 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug - - // Enables code shrinking, obfuscation, and optimization for only - // your project's release build type. - minifyEnabled true - - // Enables resource shrinking, which is performed by the - // Android Gradle plugin. - shrinkResources true - - // Includes the default ProGuard rules files that are packaged with - // the Android Gradle plugin. To learn more, go to the section about - // R8 configuration files. - proguardFiles getDefaultProguardFile( - 'proguard-android-optimize.txt'), - 'proguard-rules.pro' } } + + // Temporary workaround as per https://issuetracker.google.com/issues/158060799 + lintOptions { + checkReleaseBuilds false + } } flutter { @@ -59,10 +61,10 @@ flutter { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.jakewharton.threetenabp:threetenabp:1.2.3" + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' - 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 fd1b02bdb..2d6b4a959 100644 --- a/flutter_local_notifications/example/android/app/proguard-rules.pro +++ b/flutter_local_notifications/example/android/app/proguard-rules.pro @@ -1,12 +1,3 @@ -## Flutter wrapper --keep class io.flutter.app.** { *; } --keep class io.flutter.plugin.** { *; } --keep class io.flutter.util.** { *; } --keep class io.flutter.view.** { *; } --keep class io.flutter.** { *; } --keep class io.flutter.plugins.** { *; } --dontwarn io.flutter.embedding.** - ## Gson rules # Gson uses generic type information stored in a class file when working with fields. Proguard # removes such information by default, so configure it to keep all of it. diff --git a/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutter_local_notifications_example/MainActivityTest.java b/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutter_local_notifications_example/MainActivityTest.java new file mode 100644 index 000000000..573a9da5f --- /dev/null +++ b/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutter_local_notifications_example/MainActivityTest.java @@ -0,0 +1,14 @@ +package com.dexterous.flutter_local_notifications_example; + +import androidx.test.rule.ActivityTestRule; +import org.junit.Rule; +import org.junit.runner.RunWith; + +import dev.flutter.plugins.integration_test.FlutterTestRunner; + +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(MainActivity.class, true, false); +} 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 deleted file mode 100644 index 5c60399f6..000000000 --- a/flutter_local_notifications/example/android/app/src/androidTest/java/com/dexterous/flutterlocalnotifications/MainActivityTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.dexterous.flutterlocalnotifications; - -import androidx.test.rule.ActivityTestRule; - -import com.dexterous.flutterlocalnotificationsexample.MainActivity; - -import dev.flutter.plugins.e2e.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class MainActivityTest { - @Rule - 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/debug/AndroidManifest.xml b/flutter_local_notifications/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..c1569a936 --- /dev/null +++ b/flutter_local_notifications/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + 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 538e279e7..0b2815d0d 100644 --- a/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml +++ b/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml @@ -1,39 +1,52 @@ - - - - - - + + android:turnScreenOn="true" + android:exported="true"> + + + + + This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> + + + diff --git a/flutter_local_notifications/example/android/app/src/main/java/com/dexterous/flutterlocalnotificationsexample/MainActivity.java b/flutter_local_notifications/example/android/app/src/main/java/com/dexterous/flutterlocalnotificationsexample/MainActivity.java deleted file mode 100644 index 5a5ab53fb..000000000 --- a/flutter_local_notifications/example/android/app/src/main/java/com/dexterous/flutterlocalnotificationsexample/MainActivity.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.dexterous.flutterlocalnotificationsexample; - -import android.content.ContentResolver; -import android.content.Context; -import android.media.RingtoneManager; -import android.os.Bundle; - -import java.util.TimeZone; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - private static String resourceToUriString(Context context, int resId) { - return - ContentResolver.SCHEME_ANDROID_RESOURCE - + "://" - + context.getResources().getResourcePackageName(resId) - + "/" - + context.getResources().getResourceTypeName(resId) - + "/" - + context.getResources().getResourceEntryName(resId); - } - - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine); - new MethodChannel(flutterEngine.getDartExecutor(), "dexterx.dev/flutter_local_notifications_example").setMethodCallHandler( - (call, result) -> { - if ("drawableToUri".equals(call.method)) { - int resourceId = MainActivity.this.getResources().getIdentifier((String) call.arguments, "drawable", MainActivity.this.getPackageName()); - result.success(resourceToUriString(MainActivity.this.getApplicationContext(), resourceId)); - } - if("getAlarmUri".equals(call.method)) { - result.success(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM).toString()); - } - if("getTimeZoneName".equals(call.method)) { - result.success(TimeZone.getDefault().getID()); - } - }); - } -} \ No newline at end of file diff --git a/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt b/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt new file mode 100644 index 000000000..e36ed7c92 --- /dev/null +++ b/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt @@ -0,0 +1,34 @@ +package com.dexterous.flutter_local_notifications_example + +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.plugin.common.MethodChannel +import java.util.* + + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "dexterx.dev/flutter_local_notifications_example").setMethodCallHandler { call, result -> + if ("drawableToUri" == call.method) { + val resourceId = this@MainActivity.resources.getIdentifier(call.arguments as String, "drawable", this@MainActivity.packageName) + result.success(resourceToUriString(this@MainActivity.applicationContext, resourceId)) + } + if ("getAlarmUri" == call.method) { + result.success(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM).toString()) + } + } + } + + private fun resourceToUriString(context: Context, resId: Int): String? { + return (ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + context.resources.getResourcePackageName(resId) + + "/" + + context.resources.getResourceTypeName(resId) + + "/" + + context.resources.getResourceEntryName(resId)) + } +} diff --git a/flutter_local_notifications/example/android/app/src/main/res/drawable-v21/launch_background.xml b/flutter_local_notifications/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/flutter_local_notifications/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter_local_notifications/example/android/app/src/main/res/values-night/styles.xml b/flutter_local_notifications/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..449a9f930 --- /dev/null +++ b/flutter_local_notifications/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/flutter_local_notifications/example/android/app/src/main/res/values/styles.xml b/flutter_local_notifications/example/android/app/src/main/res/values/styles.xml index 00fa4417c..d74aa35c2 100644 --- a/flutter_local_notifications/example/android/app/src/main/res/values/styles.xml +++ b/flutter_local_notifications/example/android/app/src/main/res/values/styles.xml @@ -1,8 +1,18 @@ - + + diff --git a/flutter_local_notifications/example/android/app/src/profile/AndroidManifest.xml b/flutter_local_notifications/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..c1569a936 --- /dev/null +++ b/flutter_local_notifications/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/flutter_local_notifications/example/android/build.gradle b/flutter_local_notifications/example/android/build.gradle index 1f45b28b5..24047dce5 100644 --- a/flutter_local_notifications/example/android/build.gradle +++ b/flutter_local_notifications/example/android/build.gradle @@ -1,23 +1,20 @@ buildscript { + ext.kotlin_version = '1.3.50' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() - jcenter() - } - gradle.projectsEvaluated { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" - } + mavenCentral() } } diff --git a/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties b/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties index 2d0da59ad..bc6a58afd 100644 --- a/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun May 03 15:09:14 AEST 2020 +#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/flutter_local_notifications/example/android/settings.gradle b/flutter_local_notifications/example/android/settings.gradle index 5a2f14fb1..44e62bcf0 100644 --- a/flutter_local_notifications/example/android/settings.gradle +++ b/flutter_local_notifications/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/flutter_local_notifications/example/icons/1.5x/app_icon_density.png b/flutter_local_notifications/example/icons/1.5x/app_icon_density.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/flutter_local_notifications/example/icons/1.5x/app_icon_density.png differ diff --git a/flutter_local_notifications/example/icons/2.0x/app_icon_density.png b/flutter_local_notifications/example/icons/2.0x/app_icon_density.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/flutter_local_notifications/example/icons/2.0x/app_icon_density.png differ diff --git a/flutter_local_notifications/example/icons/3.0x/app_icon_density.png b/flutter_local_notifications/example/icons/3.0x/app_icon_density.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/flutter_local_notifications/example/icons/3.0x/app_icon_density.png differ diff --git a/flutter_local_notifications/example/icons/4.0x/app_icon_density.png b/flutter_local_notifications/example/icons/4.0x/app_icon_density.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/flutter_local_notifications/example/icons/4.0x/app_icon_density.png differ diff --git a/flutter_local_notifications/example/icons/app_icon.png b/flutter_local_notifications/example/icons/app_icon.png new file mode 100644 index 000000000..c4cc798d7 Binary files /dev/null and b/flutter_local_notifications/example/icons/app_icon.png differ diff --git a/flutter_local_notifications/example/icons/app_icon_density.png b/flutter_local_notifications/example/icons/app_icon_density.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/flutter_local_notifications/example/icons/app_icon_density.png differ diff --git a/flutter_local_notifications/example/test_driver/flutter_local_notifications_e2e.dart b/flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart similarity index 96% rename from flutter_local_notifications/example/test_driver/flutter_local_notifications_e2e.dart rename to flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart index e59f77888..a21e81aa0 100644 --- a/flutter_local_notifications/example/test_driver/flutter_local_notifications_e2e.dart +++ b/flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart @@ -1,11 +1,13 @@ +// @dart = 2.9 + import 'dart:io'; -import 'package:e2e/e2e.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; group('initialize()', () { setUpAll(() async { diff --git a/flutter_local_notifications/example/ios/Flutter/AppFrameworkInfo.plist b/flutter_local_notifications/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483e..8d4492f97 100644 --- a/flutter_local_notifications/example/ios/Flutter/AppFrameworkInfo.plist +++ b/flutter_local_notifications/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/flutter_local_notifications/example/ios/Runner.xcodeproj/project.pbxproj b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.pbxproj index 25001d56f..b1317b56b 100644 --- a/flutter_local_notifications/example/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,21 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 51AC6CD32084C6F70042C077 /* slow_spring_board.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 51AC6CD22084C6F70042C077 /* slow_spring_board.aiff */; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 51155F9225B5B31600CEBA3A /* slow_spring_board.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 51155F9125B5B31600CEBA3A /* slow_spring_board.aiff */; }; + 6C09322FA59197E669E2EE5E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59531C21662011537870918B /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B6BC096867E2CDC7D72158C1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FDAE19F7270673E2DA66D4E /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -37,21 +34,21 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 51AC6CD22084C6F70042C077 /* slow_spring_board.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = slow_spring_board.aiff; sourceTree = SOURCE_ROOT; }; - 534C29A22B3DE51E7D11AAB7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 5FDAE19F7270673E2DA66D4E /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 68ABB3A076FEC4EC3F71EBB4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 40C5B899F98F1C3C5F54F083 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 51155F9125B5B31600CEBA3A /* slow_spring_board.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = slow_spring_board.aiff; sourceTree = SOURCE_ROOT; }; + 59531C21662011537870918B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D08F0A5F4043804786EAECD7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F2ADC50DEEB76E0B5C93F37C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -59,13 +56,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B6BC096867E2CDC7D72158C1 /* libPods-Runner.a in Frameworks */, + 6C09322FA59197E669E2EE5E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 05167A11411FF5D8B2E37DB8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 59531C21662011537870918B /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -83,8 +88,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - BFDDF33B560D99A1F1EB205D /* Pods */, - FEB620C7FF7840C2297690B8 /* Frameworks */, + CC9CABD983DC0B414464B379 /* Pods */, + 05167A11411FF5D8B2E37DB8 /* Frameworks */, ); sourceTree = ""; }; @@ -99,43 +104,28 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 51AC6CD22084C6F70042C077 /* slow_spring_board.aiff */, - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 51155F9125B5B31600CEBA3A /* slow_spring_board.aiff */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - BFDDF33B560D99A1F1EB205D /* Pods */ = { + CC9CABD983DC0B414464B379 /* Pods */ = { isa = PBXGroup; children = ( - 68ABB3A076FEC4EC3F71EBB4 /* Pods-Runner.debug.xcconfig */, - 534C29A22B3DE51E7D11AAB7 /* Pods-Runner.release.xcconfig */, + 40C5B899F98F1C3C5F54F083 /* Pods-Runner.debug.xcconfig */, + F2ADC50DEEB76E0B5C93F37C /* Pods-Runner.release.xcconfig */, + D08F0A5F4043804786EAECD7 /* Pods-Runner.profile.xcconfig */, ); name = Pods; - sourceTree = ""; - }; - FEB620C7FF7840C2297690B8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5FDAE19F7270673E2DA66D4E /* libPods-Runner.a */, - ); - name = Frameworks; + path = Pods; sourceTree = ""; }; /* End PBXGroup section */ @@ -145,13 +135,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B5B2A146A868A8661960FBBA /* [CP] Check Pods Manifest.lock */, + 11ABBB00F6B4C56238928421 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + B18758020F275E3A9128BDE4 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -168,20 +159,20 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; - ORGANIZATIONNAME = "The Chromium Authors"; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - English, en, Base, ); @@ -200,11 +191,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 51AC6CD32084C6F70042C077 /* slow_spring_board.aiff in Resources */, + 51155F9225B5B31600CEBA3A /* slow_spring_board.aiff in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -213,6 +202,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 11ABBB00F6B4C56238928421 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -241,22 +252,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B5B2A146A868A8661960FBBA /* [CP] Check Pods Manifest.lock */ = { + B18758020F275E3A9128BDE4 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -266,8 +276,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -294,6 +303,77 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.dexterous.flutterLocalNotificationsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -307,12 +387,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -339,7 +421,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -360,12 +442,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -386,9 +470,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -399,19 +486,20 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.dexterous.flutterLocalNotificationsExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -420,19 +508,19 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.dexterous.flutterLocalNotificationsExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -444,6 +532,7 @@ buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -453,6 +542,7 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/flutter_local_notifications/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/flutter_local_notifications/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter_local_notifications/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1263ac84b..a28140cfd 100644 --- a/flutter_local_notifications/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_local_notifications/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -67,7 +65,7 @@ + + + + PreviewsEnabled + + + diff --git a/flutter_local_notifications/example/ios/Runner/AppDelegate.h b/flutter_local_notifications/example/ios/Runner/AppDelegate.h deleted file mode 100644 index cf210d213..000000000 --- a/flutter_local_notifications/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/flutter_local_notifications/example/ios/Runner/AppDelegate.m b/flutter_local_notifications/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 21ccdbc67..000000000 --- a/flutter_local_notifications/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,44 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" -#import -#import -#import - -void registerPlugins(NSObject* registry) { - [GeneratedPluginRegistrant registerWithRegistry:registry]; -} - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - - // This is required to make any communication available in the action isolate. - [FlutterLocalNotificationsPlugin setPluginRegistrantCallback:registerPlugins]; - - // cancel old notifications that were scheduled to be periodically shown upon a reinstallation of the app - if(![[NSUserDefaults standardUserDefaults]objectForKey:@"Notification"]){ - [[UIApplication sharedApplication] cancelAllLocalNotifications]; - [[NSUserDefaults standardUserDefaults]setBool:YES forKey:@"Notification"]; - } - if(@available(iOS 10.0, *)) { - [UNUserNotificationCenter currentNotificationCenter].delegate = (id) self; - } - - FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; - - FlutterMethodChannel* channel = [FlutterMethodChannel - methodChannelWithName:@"dexterx.dev/flutter_local_notifications_example" - binaryMessenger:controller.binaryMessenger]; - - [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { - if([@"getTimeZoneName" isEqualToString:call.method]) { - result([[NSTimeZone localTimeZone] name]); - } - }]; - - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/flutter_local_notifications/example/ios/Runner/AppDelegate.swift b/flutter_local_notifications/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..53b73dddc --- /dev/null +++ b/flutter_local_notifications/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,24 @@ +import UIKit +import Flutter +import flutter_local_notifications + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // This is required to make any communication available in the action isolate. + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) + } + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/flutter_local_notifications/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter_local_notifications/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e6..dc9ada472 100644 Binary files a/flutter_local_notifications/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter_local_notifications/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/flutter_local_notifications/example/ios/Runner/Info.plist b/flutter_local_notifications/example/ios/Runner/Info.plist index 449dca7e6..ffb007d95 100644 --- a/flutter_local_notifications/example/ios/Runner/Info.plist +++ b/flutter_local_notifications/example/ios/Runner/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/flutter_local_notifications/example/ios/Runner/Runner-Bridging-Header.h b/flutter_local_notifications/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..308a2a560 --- /dev/null +++ b/flutter_local_notifications/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/flutter_local_notifications/example/ios/Runner/main.m b/flutter_local_notifications/example/ios/Runner/main.m deleted file mode 100644 index 0ccc45001..000000000 --- a/flutter_local_notifications/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char * argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 108a51d36..fd0dab5db 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1,17 +1,21 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:device_info/device_info.dart'; import 'package:flutter/cupertino.dart'; +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_native_timezone/flutter_native_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:rxdart/subjects.dart'; -import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = @@ -22,30 +26,32 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final BehaviorSubject didReceiveLocalNotificationSubject = BehaviorSubject(); -final BehaviorSubject selectNotificationSubject = - BehaviorSubject(); +final BehaviorSubject selectNotificationSubject = + BehaviorSubject(); const MethodChannel platform = MethodChannel('dexterx.dev/flutter_local_notifications_example'); class ReceivedNotification { ReceivedNotification({ - @required this.id, - @required this.title, - @required this.body, - @required this.payload, + required this.id, + required this.title, + required this.body, + required this.payload, }); final int id; - final String title; - final String body; - final String payload; + final String? title; + final String? body; + final String? payload; } -void notificationTapBackground(String id, String input, String payload) { +String? selectedNotificationPayload; + +void notificationTapBackground(String id, String? input, String? payload) { // ignore: avoid_print print('notification action tapped: $id with payload: $payload'); - if (input.isNotEmpty) { + if (input?.isNotEmpty ?? false) { // ignore: avoid_print print('notification action tapped with input: $input'); } @@ -62,8 +68,15 @@ Future main() async { await _configureLocalTimeZone(); - final NotificationAppLaunchDetails notificationAppLaunchDetails = - await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb && + Platform.isLinux + ? null + : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + String initialRoute = HomePage.routeName; + if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + selectedNotificationPayload = notificationAppLaunchDetails!.payload; + initialRoute = SecondPage.routeName; + } const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); @@ -76,10 +89,14 @@ Future main() async { requestBadgePermission: false, requestSoundPermission: false, onDidReceiveLocalNotification: - (int id, String title, String body, String payload) async { + (int id, String? title, String? body, String? payload) async { didReceiveLocalNotificationSubject.add( ReceivedNotification( - id: id, title: title, body: body, payload: payload), + id: id, + title: title, + body: body, + payload: payload, + ), ); }, notificationCategories: [ @@ -124,16 +141,24 @@ Future main() async { ); const MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings( - requestAlertPermission: false, - requestBadgePermission: false, - requestSoundPermission: false); + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + ); + final LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings( + defaultActionName: 'Open notification', + defaultIcon: AssetsLinuxIcon('icons/app_icon.png'), + ); final InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS, - macOS: initializationSettingsMacOS); + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + macOS: initializationSettingsMacOS, + linux: initializationSettingsLinux, + ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, - onSelectNotification: (String payload) async { + onSelectNotification: (String? payload) async { if (payload != null) { debugPrint('notification payload: $payload'); } @@ -143,24 +168,29 @@ Future main() async { ); runApp( MaterialApp( - home: HomePage( - notificationAppLaunchDetails, - ), + initialRoute: initialRoute, + routes: { + HomePage.routeName: (_) => HomePage(notificationAppLaunchDetails), + SecondPage.routeName: (_) => SecondPage(selectedNotificationPayload) + }, ), ); } Future _configureLocalTimeZone() async { + if (kIsWeb || Platform.isLinux) { + return; + } tz.initializeTimeZones(); - final String timeZoneName = await platform.invokeMethod('getTimeZoneName'); - tz.setLocalLocation(tz.getLocation(timeZoneName)); + final String? timeZoneName = await FlutterNativeTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(timeZoneName!)); } -class PaddedRaisedButton extends StatelessWidget { - const PaddedRaisedButton({ - @required this.buttonText, - @required this.onPressed, - Key key, +class PaddedElevatedButton extends StatelessWidget { + const PaddedElevatedButton({ + required this.buttonText, + required this.onPressed, + Key? key, }) : super(key: key); final String buttonText; @@ -169,7 +199,7 @@ class PaddedRaisedButton extends StatelessWidget { @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: RaisedButton( + child: ElevatedButton( onPressed: onPressed, child: Text(buttonText), ), @@ -179,10 +209,13 @@ class PaddedRaisedButton extends StatelessWidget { class HomePage extends StatefulWidget { const HomePage( this.notificationAppLaunchDetails, { - Key key, + Key? key, }) : super(key: key); - final NotificationAppLaunchDetails notificationAppLaunchDetails; + static const String routeName = '/'; + + final NotificationAppLaunchDetails? notificationAppLaunchDetails; + bool get didNotificationLaunchApp => notificationAppLaunchDetails?.didNotificationLaunchApp ?? false; @@ -225,10 +258,10 @@ class _HomePageState extends State { context: context, builder: (BuildContext context) => CupertinoAlertDialog( title: receivedNotification.title != null - ? Text(receivedNotification.title) + ? Text(receivedNotification.title!) : null, content: receivedNotification.body != null - ? Text(receivedNotification.body) + ? Text(receivedNotification.body!) : null, actions: [ CupertinoDialogAction( @@ -239,7 +272,7 @@ class _HomePageState extends State { context, MaterialPageRoute( builder: (BuildContext context) => - SecondScreen(receivedNotification.payload), + SecondPage(receivedNotification.payload), ), ); }, @@ -252,12 +285,8 @@ class _HomePageState extends State { } void _configureSelectNotificationSubject() { - selectNotificationSubject.stream.listen((String payload) async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => SecondScreen(payload)), - ); + selectNotificationSubject.stream.listen((String? payload) async { + await Navigator.pushNamed(context, '/secondPage'); }); } @@ -286,47 +315,22 @@ class _HomePageState extends State { 'Tap on a notification when it appears to trigger' ' navigation'), ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: Text.rich( - TextSpan( - children: [ - const TextSpan( - text: 'Did notification launch app? ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: '${widget.didNotificationLaunchApp}', - ) - ], - ), - ), + _InfoValueString( + title: 'Did notification launch app?', + value: widget.didNotificationLaunchApp, ), if (widget.didNotificationLaunchApp) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: Text.rich( - TextSpan( - children: [ - const TextSpan( - text: 'Launch notification payload: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: - widget.notificationAppLaunchDetails.payload, - ) - ], - ), - ), + _InfoValueString( + title: 'Launch notification payload:', + value: widget.notificationAppLaunchDetails!.payload, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show plain notification with payload', onPressed: () async { await _showNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show plain notification that has no title with ' 'payload', @@ -334,7 +338,7 @@ class _HomePageState extends State { await _showNotificationWithNoTitle(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show plain notification that has no body with ' 'payload', @@ -342,69 +346,95 @@ class _HomePageState extends State { await _showNotificationWithNoBody(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with custom sound', onPressed: () async { await _showNotificationCustomSound(); }, ), - PaddedRaisedButton( - buttonText: - 'Schedule notification to appear in 5 seconds based ' - 'on local time zone', - onPressed: () async { - await _zonedScheduleNotification(); - }, - ), - PaddedRaisedButton( - buttonText: 'Repeat notification every minute', - onPressed: () async { - await _repeatNotification(); - }, - ), - PaddedRaisedButton( - buttonText: - 'Schedule daily 10:00:00 am notification in your ' - 'local time zone', - onPressed: () async { - await _scheduleDailyTenAMNotification(); - }, - ), - PaddedRaisedButton( + if (kIsWeb || !Platform.isLinux) ...[ + PaddedElevatedButton( + buttonText: + 'Schedule notification to appear in 5 seconds ' + 'based on local time zone', + onPressed: () async { + await _zonedScheduleNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Repeat notification every minute', + onPressed: () async { + await _repeatNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule daily 10:00:00 am notification in your ' + 'local time zone', + onPressed: () async { + await _scheduleDailyTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule daily 10:00:00 am notification in your ' + "local time zone using last year's date", + onPressed: () async { + await _scheduleDailyTenAMLastYearNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule weekly 10:00:00 am notification in your ' + 'local time zone', + onPressed: () async { + await _scheduleWeeklyTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule weekly Monday 10:00:00 am notification ' + 'in your local time zone', + onPressed: () async { + await _scheduleWeeklyMondayTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Check pending notifications', + onPressed: () async { + await _checkPendingNotificationRequests(); + }, + ), + ], + PaddedElevatedButton( buttonText: - 'Schedule weekly 10:00:00 am notification in your ' - 'local time zone', + 'Schedule monthly Monday 10:00:00 am notification in ' + 'your local time zone', onPressed: () async { - await _scheduleWeeklyTenAMNotification(); + await _scheduleMonthlyMondayTenAMNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: - 'Schedule weekly Monday 10:00:00 am notification in ' + 'Schedule yearly Monday 10:00:00 am notification in ' 'your local time zone', onPressed: () async { - await _scheduleWeeklyMondayTenAMNotification(); + await _scheduleYearlyMondayTenAMNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with no sound', onPressed: () async { await _showNotificationWithNoSound(); }, ), - PaddedRaisedButton( - buttonText: 'Check pending notifications', - onPressed: () async { - await _checkPendingNotificationRequests(); - }, - ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Cancel notification', onPressed: () async { await _cancelNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Cancel all notifications', onPressed: () async { await _cancelAllNotifications(); @@ -415,19 +445,19 @@ class _HomePageState extends State { 'Notifications with actions', style: TextStyle(fontWeight: FontWeight.bold), ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with plain actions', onPressed: () async { await _showNotificationWithActions(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with text actions', onPressed: () async { await _showNotificationWithTextAction(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with text choice', onPressed: () async { await _showNotificationWithTextChoice(); @@ -439,7 +469,7 @@ class _HomePageState extends State { 'Android-specific examples', style: TextStyle(fontWeight: FontWeight.bold), ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show plain notification with payload and update ' 'channel description', @@ -447,7 +477,7 @@ class _HomePageState extends State { await _showNotificationUpdateChannelDescription(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show plain notification as public on every ' 'lockscreen', @@ -455,7 +485,7 @@ class _HomePageState extends State { await _showPublicNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with custom vibration pattern, ' 'red LED and red icon', @@ -463,32 +493,49 @@ class _HomePageState extends State { await _showNotificationCustomVibrationIconLed(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification using Android Uri sound', onPressed: () async { await _showSoundUriNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification that times out after 3 seconds', onPressed: () async { await _showTimeoutNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show insistent notification', onPressed: () async { await _showInsistentNotification(); }, ), - PaddedRaisedButton( - buttonText: 'Show big picture notification', + PaddedElevatedButton( + buttonText: + 'Show big picture notification using local images', onPressed: () async { await _showBigPictureNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( + buttonText: + 'Show big picture notification using base64 String ' + 'for images', + onPressed: () async { + await _showBigPictureNotificationBase64(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Show big picture notification using URLs for ' + 'Images', + onPressed: () async { + await _showBigPictureNotificationURL(); + }, + ), + PaddedElevatedButton( buttonText: 'Show big picture notification, hide large icon ' 'on expand', @@ -496,140 +543,321 @@ class _HomePageState extends State { await _showBigPictureNotificationHiddenLargeIcon(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show media notification', onPressed: () async { await _showNotificationMediaStyle(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show big text notification', onPressed: () async { await _showBigTextNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show inbox notification', onPressed: () async { await _showInboxNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show messaging notification', onPressed: () async { await _showMessagingNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show grouped notifications', onPressed: () async { await _showGroupedNotifications(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( + buttonText: 'Show notification with tag', + onPressed: () async { + await _showNotificationWithTag(); + }, + ), + PaddedElevatedButton( + buttonText: 'Cancel notification with tag', + onPressed: () async { + await _cancelNotificationWithTag(); + }, + ), + PaddedElevatedButton( buttonText: 'Show ongoing notification', onPressed: () async { await _showOngoingNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with no badge, alert only once', onPressed: () async { await _showNotificationWithNoBadge(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show progress notification - updates every second', onPressed: () async { await _showProgressNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show indeterminate progress notification', onPressed: () async { await _showIndeterminateProgressNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification without timestamp', onPressed: () async { await _showNotificationWithoutTimestamp(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with custom timestamp', onPressed: () async { await _showNotificationWithCustomTimestamp(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( + buttonText: 'Show notification with custom sub-text', + onPressed: () async { + await _showNotificationWithCustomSubText(); + }, + ), + PaddedElevatedButton( buttonText: 'Show notification with chronometer', onPressed: () async { await _showNotificationWithChronometer(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show full-screen notification', onPressed: () async { await _showFullScreenNotification(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Create grouped notification channels', onPressed: () async { await _createNotificationChannelGroup(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Delete notification channel group', onPressed: () async { await _deleteNotificationChannelGroup(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Create notification channel', onPressed: () async { await _createNotificationChannel(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Delete notification channel', onPressed: () async { await _deleteNotificationChannel(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( + buttonText: 'Get notification channels', + onPressed: () async { + await _getNotificationChannels(); + }, + ), + PaddedElevatedButton( buttonText: 'Get active notifications', onPressed: () async { await _getActiveNotifications(); }, ), + PaddedElevatedButton( + buttonText: 'Start foreground service', + onPressed: () async { + await _startForegroundService(); + }, + ), + PaddedElevatedButton( + buttonText: 'Stop foreground service', + onPressed: () async { + await _stopForegroundService(); + }, + ), ], - if (Platform.isIOS || Platform.isMacOS) ...[ + if (!kIsWeb && + (Platform.isIOS || Platform.isMacOS)) ...[ const Text( 'iOS and macOS-specific examples', style: TextStyle(fontWeight: FontWeight.bold), ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with subtitle', onPressed: () async { await _showNotificationWithSubtitle(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with icon badge', onPressed: () async { await _showNotificationWithIconBadge(); }, ), - PaddedRaisedButton( + PaddedElevatedButton( buttonText: 'Show notification with attachment', onPressed: () async { await _showNotificationWithAttachment(); }, ), + PaddedElevatedButton( + buttonText: 'Show notifications with thread identifier', + onPressed: () async { + await _showNotificationsWithThreadIdentifier(); + }, + ), + ], + if (!kIsWeb && Platform.isLinux) ...[ + const Text( + 'Linux-specific examples', + style: TextStyle(fontWeight: FontWeight.bold), + ), + FutureBuilder( + future: getLinuxCapabilities(), + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + final LinuxServerCapabilities caps = snapshot.data!; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Capabilities of the current system:', + style: Theme.of(context) + .textTheme + .subtitle1! + .copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + _InfoValueString( + title: 'Body text:', + value: caps.body, + ), + _InfoValueString( + title: 'Hyperlinks in body text:', + value: caps.bodyHyperlinks, + ), + _InfoValueString( + title: 'Images in body:', + value: caps.bodyImages, + ), + _InfoValueString( + title: 'Markup in the body text:', + value: caps.bodyMarkup, + ), + _InfoValueString( + title: 'Animated icons:', + value: caps.iconMulti, + ), + _InfoValueString( + title: 'Static icons:', + value: caps.iconStatic, + ), + _InfoValueString( + title: 'Notification persistence:', + value: caps.persistence, + ), + _InfoValueString( + title: 'Sound:', + value: caps.sound, + ), + _InfoValueString( + title: 'Other capabilities:', + value: caps.otherCapabilities, + ), + ], + ), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with body markup', + onPressed: () async { + await _showLinuxNotificationWithBodyMarkup(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with category', + onPressed: () async { + await _showLinuxNotificationWithCategory(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with byte data icon', + onPressed: () async { + await _showLinuxNotificationWithByteDataIcon(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with theme icon', + onPressed: () async { + await _showLinuxNotificationWithThemeIcon(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with theme sound', + onPressed: () async { + await _showLinuxNotificationWithThemeSound(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with critical urgency', + onPressed: () async { + await _showLinuxNotificationWithCriticalUrgency(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with timeout', + onPressed: () async { + await _showLinuxNotificationWithTimeout(); + }, + ), + PaddedElevatedButton( + buttonText: 'Suppress notification sound', + onPressed: () async { + await _showLinuxNotificationSuppressSound(); + }, + ), + PaddedElevatedButton( + buttonText: 'Transient notification', + onPressed: () async { + await _showLinuxNotificationTransient(); + }, + ), + PaddedElevatedButton( + buttonText: 'Resident notification', + onPressed: () async { + await _showLinuxNotificationResident(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification on ' + 'different screen location', + onPressed: () async { + await _showLinuxNotificationDifferentLocation(); + }, + ), ], ], ), @@ -641,8 +869,8 @@ class _HomePageState extends State { Future _showNotification() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); @@ -658,7 +886,7 @@ class _HomePageState extends State { AndroidNotificationDetails( 'your channel id', 'your channel name', - 'your channel description', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker', @@ -696,7 +924,7 @@ class _HomePageState extends State { AndroidNotificationDetails( 'your channel id', 'your channel name', - 'your channel description', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker', @@ -733,7 +961,7 @@ class _HomePageState extends State { AndroidNotificationDetails( 'your channel id', 'your channel name', - 'your channel description', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker', @@ -769,50 +997,50 @@ class _HomePageState extends State { Future _showFullScreenNotification() async { await showDialog( - context: context, - child: AlertDialog( - title: const Text('Turn off your screen'), - content: const Text( - 'to see the full-screen intent in 5 seconds, press OK and TURN ' - 'OFF your screen'), - actions: [ - FlatButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Cancel'), - ), - FlatButton( - onPressed: () async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'scheduled title', - 'scheduled body', - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), - const NotificationDetails( - android: AndroidNotificationDetails( - 'full screen channel id', - 'full screen channel name', - 'full screen channel description', - priority: Priority.high, - importance: Importance.high, - fullScreenIntent: true)), - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime); - - Navigator.pop(context); - }, - child: const Text('OK'), - ) - ], - )); + context: context, + builder: (_) => AlertDialog( + title: const Text('Turn off your screen'), + content: const Text( + 'to see the full-screen intent in 5 seconds, press OK and TURN ' + 'OFF your screen'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'scheduled title', + 'scheduled body', + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), + const NotificationDetails( + android: AndroidNotificationDetails( + 'full screen channel id', 'full screen channel name', + channelDescription: 'full screen channel description', + priority: Priority.high, + importance: Importance.high, + fullScreenIntent: true)), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime); + + Navigator.pop(context); + }, + child: const Text('OK'), + ) + ], + ), + ); } Future _showNotificationWithNoBody() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); @@ -826,8 +1054,8 @@ class _HomePageState extends State { Future _showNotificationWithNoTitle() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); @@ -843,27 +1071,38 @@ class _HomePageState extends State { await flutterLocalNotificationsPlugin.cancel(0); } + Future _cancelNotificationWithTag() async { + await flutterLocalNotificationsPlugin.cancel(0, tag: 'tag'); + } + Future _showNotificationCustomSound() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( 'your other channel id', 'your other channel name', - 'your other channel description', + channelDescription: 'your other channel description', sound: RawResourceAndroidNotificationSound('slow_spring_board'), ); const IOSNotificationDetails iOSPlatformChannelSpecifics = IOSNotificationDetails(sound: 'slow_spring_board.aiff'); const MacOSNotificationDetails macOSPlatformChannelSpecifics = MacOSNotificationDetails(sound: 'slow_spring_board.aiff'); - const NotificationDetails platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics, - macOS: macOSPlatformChannelSpecifics); + final LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + sound: AssetsLinuxSound('sound/slow_spring_board.mp3'), + ); + final NotificationDetails platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + macOS: macOSPlatformChannelSpecifics, + linux: linuxPlatformChannelSpecifics, + ); await flutterLocalNotificationsPlugin.show( - 0, - 'custom sound notification title', - 'custom sound notification body', - platformChannelSpecifics); + 0, + 'custom sound notification title', + 'custom sound notification body', + platformChannelSpecifics, + ); } Future _showNotificationCustomVibrationIconLed() async { @@ -874,8 +1113,9 @@ class _HomePageState extends State { vibrationPattern[3] = 2000; final AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('other custom channel id', - 'other custom channel name', 'other custom channel description', + AndroidNotificationDetails( + 'other custom channel id', 'other custom channel name', + channelDescription: 'other custom channel description', icon: 'secondary_icon', largeIcon: const DrawableResourceAndroidBitmap('sample_large_icon'), vibrationPattern: vibrationPattern, @@ -901,8 +1141,9 @@ class _HomePageState extends State { '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')), + android: AndroidNotificationDetails( + 'your channel id', 'your channel name', + channelDescription: 'your channel description')), androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime); @@ -911,7 +1152,7 @@ class _HomePageState extends State { Future _showNotificationWithNoSound() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('silent channel id', 'silent channel name', - 'silent channel description', + channelDescription: 'silent channel description', playSound: false, styleInformation: DefaultStyleInformation(true, true)); const IOSNotificationDetails iOSPlatformChannelSpecifics = @@ -930,12 +1171,12 @@ class _HomePageState extends State { /// this calls a method over a platform channel implemented within the /// example app to return the Uri for the default alarm sound and uses /// as the notification sound - final String alarmUri = await platform.invokeMethod('getAlarmUri'); + final String? alarmUri = await platform.invokeMethod('getAlarmUri'); final UriAndroidNotificationSound uriSound = - UriAndroidNotificationSound(alarmUri); + UriAndroidNotificationSound(alarmUri!); final AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'uri channel id', 'uri channel name', 'uri channel description', + AndroidNotificationDetails('uri channel id', 'uri channel name', + channelDescription: 'uri channel description', sound: uriSound, styleInformation: const DefaultStyleInformation(true, true)); final NotificationDetails platformChannelSpecifics = @@ -947,7 +1188,7 @@ class _HomePageState extends State { Future _showTimeoutNotification() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('silent channel id', 'silent channel name', - 'silent channel description', + channelDescription: 'silent channel description', timeoutAfter: 3000, styleInformation: DefaultStyleInformation(true, true)); const NotificationDetails platformChannelSpecifics = @@ -960,8 +1201,8 @@ class _HomePageState extends State { // This value is from: https://developer.android.com/reference/android/app/Notification.html#FLAG_INSISTENT const int insistentFlag = 4; final AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker', @@ -976,7 +1217,7 @@ class _HomePageState extends State { Future _downloadAndSaveFile(String url, String fileName) async { final Directory directory = await getApplicationDocumentsDirectory(); final String filePath = '${directory.path}/$fileName'; - final http.Response response = await http.get(url); + final http.Response response = await http.get(Uri.parse(url)); final File file = File(filePath); await file.writeAsBytes(response.bodyBytes); return filePath; @@ -995,8 +1236,70 @@ class _HomePageState extends State { summaryText: 'summary text', htmlFormatSummaryText: true); final AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('big text channel id', - 'big text channel name', 'big text channel description', + AndroidNotificationDetails( + 'big text channel id', 'big text channel name', + channelDescription: 'big text channel description', + styleInformation: bigPictureStyleInformation); + final NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 0, 'big text title', 'silent body', platformChannelSpecifics); + } + + Future _base64encodedImage(String url) async { + final http.Response response = await http.get(Uri.parse(url)); + final String base64Data = base64Encode(response.bodyBytes); + return base64Data; + } + + Future _showBigPictureNotificationBase64() async { + final String largeIcon = + await _base64encodedImage('https://via.placeholder.com/48x48'); + final String bigPicture = + await _base64encodedImage('https://via.placeholder.com/400x800'); + + final BigPictureStyleInformation bigPictureStyleInformation = + BigPictureStyleInformation( + ByteArrayAndroidBitmap.fromBase64String( + bigPicture), //Base64AndroidBitmap(bigPicture), + largeIcon: ByteArrayAndroidBitmap.fromBase64String(largeIcon), + contentTitle: 'overridden big content title', + htmlFormatContentTitle: true, + summaryText: 'summary text', + htmlFormatSummaryText: true); + final AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'big text channel id', 'big text channel name', + channelDescription: 'big text channel description', + styleInformation: bigPictureStyleInformation); + final NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 0, 'big text title', 'silent body', platformChannelSpecifics); + } + + Future _getByteArrayFromUrl(String url) async { + final http.Response response = await http.get(Uri.parse(url)); + return response.bodyBytes; + } + + Future _showBigPictureNotificationURL() async { + final ByteArrayAndroidBitmap largeIcon = ByteArrayAndroidBitmap( + await _getByteArrayFromUrl('https://via.placeholder.com/48x48')); + final ByteArrayAndroidBitmap bigPicture = ByteArrayAndroidBitmap( + await _getByteArrayFromUrl('https://via.placeholder.com/400x800')); + + final BigPictureStyleInformation bigPictureStyleInformation = + BigPictureStyleInformation(bigPicture, + largeIcon: largeIcon, + contentTitle: 'overridden big content title', + htmlFormatContentTitle: true, + summaryText: 'summary text', + htmlFormatSummaryText: true); + final AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'big text channel id', 'big text channel name', + channelDescription: 'big text channel description', styleInformation: bigPictureStyleInformation); final NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); @@ -1017,8 +1320,9 @@ class _HomePageState extends State { summaryText: 'summary text', htmlFormatSummaryText: true); final AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('big text channel id', - 'big text channel name', 'big text channel description', + AndroidNotificationDetails( + 'big text channel id', 'big text channel name', + channelDescription: 'big text channel description', largeIcon: FilePathAndroidBitmap(largeIconPath), styleInformation: bigPictureStyleInformation); final NotificationDetails platformChannelSpecifics = @@ -1034,7 +1338,7 @@ class _HomePageState extends State { AndroidNotificationDetails( 'media channel id', 'media channel name', - 'media channel description', + channelDescription: 'media channel description', largeIcon: FilePathAndroidBitmap(largeIconPath), styleInformation: const MediaStyleInformation(), ); @@ -1055,8 +1359,9 @@ class _HomePageState extends State { htmlFormatSummaryText: true, ); const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('big text channel id', - 'big text channel name', 'big text channel description', + AndroidNotificationDetails( + 'big text channel id', 'big text channel name', + channelDescription: 'big text channel description', styleInformation: bigTextStyleInformation); const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); @@ -1075,7 +1380,7 @@ class _HomePageState extends State { htmlFormatSummaryText: true); final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('inbox channel id', 'inboxchannel name', - 'inbox channel description', + channelDescription: 'inbox channel description', styleInformation: inboxStyleInformation); final NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); @@ -1087,7 +1392,7 @@ class _HomePageState extends State { // use a platform channel to resolve an Android drawable resource to a URI. // This is NOT part of the notifications plugin. Calls made over this /// channel is handled by the app - final String imageUri = + final String? imageUri = await platform.invokeMethod('drawableToUri', 'food'); /// First two person objects will use icons that part of the Android app's @@ -1114,6 +1419,13 @@ class _HomePageState extends State { bot: true, icon: BitmapFilePathAndroidIcon(largeIconPath), ); + final Person chef = Person( + name: 'Master Chef', + key: '3', + uri: 'tel:111222333444', + icon: ByteArrayAndroidIcon.fromBase64String( + await _base64encodedImage('https://placekitten.com/48/48'))); + final List messages = [ Message('Hi', DateTime.now(), null), Message("What's up?", DateTime.now().add(const Duration(minutes: 5)), @@ -1122,6 +1434,8 @@ class _HomePageState extends State { dataMimeType: 'image/png', dataUri: imageUri), Message('What kind of food would you prefer?', DateTime.now().add(const Duration(minutes: 10)), lunchBot), + Message('You do not have time eat! Keep working!', + DateTime.now().add(const Duration(minutes: 11)), chef), ]; final MessagingStyleInformation messagingStyle = MessagingStyleInformation( me, @@ -1132,8 +1446,9 @@ class _HomePageState extends State { messages: messages); final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('message channel id', 'message channel name', - 'message channel description', - category: 'msg', styleInformation: messagingStyle); + channelDescription: 'message channel description', + category: 'msg', + styleInformation: messagingStyle); final NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.show( @@ -1141,8 +1456,8 @@ class _HomePageState extends State { // wait 10 seconds and add another message to simulate another response await Future.delayed(const Duration(seconds: 10), () async { - messages.add(Message( - 'Thai', DateTime.now().add(const Duration(minutes: 11)), null)); + messages.add(Message("I'm so sorry!!! But I really like thai food ...", + DateTime.now().add(const Duration(minutes: 11)), null)); await flutterLocalNotificationsPlugin.show( 0, 'message title', 'message body', platformChannelSpecifics); }); @@ -1155,8 +1470,8 @@ class _HomePageState extends State { const String groupChannelDescription = 'grouped channel description'; // example based on https://developer.android.com/training/notify-user/group.html const AndroidNotificationDetails firstNotificationAndroidSpecifics = - AndroidNotificationDetails( - groupChannelId, groupChannelName, groupChannelDescription, + AndroidNotificationDetails(groupChannelId, groupChannelName, + channelDescription: groupChannelDescription, importance: Importance.max, priority: Priority.high, groupKey: groupKey); @@ -1165,8 +1480,8 @@ class _HomePageState extends State { await flutterLocalNotificationsPlugin.show(1, 'Alex Faarborg', 'You will not believe...', firstNotificationPlatformSpecifics); const AndroidNotificationDetails secondNotificationAndroidSpecifics = - AndroidNotificationDetails( - groupChannelId, groupChannelName, groupChannelDescription, + AndroidNotificationDetails(groupChannelId, groupChannelName, + channelDescription: groupChannelDescription, importance: Importance.max, priority: Priority.high, groupKey: groupKey); @@ -1192,8 +1507,8 @@ class _HomePageState extends State { contentTitle: '2 messages', summaryText: 'janedoe@example.com'); const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - groupChannelId, groupChannelName, groupChannelDescription, + AndroidNotificationDetails(groupChannelId, groupChannelName, + channelDescription: groupChannelDescription, styleInformation: inboxStyleInformation, groupKey: groupKey, setAsGroupSummary: true); @@ -1203,6 +1518,20 @@ class _HomePageState extends State { 3, 'Attention', 'Two messages', platformChannelSpecifics); } + Future _showNotificationWithTag() async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', + importance: Importance.max, + priority: Priority.high, + tag: 'tag'); + const NotificationDetails platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, 'first notification', null, platformChannelSpecifics); + } + Future _checkPendingNotificationRequests() async { final List pendingNotificationRequests = await flutterLocalNotificationsPlugin.pendingNotificationRequests(); @@ -1213,7 +1542,7 @@ class _HomePageState extends State { Text('${pendingNotificationRequests.length} pending notification ' 'requests'), actions: [ - FlatButton( + TextButton( onPressed: () { Navigator.of(context).pop(); }, @@ -1230,8 +1559,8 @@ class _HomePageState extends State { Future _showOngoingNotification() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ongoing: true, @@ -1244,8 +1573,9 @@ class _HomePageState extends State { Future _repeatNotification() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('repeating channel id', - 'repeating channel name', 'repeating description'); + AndroidNotificationDetails( + 'repeating channel id', 'repeating channel name', + channelDescription: 'repeating description'); const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.periodicallyShow(0, 'repeating title', @@ -1260,10 +1590,27 @@ class _HomePageState extends State { 'daily scheduled notification body', _nextInstanceOfTenAM(), const NotificationDetails( - android: AndroidNotificationDetails( - 'daily notification channel id', + android: AndroidNotificationDetails('daily notification channel id', 'daily notification channel name', - 'daily notification description'), + channelDescription: 'daily notification description'), + ), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.time); + } + + /// To test we don't validate past dates when using `matchDateTimeComponents` + Future _scheduleDailyTenAMLastYearNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'daily scheduled notification title', + 'daily scheduled notification body', + _nextInstanceOfTenAMLastYear(), + const NotificationDetails( + android: AndroidNotificationDetails('daily notification channel id', + 'daily notification channel name', + channelDescription: 'daily notification description'), ), androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: @@ -1278,10 +1625,9 @@ class _HomePageState extends State { 'weekly scheduled notification body', _nextInstanceOfTenAM(), const NotificationDetails( - android: AndroidNotificationDetails( - 'weekly notification channel id', + android: AndroidNotificationDetails('weekly notification channel id', 'weekly notification channel name', - 'weekly notificationdescription'), + channelDescription: 'weekly notificationdescription'), ), androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: @@ -1296,10 +1642,9 @@ class _HomePageState extends State { 'weekly scheduled notification body', _nextInstanceOfMondayTenAM(), const NotificationDetails( - android: AndroidNotificationDetails( - 'weekly notification channel id', + android: AndroidNotificationDetails('weekly notification channel id', 'weekly notification channel name', - 'weekly notificationdescription'), + channelDescription: 'weekly notificationdescription'), ), androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: @@ -1307,6 +1652,40 @@ class _HomePageState extends State { matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); } + Future _scheduleMonthlyMondayTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'monthly scheduled notification title', + 'monthly scheduled notification body', + _nextInstanceOfMondayTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('monthly notification channel id', + 'monthly notification channel name', + channelDescription: 'monthly notificationdescription'), + ), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime); + } + + Future _scheduleYearlyMondayTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'yearly scheduled notification title', + 'yearly scheduled notification body', + _nextInstanceOfMondayTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('yearly notification channel id', + 'yearly notification channel name', + channelDescription: 'yearly notification description'), + ), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dateAndTime); + } + tz.TZDateTime _nextInstanceOfTenAM() { final tz.TZDateTime now = tz.TZDateTime.now(tz.local); tz.TZDateTime scheduledDate = @@ -1317,6 +1696,11 @@ class _HomePageState extends State { return scheduledDate; } + tz.TZDateTime _nextInstanceOfTenAMLastYear() { + final tz.TZDateTime now = tz.TZDateTime.now(tz.local); + return tz.TZDateTime(tz.local, now.year - 1, now.month, now.day, 10); + } + tz.TZDateTime _nextInstanceOfMondayTenAM() { tz.TZDateTime scheduledDate = _nextInstanceOfTenAM(); while (scheduledDate.weekday != DateTime.monday) { @@ -1327,8 +1711,8 @@ class _HomePageState extends State { Future _showNotificationWithNoBadge() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'no badge channel', 'no badge name', 'no badge description', + AndroidNotificationDetails('no badge channel', 'no badge name', + channelDescription: 'no badge description', channelShowBadge: false, importance: Importance.max, priority: Priority.high, @@ -1346,7 +1730,7 @@ class _HomePageState extends State { await Future.delayed(const Duration(seconds: 1), () async { final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('progress channel', 'progress channel', - 'progress channel description', + channelDescription: 'progress channel description', channelShowBadge: false, importance: Importance.max, priority: Priority.high, @@ -1369,9 +1753,8 @@ class _HomePageState extends State { Future _showIndeterminateProgressNotification() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( - 'indeterminate progress channel', - 'indeterminate progress channel', - 'indeterminate progress channel description', + 'indeterminate progress channel', 'indeterminate progress channel', + channelDescription: 'indeterminate progress channel description', channelShowBadge: false, importance: Importance.max, priority: Priority.high, @@ -1391,7 +1774,7 @@ class _HomePageState extends State { Future _showNotificationUpdateChannelDescription() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('your channel id', 'your channel name', - 'your updated channel description', + channelDescription: 'your updated channel description', importance: Importance.max, priority: Priority.high, channelAction: AndroidNotificationChannelAction.update); @@ -1407,8 +1790,8 @@ class _HomePageState extends State { Future _showPublicNotification() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, ticker: 'ticker', @@ -1447,10 +1830,43 @@ class _HomePageState extends State { payload: 'item x'); } + Future _showNotificationsWithThreadIdentifier() async { + NotificationDetails buildNotificationDetailsForThread( + String threadIdentifier, + ) { + final IOSNotificationDetails iOSPlatformChannelSpecifics = + IOSNotificationDetails(threadIdentifier: threadIdentifier); + final MacOSNotificationDetails macOSPlatformChannelSpecifics = + MacOSNotificationDetails(threadIdentifier: threadIdentifier); + return NotificationDetails( + iOS: iOSPlatformChannelSpecifics, + macOS: macOSPlatformChannelSpecifics); + } + + final NotificationDetails thread1PlatformChannelSpecifics = + buildNotificationDetailsForThread('thread1'); + final NotificationDetails thread2PlatformChannelSpecifics = + buildNotificationDetailsForThread('thread2'); + + await flutterLocalNotificationsPlugin.show( + 0, 'thread 1', 'first notification', thread1PlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 1, 'thread 1', 'second notification', thread1PlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 2, 'thread 1', 'third notification', thread1PlatformChannelSpecifics); + + await flutterLocalNotificationsPlugin.show( + 3, 'thread 2', 'first notification', thread2PlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 4, 'thread 2', 'second notification', thread2PlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 5, 'thread 2', 'third notification', thread2PlatformChannelSpecifics); + } + Future _showNotificationWithoutTimestamp() async { const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, showWhen: false); @@ -1466,7 +1882,7 @@ class _HomePageState extends State { AndroidNotificationDetails( 'your channel id', 'your channel name', - 'your channel description', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, when: DateTime.now().millisecondsSinceEpoch - 120 * 1000, @@ -1478,12 +1894,30 @@ class _HomePageState extends State { payload: 'item x'); } + Future _showNotificationWithCustomSubText() async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'your channel id', + 'your channel name', + channelDescription: 'your channel description', + importance: Importance.max, + priority: Priority.high, + showWhen: false, + subText: 'custom subtext', + ); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 0, 'plain title', 'plain body', platformChannelSpecifics, + payload: 'item x'); + } + Future _showNotificationWithChronometer() async { final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( 'your channel id', 'your channel name', - 'your channel description', + channelDescription: 'your channel description', importance: Importance.max, priority: Priority.high, when: DateTime.now().millisecondsSinceEpoch - 120 * 1000, @@ -1525,26 +1959,24 @@ class _HomePageState extends State { description: 'your channel group description'); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannelGroup(androidNotificationChannelGroup); // create channels associated with the group await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannel(const AndroidNotificationChannel( - 'grouped channel id 1', - 'grouped channel name 1', - 'grouped channel description 1', + 'grouped channel id 1', 'grouped channel name 1', + description: 'grouped channel description 1', groupId: channelGroupId)); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannel(const AndroidNotificationChannel( - 'grouped channel id 2', - 'grouped channel name 2', - 'grouped channel description 2', + 'grouped channel id 2', 'grouped channel name 2', + description: 'grouped channel description 2', groupId: channelGroupId)); await showDialog( @@ -1553,7 +1985,7 @@ class _HomePageState extends State { content: Text('Channel group with name ' '${androidNotificationChannelGroup.name} created'), actions: [ - FlatButton( + TextButton( onPressed: () { Navigator.of(context).pop(); }, @@ -1575,7 +2007,7 @@ class _HomePageState extends State { builder: (BuildContext context) => AlertDialog( content: const Text('Channel group with id $channelGroupId deleted'), actions: [ - FlatButton( + TextButton( onPressed: () { Navigator.of(context).pop(); }, @@ -1586,12 +2018,34 @@ class _HomePageState extends State { ); } + Future _startForegroundService() async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails('your channel id', 'your channel name', + channelDescription: 'your channel description', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.startForegroundService(1, 'plain title', 'plain body', + notificationDetails: androidPlatformChannelSpecifics, + payload: 'item x'); + } + + Future _stopForegroundService() async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.stopForegroundService(); + } + Future _createNotificationChannel() async { const AndroidNotificationChannel androidNotificationChannel = AndroidNotificationChannel( 'your channel id 2', 'your channel name 2', - 'your channel description 2', + description: 'your channel description 2', ); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< @@ -1605,7 +2059,7 @@ class _HomePageState extends State { Text('Channel with name ${androidNotificationChannel.name} ' 'created'), actions: [ - FlatButton( + TextButton( onPressed: () { Navigator.of(context).pop(); }, @@ -1627,7 +2081,7 @@ class _HomePageState extends State { builder: (BuildContext context) => AlertDialog( content: const Text('Channel with id $channelId deleted'), actions: [ - FlatButton( + TextButton( onPressed: () { Navigator.of(context).pop(); }, @@ -1646,7 +2100,7 @@ class _HomePageState extends State { builder: (BuildContext context) => AlertDialog( content: activeNotificationsDialogContent, actions: [ - FlatButton( + TextButton( onPressed: () { Navigator.of(context).pop(); }, @@ -1667,11 +2121,11 @@ class _HomePageState extends State { } try { - final List activeNotifications = + final List? activeNotifications = await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.getActiveNotifications(); + AndroidFlutterLocalNotificationsPlugin>()! + .getActiveNotifications(); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1682,7 +2136,7 @@ class _HomePageState extends State { style: TextStyle(fontWeight: FontWeight.bold), ), const Divider(color: Colors.black), - if (activeNotifications.isEmpty) + if (activeNotifications!.isEmpty) const Text('No active notifications'), if (activeNotifications.isNotEmpty) for (ActiveNotification activeNotification in activeNotifications) @@ -1708,22 +2162,289 @@ class _HomePageState extends State { ); } } + + Future _getNotificationChannels() async { + final Widget notificationChannelsDialogContent = + await _getNotificationChannelsDialogContent(); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + content: notificationChannelsDialogContent, + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ), + ); + } + + Future _getNotificationChannelsDialogContent() async { + try { + final List? channels = + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()! + .getNotificationChannels(); + + return Container( + width: double.maxFinite, + child: ListView( + children: [ + const Text( + 'Notifications Channels', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const Divider(color: Colors.black), + if (channels?.isEmpty ?? true) + const Text('No notification channels') + else + for (AndroidNotificationChannel channel in channels!) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('id: ${channel.id}\n' + 'name: ${channel.name}\n' + 'description: ${channel.description}\n' + 'groupId: ${channel.groupId}\n' + 'importance: ${channel.importance.value}\n' + 'playSound: ${channel.playSound}\n' + 'sound: ${channel.sound?.sound}\n' + 'enableVibration: ${channel.enableVibration}\n' + 'vibrationPattern: ${channel.vibrationPattern}\n' + 'showBadge: ${channel.showBadge}\n' + 'enableLights: ${channel.enableLights}\n' + 'ledColor: ${channel.ledColor}\n'), + const Divider(color: Colors.black), + ], + ), + ], + ), + ); + } on PlatformException catch (error) { + return Text( + 'Error calling "getNotificationChannels"\n' + 'code: ${error.code}\n' + 'message: ${error.message}', + ); + } + } +} + +Future _showLinuxNotificationWithBodyMarkup() async { + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with body markup', + 'bold text\n' + 'italic text\n' + 'underline text\n' + 'https://example.com\n' + 'example.com', + null, + ); +} + +Future _showLinuxNotificationWithCategory() async { + final LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + category: LinuxNotificationCategory.emailArrived(), + ); + final NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with category', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationWithByteDataIcon() async { + final ByteData assetIcon = await rootBundle.load( + 'icons/app_icon_density.png', + ); + final image.Image? iconData = image.decodePng( + assetIcon.buffer.asUint8List().toList(), + ); + final Uint8List iconBytes = iconData!.getBytes(); + final LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + icon: ByteDataLinuxIcon( + LinuxRawIconData( + data: iconBytes, + width: iconData.width, + height: iconData.height, + channels: 4, // The icon has an alpha channel + hasAlpha: true, + ), + ), + ); + final NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with byte data icon', + null, + platformChannelSpecifics, + ); } -class SecondScreen extends StatefulWidget { - const SecondScreen( +Future _showLinuxNotificationWithThemeIcon() async { + final LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + icon: ThemeLinuxIcon('media-eject'), + ); + final NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with theme icon', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationWithThemeSound() async { + final LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + sound: ThemeLinuxSound('message-new-email'), + ); + final NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with theme sound', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationWithCriticalUrgency() async { + const LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + urgency: LinuxNotificationUrgency.critical, + ); + const NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with critical urgency', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationWithTimeout() async { + final LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + timeout: LinuxNotificationTimeout.fromDuration( + const Duration(seconds: 1), + ), + ); + final NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification with timeout', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationSuppressSound() async { + const LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + suppressSound: true, + ); + const NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'suppress notification sound', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationTransient() async { + const LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + transient: true, + ); + const NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'transient notification', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationResident() async { + const LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails( + resident: true, + ); + const NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'resident notification', + null, + platformChannelSpecifics, + ); +} + +Future _showLinuxNotificationDifferentLocation() async { + const LinuxNotificationDetails linuxPlatformChannelSpecifics = + LinuxNotificationDetails(location: LinuxNotificationLocation(10, 10)); + const NotificationDetails platformChannelSpecifics = NotificationDetails( + linux: linuxPlatformChannelSpecifics, + ); + await flutterLocalNotificationsPlugin.show( + 0, + 'notification on different screen location', + null, + platformChannelSpecifics, + ); +} + +Future getLinuxCapabilities() => + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + LinuxFlutterLocalNotificationsPlugin>()! + .getCapabilities(); + +class SecondPage extends StatefulWidget { + const SecondPage( this.payload, { - Key key, + Key? key, }) : super(key: key); - final String payload; + static const String routeName = '/secondPage'; + + final String? payload; @override - State createState() => SecondScreenState(); + State createState() => SecondPageState(); } -class SecondScreenState extends State { - String _payload; +class SecondPageState extends State { + String? _payload; + @override void initState() { super.initState(); @@ -1736,7 +2457,7 @@ class SecondScreenState extends State { title: Text('Second Screen with payload: ${_payload ?? ''}'), ), body: Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () { Navigator.pop(context); }, @@ -1745,3 +2466,34 @@ class SecondScreenState extends State { ), ); } + +class _InfoValueString extends StatelessWidget { + const _InfoValueString({ + required this.title, + required this.value, + Key? key, + }) : super(key: key); + + final String title; + final Object? value; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '$title ', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: '$value', + ) + ], + ), + ), + ); +} diff --git a/flutter_local_notifications/example/linux/.gitignore b/flutter_local_notifications/example/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/flutter_local_notifications/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/flutter_local_notifications/example/linux/CMakeLists.txt b/flutter_local_notifications/example/linux/CMakeLists.txt new file mode 100644 index 000000000..afbac9a84 --- /dev/null +++ b/flutter_local_notifications/example/linux/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "flutter_local_notifications_example") +set(APPLICATION_ID "com.dexterous.flutter_local_notifications") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/flutter_local_notifications/example/linux/flutter/CMakeLists.txt b/flutter_local_notifications/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..33fd5801e --- /dev/null +++ b/flutter_local_notifications/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/flutter_local_notifications/example/linux/flutter/generated_plugin_registrant.cc b/flutter_local_notifications/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..d38195aa0 --- /dev/null +++ b/flutter_local_notifications/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,9 @@ +// +// Generated file. Do not edit. +// + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/flutter_local_notifications/example/linux/flutter/generated_plugin_registrant.h b/flutter_local_notifications/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..9bf747894 --- /dev/null +++ b/flutter_local_notifications/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter_local_notifications/example/linux/flutter/generated_plugins.cmake b/flutter_local_notifications/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..51436ae8c --- /dev/null +++ b/flutter_local_notifications/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/flutter_local_notifications/example/linux/main.cc b/flutter_local_notifications/example/linux/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/flutter_local_notifications/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/flutter_local_notifications/example/linux/my_application.cc b/flutter_local_notifications/example/linux/my_application.cc new file mode 100644 index 000000000..d9775062c --- /dev/null +++ b/flutter_local_notifications/example/linux/my_application.cc @@ -0,0 +1,105 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_local_notifications_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } + else { + gtk_window_set_title(window, "flutter_local_notifications_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar ***arguments, int *exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject *object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/flutter_local_notifications/example/linux/my_application.h b/flutter_local_notifications/example/linux/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/flutter_local_notifications/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/flutter_local_notifications/example/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter_local_notifications/example/macos/Flutter/GeneratedPluginRegistrant.swift index 7e7d18d10..3b8574d1d 100644 --- a/flutter_local_notifications/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter_local_notifications/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,13 @@ import FlutterMacOS import Foundation import flutter_local_notifications +import flutter_native_timezone import path_provider_macos import shared_preferences_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FlutterNativeTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterNativeTimezonePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/flutter_local_notifications/example/macos/Runner.xcodeproj/project.pbxproj b/flutter_local_notifications/example/macos/Runner.xcodeproj/project.pbxproj index fd824535a..b7a623f26 100644 --- a/flutter_local_notifications/example/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter_local_notifications/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,12 +26,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 5153E5A3247A426900B646BC /* slow_spring_board.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 5153E5A2247A426900B646BC /* slow_spring_board.aiff */; }; - 6B54059AE2FB4C90D9D99DAC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 762D02E73EA6ABDB0FAFD091 /* Pods_Runner.framework */; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 51644AFE25B3BDAD00A1F97B /* slow_spring_board.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 51644AFD25B3BDAD00A1F97B /* slow_spring_board.aiff */; }; + 6DEC1E4EF7EB33B65651F050 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF459FAC079661014F754FE6 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,8 +47,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -60,6 +54,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 13FC150C898181695ADDEFFB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* flutter_local_notifications_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_local_notifications_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -71,18 +66,15 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 5153E5A2247A426900B646BC /* slow_spring_board.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = slow_spring_board.aiff; sourceTree = ""; }; - 615743662875AFFEF6D07C7A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 64D6E94E2E5B847AAB992EF7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 762D02E73EA6ABDB0FAFD091 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4A046A0F9B68C79AE0BC8AEB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 51644AFD25B3BDAD00A1F97B /* slow_spring_board.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = slow_spring_board.aiff; sourceTree = SOURCE_ROOT; }; + 5E9C0662C4DB5505A8130C5C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; - EBF76761195F0D831D5259D6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + AF459FAC079661014F754FE6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,9 +82,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - 6B54059AE2FB4C90D9D99DAC /* Pods_Runner.framework in Frameworks */, + 6DEC1E4EF7EB33B65651F050 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -117,7 +107,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 4E06C161AA1A5BA26DB88724 /* Pods */, + 45C6A21E4A86F545DE32BA57 /* Pods */, ); sourceTree = ""; }; @@ -147,8 +137,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -156,23 +144,23 @@ 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( - 5153E5A2247A426900B646BC /* slow_spring_board.aiff */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, + 51644AFD25B3BDAD00A1F97B /* slow_spring_board.aiff */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; - 4E06C161AA1A5BA26DB88724 /* Pods */ = { + 45C6A21E4A86F545DE32BA57 /* Pods */ = { isa = PBXGroup; children = ( - EBF76761195F0D831D5259D6 /* Pods-Runner.debug.xcconfig */, - 615743662875AFFEF6D07C7A /* Pods-Runner.release.xcconfig */, - 64D6E94E2E5B847AAB992EF7 /* Pods-Runner.profile.xcconfig */, + 5E9C0662C4DB5505A8130C5C /* Pods-Runner.debug.xcconfig */, + 13FC150C898181695ADDEFFB /* Pods-Runner.release.xcconfig */, + 4A046A0F9B68C79AE0BC8AEB /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -180,7 +168,7 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 762D02E73EA6ABDB0FAFD091 /* Pods_Runner.framework */, + AF459FAC079661014F754FE6 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -192,13 +180,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - D6F9983C62DED65726A9AFE4 /* [CP] Check Pods Manifest.lock */, + CB7C7AA6F6C20C41B181EB5D /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 6CF284EFDB7F66625505BC1E /* [CP] Embed Pods Frameworks */, + FF9D5ADF2F93716F0052AA35 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -218,7 +206,7 @@ attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "The Flutter Authors"; + ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; @@ -237,7 +225,7 @@ }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -260,7 +248,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5153E5A3247A426900B646BC /* slow_spring_board.aiff in Resources */, + 51644AFE25B3BDAD00A1F97B /* slow_spring_board.aiff in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); @@ -284,7 +272,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -304,43 +292,45 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 6CF284EFDB7F66625505BC1E /* [CP] Embed Pods Frameworks */ = { + CB7C7AA6F6C20C41B181EB5D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - D6F9983C62DED65726A9AFE4 /* [CP] Check Pods Manifest.lock */ = { + FF9D5ADF2F93716F0052AA35 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -434,10 +424,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -564,10 +550,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -588,10 +570,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/flutter_local_notifications/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter_local_notifications/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 03aefde57..f64829ccb 100644 --- a/flutter_local_notifications/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_local_notifications/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,18 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - Void in - if ("getTimeZoneName" == call.method) { - result(TimeZone.current.identifier) - } - }) + RegisterGeneratedPlugins(registry: flutterViewController) - + super.awakeFromNib() } } diff --git a/flutter_local_notifications/example/macos/Runner/Release.entitlements b/flutter_local_notifications/example/macos/Runner/Release.entitlements index ee95ab7e5..852fa1a47 100644 --- a/flutter_local_notifications/example/macos/Runner/Release.entitlements +++ b/flutter_local_notifications/example/macos/Runner/Release.entitlements @@ -4,7 +4,5 @@ com.apple.security.app-sandbox - com.apple.security.network.client - diff --git a/flutter_local_notifications/example/macos/Runner/slow_spring_board.aiff b/flutter_local_notifications/example/macos/slow_spring_board.aiff similarity index 100% rename from flutter_local_notifications/example/macos/Runner/slow_spring_board.aiff rename to flutter_local_notifications/example/macos/slow_spring_board.aiff diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index cdaf1df7c..b90e57864 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -3,29 +3,37 @@ description: Demonstrates how to use the flutter_local_notifications plugin. publish_to: none dependencies: + cupertino_icons: ^1.0.2 + device_info: ^2.0.2 flutter: sdk: flutter - cupertino_icons: ^0.1.3 - device_info: ^0.4.2+4 - http: ^0.12.0+4 - path_provider: ^1.6.7 - shared_preferences: ^0.5.7 - rxdart: ^0.24.0 flutter_local_notifications: path: ../ + flutter_local_notifications_platform_interface: + path: ../../flutter_local_notifications_platform_interface/ + flutter_local_notifications_linux: + path: ../../flutter_local_notifications_linux/ + flutter_native_timezone: ^2.0.0 + http: ^0.13.4 + image: ^3.0.8 + path_provider: ^2.0.0 + rxdart: ^0.27.2 + shared_preferences: ^2.0.1 dev_dependencies: flutter_driver: sdk: flutter flutter_test: sdk: flutter - e2e: ^0.4.3+1 + integration_test: + sdk: flutter flutter: uses-material-design: true assets: - icons/ + - sound/ environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" + sdk: '>=2.12.0-0 <3.0.0' + flutter: '>=1.26.0-0' # allows for using integration_test from SDK diff --git a/flutter_local_notifications/example/sound/slow_spring_board.mp3 b/flutter_local_notifications/example/sound/slow_spring_board.mp3 new file mode 100644 index 000000000..60dbf9794 Binary files /dev/null and b/flutter_local_notifications/example/sound/slow_spring_board.mp3 differ diff --git a/flutter_local_notifications/example/test_driver/flutter_local_notifications_e2e_test.dart b/flutter_local_notifications/example/test_driver/flutter_local_notifications_e2e_test.dart deleted file mode 100644 index 983c3863d..000000000 --- a/flutter_local_notifications/example/test_driver/flutter_local_notifications_e2e_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'dart:async'; - -import 'package:e2e/e2e_driver.dart' as e2e; - -Future main() async => e2e.main(); diff --git a/flutter_local_notifications/example/test_driver/integration_test.dart b/flutter_local_notifications/example/test_driver/integration_test.dart new file mode 100644 index 000000000..ccc4438b1 --- /dev/null +++ b/flutter_local_notifications/example/test_driver/integration_test.dart @@ -0,0 +1,4 @@ +// @dart = 2.9 +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/flutter_local_notifications/ios/Classes/ActionEventSink.h b/flutter_local_notifications/ios/Classes/ActionEventSink.h index 80341bbdc..93938560c 100644 --- a/flutter_local_notifications/ios/Classes/ActionEventSink.h +++ b/flutter_local_notifications/ios/Classes/ActionEventSink.h @@ -11,9 +11,9 @@ NS_ASSUME_NONNULL_BEGIN -@interface ActionEventSink : NSObject +@interface ActionEventSink : NSObject -- (void)addItem:(NSDictionary*)item; +- (void)addItem:(NSDictionary *)item; @end diff --git a/flutter_local_notifications/ios/Classes/ActionEventSink.m b/flutter_local_notifications/ios/Classes/ActionEventSink.m index 57353bd4f..228bb32d3 100644 --- a/flutter_local_notifications/ios/Classes/ActionEventSink.m +++ b/flutter_local_notifications/ios/Classes/ActionEventSink.m @@ -8,48 +8,47 @@ #import "ActionEventSink.h" @interface ActionEventSink () { - NSMutableArray* cache; - FlutterEventSink eventSink; + NSMutableArray *cache; + FlutterEventSink eventSink; } @end @implementation ActionEventSink -- (instancetype)init -{ - self = [super init]; - if (self) { - cache = [NSMutableArray array]; - } - return self; +- (instancetype)init { + self = [super init]; + if (self) { + cache = [NSMutableArray array]; + } + return self; } - (void)addItem:(NSDictionary *)item { - if (eventSink) { - eventSink(item); - } else { - [cache addObject:item]; - } + if (eventSink) { + eventSink(item); + } else { + [cache addObject:item]; + } } -- (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments - eventSink:(nonnull FlutterEventSink)events { - for (NSDictionary* item in cache) { - events(item); - } - [cache removeAllObjects]; - - eventSink = events; - - return nil; -} +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink: + (nonnull FlutterEventSink)events { + for (NSDictionary *item in cache) { + events(item); + } + [cache removeAllObjects]; + + eventSink = events; -- (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { - eventSink = nil; - - return nil; + return nil; } +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + eventSink = nil; + + return nil; +} @end diff --git a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.h b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.h index de0c9e64a..a8ab2546d 100644 --- a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.h +++ b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.h @@ -2,5 +2,5 @@ #import @interface FlutterLocalNotificationsPlugin : NSObject -//+ (void)setRegisterPlugins:(FlutterPluginRegistrantCallback*)callback; ++ (void)setRegisterPlugins:(FlutterPluginRegistrantCallback *)callback; @end diff --git a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m index 0bea2d887..5ef64cc13 100644 --- a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m @@ -2,22 +2,22 @@ #import "FlutterLocalNotificationsPlugin.h" -@implementation FlutterLocalNotificationsPlugin{ - FlutterMethodChannel* _channel; - bool _displayAlert; - bool _playSound; - bool _updateBadge; - bool _initialized; - bool _launchingAppFromNotification; - NSUserDefaults *_persistentState; - NSObject *_registrar; - NSString *_launchPayload; - UILocalNotification *_launchNotification; -} - -static FlutterEngine* backgroundEngine; +@implementation FlutterLocalNotificationsPlugin { + FlutterMethodChannel *_channel; + bool _displayAlert; + bool _playSound; + bool _updateBadge; + bool _initialized; + bool _launchingAppFromNotification; + NSUserDefaults *_persistentState; + NSObject *_registrar; + NSString *_launchPayload; + UILocalNotification *_launchNotification; +} + +static FlutterEngine *backgroundEngine; static FlutterPluginRegistrantCallback registerPlugins; -static ActionEventSink* actionEventSink; +static ActionEventSink *actionEventSink; NSString *const INITIALIZE_METHOD = @"initialize"; NSString *const GET_CALLBACK_METHOD = @"getCallbackHandle"; @@ -29,10 +29,13 @@ @implementation FlutterLocalNotificationsPlugin{ NSString *const SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD = @"showWeeklyAtDayAndTime"; NSString *const CANCEL_METHOD = @"cancel"; NSString *const CANCEL_ALL_METHOD = @"cancelAll"; -NSString *const PENDING_NOTIFICATIONS_REQUESTS_METHOD = @"pendingNotificationRequests"; -NSString *const GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD = @"getNotificationAppLaunchDetails"; +NSString *const PENDING_NOTIFICATIONS_REQUESTS_METHOD = + @"pendingNotificationRequests"; +NSString *const GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD = + @"getNotificationAppLaunchDetails"; NSString *const CHANNEL = @"dexterous.com/flutter/local_notifications"; -NSString *const CALLBACK_CHANNEL = @"dexterous.com/flutter/local_notifications_background"; +NSString *const CALLBACK_CHANNEL = + @"dexterous.com/flutter/local_notifications_background"; NSString *const ON_NOTIFICATION_METHOD = @"onNotification"; NSString *const DID_RECEIVE_LOCAL_NOTIFICATION = @"didReceiveLocalNotification"; NSString *const REQUEST_PERMISSIONS_METHOD = @"requestPermissions"; @@ -49,7 +52,8 @@ @implementation FlutterLocalNotificationsPlugin{ NSString *const DEFAULT_PRESENT_SOUND = @"defaultPresentSound"; NSString *const DEFAULT_PRESENT_BADGE = @"defaultPresentBadge"; NSString *const CALLBACK_DISPATCHER = @"callbackDispatcher"; -NSString *const ON_NOTIFICATION_CALLBACK_DISPATCHER = @"onNotificationCallbackDispatcher"; +NSString *const ON_NOTIFICATION_CALLBACK_DISPATCHER = + @"onNotificationCallbackDispatcher"; NSString *const PLATFORM_SPECIFICS = @"platformSpecifics"; NSString *const ID = @"id"; NSString *const TITLE = @"title"; @@ -59,6 +63,7 @@ @implementation FlutterLocalNotificationsPlugin{ NSString *const ATTACHMENTS = @"attachments"; NSString *const ATTACHMENT_IDENTIFIER = @"identifier"; NSString *const ATTACHMENT_FILE_PATH = @"filePath"; +NSString *const THREAD_IDENTIFIER = @"threadIdentifier"; NSString *const PRESENT_ALERT = @"presentAlert"; NSString *const PRESENT_SOUND = @"presentSound"; NSString *const PRESENT_BADGE = @"presentBadge"; @@ -72,881 +77,1070 @@ @implementation FlutterLocalNotificationsPlugin{ NSString *const SCHEDULED_DATE_TIME = @"scheduledDateTime"; NSString *const TIME_ZONE_NAME = @"timeZoneName"; NSString *const MATCH_DATE_TIME_COMPONENTS = @"matchDateTimeComponents"; -NSString *const UILOCALNOTIFICATION_DATE_INTERPRETATION = @"uiLocalNotificationDateInterpretation"; +NSString *const UILOCALNOTIFICATION_DATE_INTERPRETATION = + @"uiLocalNotificationDateInterpretation"; NSString *const NOTIFICATION_ID = @"NotificationId"; NSString *const PAYLOAD = @"payload"; NSString *const NOTIFICATION_LAUNCHED_APP = @"notificationLaunchedApp"; - typedef NS_ENUM(NSInteger, RepeatInterval) { - EveryMinute, - Hourly, - Daily, - Weekly + EveryMinute, + Hourly, + Daily, + Weekly }; typedef NS_ENUM(NSInteger, DateTimeComponents) { - Time, - DayOfWeekAndTime + Time, + DayOfWeekAndTime, + DayOfMonthAndTime, + DateAndTime }; typedef NS_ENUM(NSInteger, UILocalNotificationDateInterpretation) { - AbsoluteGMTTime, - WallClockTime + AbsoluteGMTTime, + WallClockTime }; static FlutterError *getFlutterError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; + return [FlutterError + errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] + message:error.localizedDescription + details:error.domain]; } -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel *channel = [FlutterMethodChannel - methodChannelWithName:CHANNEL - binaryMessenger:[registrar messenger]]; - - FlutterLocalNotificationsPlugin* instance = [[FlutterLocalNotificationsPlugin alloc] initWithChannel:channel registrar:registrar]; - if (backgroundEngine == nil || registrar.messenger != backgroundEngine.binaryMessenger) { - [registrar addApplicationDelegate:instance]; - } - - [registrar addMethodCallDelegate:instance channel:channel]; ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:CHANNEL + binaryMessenger:[registrar messenger]]; + + FlutterLocalNotificationsPlugin *instance = + [[FlutterLocalNotificationsPlugin alloc] initWithChannel:channel + registrar:registrar]; + if (backgroundEngine == nil || + registrar.messenger != backgroundEngine.binaryMessenger) { + [registrar addApplicationDelegate:instance]; + } + + [registrar addMethodCallDelegate:instance channel:channel]; } + (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { registerPlugins = callback; } -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel registrar:(NSObject *)registrar { - self = [super init]; - - if (self) { - _channel = channel; - _registrar = registrar; - _persistentState = [NSUserDefaults standardUserDefaults]; - } - - return self; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if([INITIALIZE_METHOD isEqualToString:call.method]) { - [self initialize:call.arguments result:result]; - } else if([GET_CALLBACK_METHOD isEqualToString:call.method]) { - NSNumber* handle = [_persistentState valueForKey:@"callback_handle"]; - result(handle); - } else if([SHOW_METHOD isEqualToString:call.method]) { - [self show:call.arguments result:result]; - } else if([ZONED_SCHEDULE_METHOD isEqualToString:call.method]) { - [self zonedSchedule:call.arguments result:result]; - } else if([SCHEDULE_METHOD isEqualToString:call.method]) { - [self schedule:call.arguments result:result]; - } else if([PERIODICALLY_SHOW_METHOD isEqualToString:call.method]) { - [self periodicallyShow:call.arguments result:result]; - } else if([SHOW_DAILY_AT_TIME_METHOD isEqualToString:call.method]) { - [self showDailyAtTime:call.arguments result:result]; - } else if([SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD isEqualToString:call.method]) { - [self showWeeklyAtDayAndTime:call.arguments result:result]; - } else if([REQUEST_PERMISSIONS_METHOD isEqualToString:call.method]) { - [self requestPermissions:call.arguments result:result]; - } else if([CANCEL_METHOD isEqualToString:call.method]) { - [self cancel:((NSNumber *)call.arguments) result:result]; - } else if([CANCEL_ALL_METHOD isEqualToString:call.method]) { - [self cancelAll:result]; - } else if([GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD isEqualToString:call.method]) { - NSString *payload; - if(_launchNotification != nil) { - payload = _launchNotification.userInfo[PAYLOAD]; - } else { - payload = _launchPayload; - } - NSDictionary *notificationAppLaunchDetails = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:_launchingAppFromNotification], NOTIFICATION_LAUNCHED_APP, payload, PAYLOAD, nil]; - result(notificationAppLaunchDetails); - } else if([PENDING_NOTIFICATIONS_REQUESTS_METHOD isEqualToString:call.method]) { - [self pendingNotificationRequests:result]; - } - else { - result(FlutterMethodNotImplemented); - } -} - -- (void)pendingUserNotificationRequests:(FlutterResult _Nonnull)result NS_AVAILABLE_IOS(10.0) { - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { - NSMutableArray *> *pendingNotificationRequests = [[NSMutableArray alloc] initWithCapacity:[requests count]]; - for (UNNotificationRequest *request in requests) { - NSMutableDictionary *pendingNotificationRequest = [[NSMutableDictionary alloc] init]; - pendingNotificationRequest[ID] = request.content.userInfo[NOTIFICATION_ID]; - if (request.content.title != nil) { - pendingNotificationRequest[TITLE] = request.content.title; - } - if (request.content.body != nil) { - pendingNotificationRequest[BODY] = request.content.body; - } - if (request.content.userInfo[PAYLOAD] != [NSNull null]) { - pendingNotificationRequest[PAYLOAD] = request.content.userInfo[PAYLOAD]; - } - [pendingNotificationRequests addObject:pendingNotificationRequest]; - } - result(pendingNotificationRequests); - }]; +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel + registrar:(NSObject *)registrar { + self = [super init]; + + if (self) { + _channel = channel; + _registrar = registrar; + _persistentState = [NSUserDefaults standardUserDefaults]; + } + + return self; } -- (void)pendingLocalNotificationRequests:(FlutterResult _Nonnull)result { - NSArray *notifications = [UIApplication sharedApplication].scheduledLocalNotifications; - NSMutableArray *> *pendingNotificationRequests = [[NSMutableArray alloc] initWithCapacity:[notifications count]]; - for( int i = 0; i < [notifications count]; i++) { - UILocalNotification* localNotification = [notifications objectAtIndex:i]; - NSMutableDictionary *pendingNotificationRequest = [[NSMutableDictionary alloc] init]; - pendingNotificationRequest[ID] = localNotification.userInfo[NOTIFICATION_ID]; - if (localNotification.userInfo[TITLE] != [NSNull null]) { - pendingNotificationRequest[TITLE] = localNotification.userInfo[TITLE]; - } - if (localNotification.alertBody) { - pendingNotificationRequest[BODY] = localNotification.alertBody; - } - if (localNotification.userInfo[PAYLOAD] != [NSNull null]) { - pendingNotificationRequest[PAYLOAD] = localNotification.userInfo[PAYLOAD]; - } - [pendingNotificationRequests addObject:pendingNotificationRequest]; +- (void)handleMethodCall:(FlutterMethodCall *)call + result:(FlutterResult)result { + if ([INITIALIZE_METHOD isEqualToString:call.method]) { + [self initialize:call.arguments result:result]; + } else if ([GET_CALLBACK_METHOD isEqualToString:call.method]) { + NSNumber *handle = [_persistentState valueForKey:@"callback_handle"]; + result(handle); + } else if ([SHOW_METHOD isEqualToString:call.method]) { + + [self show:call.arguments result:result]; + } else if ([ZONED_SCHEDULE_METHOD isEqualToString:call.method]) { + [self zonedSchedule:call.arguments result:result]; + } else if ([SCHEDULE_METHOD isEqualToString:call.method]) { + [self schedule:call.arguments result:result]; + } else if ([PERIODICALLY_SHOW_METHOD isEqualToString:call.method]) { + [self periodicallyShow:call.arguments result:result]; + } else if ([SHOW_DAILY_AT_TIME_METHOD isEqualToString:call.method]) { + [self showDailyAtTime:call.arguments result:result]; + } else if ([SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD isEqualToString:call.method]) { + [self showWeeklyAtDayAndTime:call.arguments result:result]; + } else if ([REQUEST_PERMISSIONS_METHOD isEqualToString:call.method]) { + [self requestPermissions:call.arguments result:result]; + } else if ([CANCEL_METHOD isEqualToString:call.method]) { + [self cancel:((NSNumber *)call.arguments) result:result]; + } else if ([CANCEL_ALL_METHOD isEqualToString:call.method]) { + [self cancelAll:result]; + } else if ([GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD + isEqualToString:call.method]) { + NSString *payload; + if (_launchNotification != nil) { + payload = _launchNotification.userInfo[PAYLOAD]; + } else { + payload = _launchPayload; + } + NSDictionary *notificationAppLaunchDetails = [NSDictionary + dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:_launchingAppFromNotification], + NOTIFICATION_LAUNCHED_APP, payload, PAYLOAD, nil]; + result(notificationAppLaunchDetails); + } else if ([PENDING_NOTIFICATIONS_REQUESTS_METHOD + isEqualToString:call.method]) { + [self pendingNotificationRequests:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)pendingUserNotificationRequests:(FlutterResult _Nonnull)result + NS_AVAILABLE_IOS(10.0) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center getPendingNotificationRequestsWithCompletionHandler:^( + NSArray *_Nonnull requests) { + NSMutableArray *> + *pendingNotificationRequests = + [[NSMutableArray alloc] initWithCapacity:[requests count]]; + for (UNNotificationRequest *request in requests) { + NSMutableDictionary *pendingNotificationRequest = + [[NSMutableDictionary alloc] init]; + pendingNotificationRequest[ID] = + request.content.userInfo[NOTIFICATION_ID]; + if (request.content.title != nil) { + pendingNotificationRequest[TITLE] = request.content.title; + } + if (request.content.body != nil) { + pendingNotificationRequest[BODY] = request.content.body; + } + if (request.content.userInfo[PAYLOAD] != [NSNull null]) { + pendingNotificationRequest[PAYLOAD] = request.content.userInfo[PAYLOAD]; + } + [pendingNotificationRequests addObject:pendingNotificationRequest]; } result(pendingNotificationRequests); + }]; +} + +- (void)pendingLocalNotificationRequests:(FlutterResult _Nonnull)result { + NSArray *notifications = + [UIApplication sharedApplication].scheduledLocalNotifications; + NSMutableArray *> + *pendingNotificationRequests = + [[NSMutableArray alloc] initWithCapacity:[notifications count]]; + for (int i = 0; i < [notifications count]; i++) { + UILocalNotification *localNotification = [notifications objectAtIndex:i]; + NSMutableDictionary *pendingNotificationRequest = + [[NSMutableDictionary alloc] init]; + pendingNotificationRequest[ID] = + localNotification.userInfo[NOTIFICATION_ID]; + if (localNotification.userInfo[TITLE] != [NSNull null]) { + pendingNotificationRequest[TITLE] = localNotification.userInfo[TITLE]; + } + if (localNotification.alertBody) { + pendingNotificationRequest[BODY] = localNotification.alertBody; + } + if (localNotification.userInfo[PAYLOAD] != [NSNull null]) { + pendingNotificationRequest[PAYLOAD] = localNotification.userInfo[PAYLOAD]; + } + [pendingNotificationRequests addObject:pendingNotificationRequest]; + } + result(pendingNotificationRequests); } - (void)pendingNotificationRequests:(FlutterResult _Nonnull)result { - if(@available(iOS 10.0, *)) { - [self pendingUserNotificationRequests:result]; - } else { - [self pendingLocalNotificationRequests:result]; - }} + if (@available(iOS 10.0, *)) { + [self pendingUserNotificationRequests:result]; + } else { + [self pendingLocalNotificationRequests:result]; + } +} -- (UNNotificationCategoryOptions)parseNotificationCategoryOptions:(NSArray*)options API_AVAILABLE(ios(10.0)){ - int result = UNNotificationCategoryOptionNone; - - for (NSNumber* option in options) { - result |= [option intValue]; - } - - return result; +- (UNNotificationCategoryOptions)parseNotificationCategoryOptions: + (NSArray *)options API_AVAILABLE(ios(10.0)) { + int result = UNNotificationCategoryOptionNone; + + for (NSNumber *option in options) { + result |= [option intValue]; + } + + return result; } -- (UNNotificationActionOptions)parseNotificationActionOptions:(NSArray*)options API_AVAILABLE(ios(10.0)){ - int result = UNNotificationActionOptionNone; - - for (NSNumber* option in options) { - result |= [option intValue]; - } - - return result; +- (UNNotificationActionOptions)parseNotificationActionOptions:(NSArray *)options + API_AVAILABLE(ios(10.0)) { + int result = UNNotificationActionOptionNone; + + for (NSNumber *option in options) { + result |= [option intValue]; + } + + return result; } -/// Extracts notification categories from [arguments] and configures them as appropriate. +/// Extracts notification categories from [arguments] and configures them as +/// appropriate. /// -/// This code will simply return the `completionHandler` if not running on a compatible iOS version or when no -/// categories were specified in [arguments]. -- (void)configureNotificationCategories:(NSDictionary * _Nonnull)arguments - completionHandler:(void (^)(void))completionHandler { - if (@available(iOS 10.0, *)) { - if ([self containsKey:@"notificationCategories" forDictionary:arguments]) { - NSMutableSet* newCategories = [NSMutableSet set]; - - NSArray* categories = arguments[@"notificationCategories"]; - - for (NSDictionary *category in categories) { - NSMutableArray* newActions = [NSMutableArray array]; - - NSArray* actions = category[@"actions"]; - for (NSDictionary *action in actions) { - NSString *type = action[@"type"]; - NSString *identifier = action[@"identifier"]; - NSString *title = action[@"title"]; - UNNotificationActionOptions options = [self parseNotificationActionOptions:action[@"options"]]; - - if ([type isEqualToString:@"plain"]) { - [newActions addObject:[UNNotificationAction actionWithIdentifier:identifier - title:title - options:options]]; - } else if ([type isEqualToString:@"text"]) { - NSString *buttonTitle = action[@"buttonTitle"]; - NSString *placeholder = action[@"placeholder"]; - [newActions addObject:[UNTextInputNotificationAction actionWithIdentifier:identifier - title:title - options:options - textInputButtonTitle:buttonTitle - textInputPlaceholder:placeholder]]; - } - } - - UNNotificationCategory *newCategory = [UNNotificationCategory categoryWithIdentifier:category[@"identifier"] - actions:newActions - intentIdentifiers:@[] - options:[self parseNotificationCategoryOptions:category[@"options"]]]; - - [newCategories addObject:newCategory]; - } - - if (newCategories.count > 0) { - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - [center getNotificationCategoriesWithCompletionHandler:^(NSSet * _Nonnull existing) { - [center setNotificationCategories:[existing setByAddingObjectsFromSet:newCategories]]; - - completionHandler(); - }]; - } else { - completionHandler(); - } +/// This code will simply return the `completionHandler` if not running on a +/// compatible iOS version or when no categories were specified in [arguments]. +- (void)configureNotificationCategories:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + if ([self containsKey:@"notificationCategories" forDictionary:arguments]) { + NSMutableSet *newCategories = + [NSMutableSet set]; + + NSArray *categories = arguments[@"notificationCategories"]; + + for (NSDictionary *category in categories) { + NSMutableArray *newActions = + [NSMutableArray array]; + + NSArray *actions = category[@"actions"]; + for (NSDictionary *action in actions) { + NSString *type = action[@"type"]; + NSString *identifier = action[@"identifier"]; + NSString *title = action[@"title"]; + UNNotificationActionOptions options = + [self parseNotificationActionOptions:action[@"options"]]; + + if ([type isEqualToString:@"plain"]) { + [newActions + addObject:[UNNotificationAction actionWithIdentifier:identifier + title:title + options:options]]; + } else if ([type isEqualToString:@"text"]) { + NSString *buttonTitle = action[@"buttonTitle"]; + NSString *placeholder = action[@"placeholder"]; + [newActions addObject:[UNTextInputNotificationAction + actionWithIdentifier:identifier + title:title + options:options + textInputButtonTitle:buttonTitle + textInputPlaceholder:placeholder]]; + } } - } else { - completionHandler(); - } -} - -- (void)initialize:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - if([self containsKey:DEFAULT_PRESENT_ALERT forDictionary:arguments]) { - _displayAlert = [[arguments objectForKey:DEFAULT_PRESENT_ALERT] boolValue]; - } - if([self containsKey:DEFAULT_PRESENT_SOUND forDictionary:arguments]) { - _playSound = [[arguments objectForKey:DEFAULT_PRESENT_SOUND] boolValue]; - } - if([self containsKey:DEFAULT_PRESENT_BADGE forDictionary:arguments]) { - _updateBadge = [[arguments objectForKey:DEFAULT_PRESENT_BADGE] boolValue]; - } - bool requestedSoundPermission = false; - bool requestedAlertPermission = false; - bool requestedBadgePermission = false; - if([self containsKey:REQUEST_SOUND_PERMISSION forDictionary:arguments]) { - requestedSoundPermission = [arguments[REQUEST_SOUND_PERMISSION] boolValue]; - } - if([self containsKey:REQUEST_ALERT_PERMISSION forDictionary:arguments]) { - requestedAlertPermission = [arguments[REQUEST_ALERT_PERMISSION] boolValue]; - } - if([self containsKey:REQUEST_BADGE_PERMISSION forDictionary:arguments]) { - requestedBadgePermission = [arguments[REQUEST_BADGE_PERMISSION] boolValue]; - } - - if([self containsKey:@"dispatcher_handle" forDictionary:arguments] && - [self containsKey:@"callback_handle" forDictionary:arguments]) { - [_persistentState setObject:arguments[@"callback_handle"] forKey:@"callback_handle"]; - [_persistentState setObject:arguments[@"dispatcher_handle"] forKey:@"dispatcher_handle"]; - } - - // Configure the notification categories before requesting permissions - [self configureNotificationCategories:arguments - completionHandler:^{ - - // Once notification categories are set up, the permissions request will pick them up properly. - [self requestPermissionsImpl:requestedSoundPermission - alertPermission:requestedAlertPermission - badgePermission:requestedBadgePermission - checkLaunchNotification:true - completionHandler:^(NSNumber* granted) { - result(granted); + + UNNotificationCategory *newCategory = [UNNotificationCategory + categoryWithIdentifier:category[@"identifier"] + actions:newActions + intentIdentifiers:@[] + options:[self parseNotificationCategoryOptions: + category[@"options"]]]; + + [newCategories addObject:newCategory]; + } + + if (newCategories.count > 0) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center getNotificationCategoriesWithCompletionHandler:^( + NSSet *_Nonnull existing) { + [center setNotificationCategories: + [existing setByAddingObjectsFromSet:newCategories]]; + + result(@YES); }]; - }]; - - - _initialized = true; + } else { + result(@YES); + } + } + } else { + result(@YES); + } } +- (void)initialize:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + if ([self containsKey:DEFAULT_PRESENT_ALERT forDictionary:arguments]) { + _displayAlert = [[arguments objectForKey:DEFAULT_PRESENT_ALERT] boolValue]; + } + if ([self containsKey:DEFAULT_PRESENT_SOUND forDictionary:arguments]) { + _playSound = [[arguments objectForKey:DEFAULT_PRESENT_SOUND] boolValue]; + } + if ([self containsKey:DEFAULT_PRESENT_BADGE forDictionary:arguments]) { + _updateBadge = [[arguments objectForKey:DEFAULT_PRESENT_BADGE] boolValue]; + } + bool requestedSoundPermission = false; + bool requestedAlertPermission = false; + bool requestedBadgePermission = false; + if ([self containsKey:REQUEST_SOUND_PERMISSION forDictionary:arguments]) { + requestedSoundPermission = [arguments[REQUEST_SOUND_PERMISSION] boolValue]; + } + if ([self containsKey:REQUEST_ALERT_PERMISSION forDictionary:arguments]) { + requestedAlertPermission = [arguments[REQUEST_ALERT_PERMISSION] boolValue]; + } + if ([self containsKey:REQUEST_BADGE_PERMISSION forDictionary:arguments]) { + requestedBadgePermission = [arguments[REQUEST_BADGE_PERMISSION] boolValue]; + } -- (void)requestPermissions:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - bool soundPermission = false; - bool alertPermission = false; - bool badgePermission = false; - if([self containsKey:SOUND_PERMISSION forDictionary:arguments]) { - soundPermission = [arguments[SOUND_PERMISSION] boolValue]; - } - if([self containsKey:ALERT_PERMISSION forDictionary:arguments]) { - alertPermission = [arguments[ALERT_PERMISSION] boolValue]; - } - if([self containsKey:BADGE_PERMISSION forDictionary:arguments]) { - badgePermission = [arguments[BADGE_PERMISSION] boolValue]; - } - [self requestPermissionsImpl:soundPermission alertPermission:alertPermission badgePermission:badgePermission checkLaunchNotification:false completionHandler:^(NSNumber* granted) { - result(granted); - }]; + if ([self containsKey:@"dispatcher_handle" forDictionary:arguments] && + [self containsKey:@"callback_handle" forDictionary:arguments]) { + [_persistentState setObject:arguments[@"callback_handle"] + forKey:@"callback_handle"]; + [_persistentState setObject:arguments[@"dispatcher_handle"] + forKey:@"dispatcher_handle"]; + } + + // Configure the notification categories before requesting permissions + [self configureNotificationCategories:arguments result:result]; + + // Once notification categories are set up, the permissions request will pick + // them up properly. + [self requestPermissionsImpl:requestedSoundPermission + alertPermission:requestedAlertPermission + badgePermission:requestedBadgePermission + result:result]; + + _initialized = true; +} +- (void)requestPermissions:(NSDictionary *_Nonnull)arguments + + result:(FlutterResult _Nonnull)result { + bool soundPermission = false; + bool alertPermission = false; + bool badgePermission = false; + if ([self containsKey:SOUND_PERMISSION forDictionary:arguments]) { + soundPermission = [arguments[SOUND_PERMISSION] boolValue]; + } + if ([self containsKey:ALERT_PERMISSION forDictionary:arguments]) { + alertPermission = [arguments[ALERT_PERMISSION] boolValue]; + } + if ([self containsKey:BADGE_PERMISSION forDictionary:arguments]) { + badgePermission = [arguments[BADGE_PERMISSION] boolValue]; + } + [self requestPermissionsImpl:soundPermission + alertPermission:alertPermission + badgePermission:badgePermission + result:result]; } - (void)requestPermissionsImpl:(bool)soundPermission alertPermission:(bool)alertPermission badgePermission:(bool)badgePermission - checkLaunchNotification:(bool)checkLaunchNotification - completionHandler:(void (^)(NSNumber *))completionHandler { - if(@available(iOS 10.0, *)) { - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - - UNAuthorizationOptions authorizationOptions = 0; - if (soundPermission) { - authorizationOptions += UNAuthorizationOptionSound; - } - if (alertPermission) { - authorizationOptions += UNAuthorizationOptionAlert; - } - if (badgePermission) { - authorizationOptions += UNAuthorizationOptionBadge; - } - [center requestAuthorizationWithOptions:(authorizationOptions) completionHandler:^(BOOL granted, NSError * _Nullable error) { - if(checkLaunchNotification && self->_launchPayload != nil) { - [self handleSelectNotification:self->_launchPayload]; - } - completionHandler(@(granted)); - }]; - } else { - UIUserNotificationType notificationTypes = 0; - if (soundPermission) { - notificationTypes |= UIUserNotificationTypeSound; - } - if (alertPermission) { - notificationTypes |= UIUserNotificationTypeAlert; - } - if (badgePermission) { - notificationTypes |= UIUserNotificationTypeBadge; - } - UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:notificationTypes categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; - if(checkLaunchNotification && _launchNotification != nil && [self isAFlutterLocalNotification:_launchNotification.userInfo]) { - NSString *payload = _launchNotification.userInfo[PAYLOAD]; - [self handleSelectNotification:payload]; - } - completionHandler(@YES); + result:(FlutterResult _Nonnull)result { + if (!soundPermission && !alertPermission && !badgePermission) { + result(@NO); + return; + } + if (@available(iOS 10.0, *)) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + + UNAuthorizationOptions authorizationOptions = 0; + if (soundPermission) { + authorizationOptions += UNAuthorizationOptionSound; + } + if (alertPermission) { + authorizationOptions += UNAuthorizationOptionAlert; } + if (badgePermission) { + authorizationOptions += UNAuthorizationOptionBadge; + } + [center requestAuthorizationWithOptions:(authorizationOptions) + completionHandler:^(BOOL granted, + NSError *_Nullable error) { + result(@(granted)); + }]; + } else { + UIUserNotificationType notificationTypes = 0; + if (soundPermission) { + notificationTypes |= UIUserNotificationTypeSound; + } + if (alertPermission) { + notificationTypes |= UIUserNotificationTypeAlert; + } + if (badgePermission) { + notificationTypes |= UIUserNotificationTypeBadge; + } + UIUserNotificationSettings *settings = + [UIUserNotificationSettings settingsForTypes:notificationTypes + categories:nil]; + [[UIApplication sharedApplication] + registerUserNotificationSettings:settings]; + result(@YES); + } } -- (UILocalNotification *)buildStandardUILocalNotification:(NSDictionary *)arguments { - UILocalNotification *notification = [[UILocalNotification alloc] init]; - if([self containsKey:BODY forDictionary:arguments]) { - notification.alertBody = arguments[BODY]; +- (UILocalNotification *)buildStandardUILocalNotification: + (NSDictionary *)arguments { + UILocalNotification *notification = [[UILocalNotification alloc] init]; + if ([self containsKey:BODY forDictionary:arguments]) { + notification.alertBody = arguments[BODY]; + } + + NSString *title; + if ([self containsKey:TITLE forDictionary:arguments]) { + title = arguments[TITLE]; + if (@available(iOS 8.2, *)) { + notification.alertTitle = title; } - - NSString *title; - if([self containsKey:TITLE forDictionary:arguments]) { - title = arguments[TITLE]; - if(@available(iOS 8.2, *)) { - notification.alertTitle = title; - } + } + + bool presentAlert = _displayAlert; + bool presentSound = _playSound; + bool presentBadge = _updateBadge; + if (arguments[PLATFORM_SPECIFICS] != [NSNull null]) { + NSDictionary *platformSpecifics = arguments[PLATFORM_SPECIFICS]; + + if ([self containsKey:PRESENT_ALERT forDictionary:platformSpecifics]) { + presentAlert = [[platformSpecifics objectForKey:PRESENT_ALERT] boolValue]; } - - bool presentAlert = _displayAlert; - bool presentSound = _playSound; - bool presentBadge = _updateBadge; - if(arguments[PLATFORM_SPECIFICS] != [NSNull null]) { - NSDictionary *platformSpecifics = arguments[PLATFORM_SPECIFICS]; - - if([self containsKey:PRESENT_ALERT forDictionary:platformSpecifics]) { - presentAlert = [[platformSpecifics objectForKey:PRESENT_ALERT] boolValue]; - } - if([self containsKey:PRESENT_SOUND forDictionary:platformSpecifics]) { - presentSound = [[platformSpecifics objectForKey:PRESENT_SOUND] boolValue]; - } - if([self containsKey:PRESENT_BADGE forDictionary:platformSpecifics]) { - presentBadge = [[platformSpecifics objectForKey:PRESENT_BADGE] boolValue]; - } - - if([self containsKey:BADGE_NUMBER forDictionary:platformSpecifics]) { - notification.applicationIconBadgeNumber = [platformSpecifics[BADGE_NUMBER] integerValue]; - } - - if([self containsKey:SOUND forDictionary:platformSpecifics]) { - notification.soundName = [platformSpecifics[SOUND] stringValue]; - } + if ([self containsKey:PRESENT_SOUND forDictionary:platformSpecifics]) { + presentSound = [[platformSpecifics objectForKey:PRESENT_SOUND] boolValue]; + } + if ([self containsKey:PRESENT_BADGE forDictionary:platformSpecifics]) { + presentBadge = [[platformSpecifics objectForKey:PRESENT_BADGE] boolValue]; + } + + if ([self containsKey:BADGE_NUMBER forDictionary:platformSpecifics]) { + notification.applicationIconBadgeNumber = + [platformSpecifics[BADGE_NUMBER] integerValue]; } - - if(presentSound && notification.soundName == nil) { - notification.soundName = UILocalNotificationDefaultSoundName; + + if ([self containsKey:SOUND forDictionary:platformSpecifics]) { + notification.soundName = [platformSpecifics[SOUND] stringValue]; } - - notification.userInfo = [self buildUserDict:arguments[ID] title:title presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge payload:arguments[PAYLOAD]]; - return notification; + } + + if (presentSound && notification.soundName == nil) { + notification.soundName = UILocalNotificationDefaultSoundName; + } + + notification.userInfo = [self buildUserDict:arguments[ID] + title:title + presentAlert:presentAlert + presentSound:presentSound + presentBadge:presentBadge + payload:arguments[PAYLOAD]]; + return notification; } - (NSString *)getIdentifier:(id)arguments { - return [arguments[ID] stringValue]; + return [arguments[ID] stringValue]; } -- (void)show:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - if(@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - [self addNotificationRequest:[self getIdentifier:arguments] content:content result:result trigger:nil]; - } else { - UILocalNotification * notification = [self buildStandardUILocalNotification:arguments]; - [[UIApplication sharedApplication] presentLocalNotificationNow:notification]; - result(nil); - } +- (void)show:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + UNMutableNotificationContent *content = + [self buildStandardNotificationContent:arguments result:result]; + [self addNotificationRequest:[self getIdentifier:arguments] + content:content + result:result + trigger:nil]; + } else { + UILocalNotification *notification = + [self buildStandardUILocalNotification:arguments]; + [[UIApplication sharedApplication] + presentLocalNotificationNow:notification]; + result(nil); + } } -- (void)zonedSchedule:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - if(@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - UNCalendarNotificationTrigger *trigger = [self buildUserNotificationCalendarTrigger:arguments]; - [self addNotificationRequest:[self getIdentifier:arguments] content:content result:result trigger:trigger]; - - } else { - UILocalNotification * notification = [self buildStandardUILocalNotification:arguments]; - NSString *scheduledDateTime = arguments[SCHEDULED_DATE_TIME]; - NSString *timeZoneName = arguments[TIME_ZONE_NAME]; - NSNumber *matchDateComponents = arguments[MATCH_DATE_TIME_COMPONENTS]; - NSNumber *uiLocalNotificationDateInterpretation = arguments[UILOCALNOTIFICATION_DATE_INTERPRETATION]; - NSTimeZone *timezone = [NSTimeZone timeZoneWithName:timeZoneName]; - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"]; - [dateFormatter setTimeZone:timezone]; - NSDate *date = [dateFormatter dateFromString:scheduledDateTime]; - notification.fireDate = date; - if (uiLocalNotificationDateInterpretation != nil) { - if([uiLocalNotificationDateInterpretation integerValue] == AbsoluteGMTTime) { - notification.timeZone = nil; - } else if([uiLocalNotificationDateInterpretation integerValue] == WallClockTime) { - notification.timeZone = timezone; - } - } - if(matchDateComponents != nil) { - if([matchDateComponents integerValue] == Time) { - notification.repeatInterval = NSCalendarUnitDay; - } else if([matchDateComponents integerValue] == DayOfWeekAndTime) { - notification.repeatInterval = NSCalendarUnitWeekOfYear; - } - } - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} - -- (void)schedule:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - NSNumber *secondsSinceEpoch = @([arguments[MILLISECONDS_SINCE_EPOCH] longLongValue] / 1000); - if(@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - NSDate *date = [NSDate dateWithTimeIntervalSince1970:[secondsSinceEpoch longLongValue]]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - NSDateComponents *dateComponents = [calendar components:(NSCalendarUnitYear | - NSCalendarUnitMonth | - NSCalendarUnitDay | - NSCalendarUnitHour | - NSCalendarUnitMinute| - NSCalendarUnitSecond) fromDate:date]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:false]; - [self addNotificationRequest:[self getIdentifier:arguments] content:content result:result trigger:trigger]; - } else { - UILocalNotification * notification = [self buildStandardUILocalNotification:arguments]; - notification.fireDate = [NSDate dateWithTimeIntervalSince1970:[secondsSinceEpoch longLongValue]]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} +- (void)zonedSchedule:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + UNMutableNotificationContent *content = + [self buildStandardNotificationContent:arguments result:result]; + UNCalendarNotificationTrigger *trigger = + [self buildUserNotificationCalendarTrigger:arguments]; + [self addNotificationRequest:[self getIdentifier:arguments] + content:content + result:result + trigger:trigger]; -- (void)periodicallyShow:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - if(@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - UNTimeIntervalNotificationTrigger *trigger = [self buildUserNotificationTimeIntervalTrigger:arguments]; - [self addNotificationRequest:[self getIdentifier:arguments] content:content result:result trigger:trigger]; - } else { - UILocalNotification * notification = [self buildStandardUILocalNotification:arguments]; - NSTimeInterval timeInterval = 0; - switch([arguments[REPEAT_INTERVAL] integerValue]) { - case EveryMinute: - timeInterval = 60; - notification.repeatInterval = NSCalendarUnitMinute; - break; - case Hourly: - timeInterval = 60 * 60; - notification.repeatInterval = NSCalendarUnitHour; - break; - case Daily: - timeInterval = 60 * 60 * 24; - notification.repeatInterval = NSCalendarUnitDay; - break; - case Weekly: - timeInterval = 60 * 60 * 24 * 7; - notification.repeatInterval = NSCalendarUnitWeekOfYear; - break; - } - notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} - -- (void)showDailyAtTime:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - NSDictionary *timeArguments = (NSDictionary *) arguments[REPEAT_TIME]; - NSNumber *hourComponent = timeArguments[HOUR]; - NSNumber *minutesComponent = timeArguments[MINUTE]; - NSNumber *secondsComponent = timeArguments[SECOND]; - if(@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats: YES]; - [self addNotificationRequest:[self getIdentifier:arguments] content:content result:result trigger:trigger]; - } else { - UILocalNotification * notification = [self buildStandardUILocalNotification:arguments]; + } else { + UILocalNotification *notification = + [self buildStandardUILocalNotification:arguments]; + NSString *scheduledDateTime = arguments[SCHEDULED_DATE_TIME]; + NSString *timeZoneName = arguments[TIME_ZONE_NAME]; + NSNumber *matchDateComponents = arguments[MATCH_DATE_TIME_COMPONENTS]; + NSNumber *uiLocalNotificationDateInterpretation = + arguments[UILOCALNOTIFICATION_DATE_INTERPRETATION]; + NSTimeZone *timezone = [NSTimeZone timeZoneWithName:timeZoneName]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"]; + [dateFormatter setTimeZone:timezone]; + NSDate *date = [dateFormatter dateFromString:scheduledDateTime]; + notification.fireDate = date; + if (uiLocalNotificationDateInterpretation != nil) { + if ([uiLocalNotificationDateInterpretation integerValue] == + AbsoluteGMTTime) { + notification.timeZone = nil; + } else if ([uiLocalNotificationDateInterpretation integerValue] == + WallClockTime) { + notification.timeZone = timezone; + } + } + if (matchDateComponents != nil) { + if ([matchDateComponents integerValue] == Time) { notification.repeatInterval = NSCalendarUnitDay; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - notification.fireDate = [calendar dateFromComponents:dateComponents]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} - -- (void)showWeeklyAtDayAndTime:(NSDictionary * _Nonnull)arguments result:(FlutterResult _Nonnull)result { - NSDictionary *timeArguments = (NSDictionary *) arguments[REPEAT_TIME]; - NSNumber *dayOfWeekComponent = arguments[DAY]; - NSNumber *hourComponent = timeArguments[HOUR]; - NSNumber *minutesComponent = timeArguments[MINUTE]; - NSNumber *secondsComponent = timeArguments[SECOND]; - if(@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - [dateComponents setWeekday:[dayOfWeekComponent integerValue]]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats: YES]; - [self addNotificationRequest:[self getIdentifier:arguments] content:content result:result trigger:trigger]; - } else { - UILocalNotification * notification = [self buildStandardUILocalNotification:arguments]; + } else if ([matchDateComponents integerValue] == DayOfWeekAndTime) { notification.repeatInterval = NSCalendarUnitWeekOfYear; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - [dateComponents setWeekday:[dayOfWeekComponent integerValue]]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - notification.fireDate = [calendar dateFromComponents:dateComponents]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); + } else if ([matchDateComponents integerValue] == DayOfMonthAndTime) { + notification.repeatInterval = NSCalendarUnitMonth; + } else if ([matchDateComponents integerValue] == DateAndTime) { + notification.repeatInterval = NSCalendarUnitYear; + } } + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; + result(nil); + } } -- (void)cancel:(NSNumber *)id result:(FlutterResult _Nonnull)result { - if(@available(iOS 10.0, *)) { - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - NSArray *idsToRemove = [[NSArray alloc] initWithObjects:[id stringValue], nil]; - [center removePendingNotificationRequestsWithIdentifiers:idsToRemove]; - [center removeDeliveredNotificationsWithIdentifiers:idsToRemove]; - } else { - NSArray *notifications = [UIApplication sharedApplication].scheduledLocalNotifications; - for( int i = 0; i < [notifications count]; i++) { - UILocalNotification* localNotification = [notifications objectAtIndex:i]; - NSNumber *userInfoNotificationId = localNotification.userInfo[NOTIFICATION_ID]; - if([userInfoNotificationId longValue] == [id longValue]) { - [[UIApplication sharedApplication] cancelLocalNotification:localNotification]; - break; - } - } - } +- (void)schedule:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + NSNumber *secondsSinceEpoch = + @([arguments[MILLISECONDS_SINCE_EPOCH] longLongValue] / 1000); + if (@available(iOS 10.0, *)) { + UNMutableNotificationContent *content = + [self buildStandardNotificationContent:arguments result:result]; + NSDate *date = [NSDate + dateWithTimeIntervalSince1970:[secondsSinceEpoch longLongValue]]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDateComponents *dateComponents = + [calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | + NSCalendarUnitDay | NSCalendarUnitHour | + NSCalendarUnitMinute | NSCalendarUnitSecond) + fromDate:date]; + UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:false]; + [self addNotificationRequest:[self getIdentifier:arguments] + content:content + result:result + trigger:trigger]; + } else { + UILocalNotification *notification = + [self buildStandardUILocalNotification:arguments]; + notification.fireDate = [NSDate + dateWithTimeIntervalSince1970:[secondsSinceEpoch longLongValue]]; + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; result(nil); + } } -- (void)cancelAll:(FlutterResult _Nonnull) result { - if(@available(iOS 10.0, *)) { - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - [center removeAllPendingNotificationRequests]; - [center removeAllDeliveredNotifications]; - } else { - [[UIApplication sharedApplication] cancelAllLocalNotifications]; +- (void)periodicallyShow:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + UNMutableNotificationContent *content = + [self buildStandardNotificationContent:arguments result:result]; + UNTimeIntervalNotificationTrigger *trigger = + [self buildUserNotificationTimeIntervalTrigger:arguments]; + [self addNotificationRequest:[self getIdentifier:arguments] + content:content + result:result + trigger:trigger]; + } else { + UILocalNotification *notification = + [self buildStandardUILocalNotification:arguments]; + NSTimeInterval timeInterval = 0; + switch ([arguments[REPEAT_INTERVAL] integerValue]) { + case EveryMinute: + timeInterval = 60; + notification.repeatInterval = NSCalendarUnitMinute; + break; + case Hourly: + timeInterval = 60 * 60; + notification.repeatInterval = NSCalendarUnitHour; + break; + case Daily: + timeInterval = 60 * 60 * 24; + notification.repeatInterval = NSCalendarUnitDay; + break; + case Weekly: + timeInterval = 60 * 60 * 24 * 7; + notification.repeatInterval = NSCalendarUnitWeekOfYear; + break; } + notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval]; + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; result(nil); + } } -- (UNMutableNotificationContent *) buildStandardNotificationContent:(NSDictionary *) arguments result:(FlutterResult _Nonnull)result API_AVAILABLE(ios(10.0)){ - UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; - if([self containsKey:TITLE forDictionary:arguments]) { - content.title = arguments[TITLE]; +- (void)showDailyAtTime:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + NSDictionary *timeArguments = (NSDictionary *)arguments[REPEAT_TIME]; + NSNumber *hourComponent = timeArguments[HOUR]; + NSNumber *minutesComponent = timeArguments[MINUTE]; + NSNumber *secondsComponent = timeArguments[SECOND]; + if (@available(iOS 10.0, *)) { + UNMutableNotificationContent *content = + [self buildStandardNotificationContent:arguments result:result]; + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setHour:[hourComponent integerValue]]; + [dateComponents setMinute:[minutesComponent integerValue]]; + [dateComponents setSecond:[secondsComponent integerValue]]; + UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:YES]; + [self addNotificationRequest:[self getIdentifier:arguments] + content:content + result:result + trigger:trigger]; + } else { + UILocalNotification *notification = + [self buildStandardUILocalNotification:arguments]; + notification.repeatInterval = NSCalendarUnitDay; + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setHour:[hourComponent integerValue]]; + [dateComponents setMinute:[minutesComponent integerValue]]; + [dateComponents setSecond:[secondsComponent integerValue]]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + notification.fireDate = [calendar dateFromComponents:dateComponents]; + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; + result(nil); + } +} + +- (void)showWeeklyAtDayAndTime:(NSDictionary *_Nonnull)arguments + result:(FlutterResult _Nonnull)result { + NSDictionary *timeArguments = (NSDictionary *)arguments[REPEAT_TIME]; + NSNumber *dayOfWeekComponent = arguments[DAY]; + NSNumber *hourComponent = timeArguments[HOUR]; + NSNumber *minutesComponent = timeArguments[MINUTE]; + NSNumber *secondsComponent = timeArguments[SECOND]; + if (@available(iOS 10.0, *)) { + UNMutableNotificationContent *content = + [self buildStandardNotificationContent:arguments result:result]; + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setHour:[hourComponent integerValue]]; + [dateComponents setMinute:[minutesComponent integerValue]]; + [dateComponents setSecond:[secondsComponent integerValue]]; + [dateComponents setWeekday:[dayOfWeekComponent integerValue]]; + UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:YES]; + [self addNotificationRequest:[self getIdentifier:arguments] + content:content + result:result + trigger:trigger]; + } else { + UILocalNotification *notification = + [self buildStandardUILocalNotification:arguments]; + notification.repeatInterval = NSCalendarUnitWeekOfYear; + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setHour:[hourComponent integerValue]]; + [dateComponents setMinute:[minutesComponent integerValue]]; + [dateComponents setSecond:[secondsComponent integerValue]]; + [dateComponents setWeekday:[dayOfWeekComponent integerValue]]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + notification.fireDate = [calendar dateFromComponents:dateComponents]; + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; + result(nil); + } +} + +- (void)cancel:(NSNumber *)id result:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + NSArray *idsToRemove = + [[NSArray alloc] initWithObjects:[id stringValue], nil]; + [center removePendingNotificationRequestsWithIdentifiers:idsToRemove]; + [center removeDeliveredNotificationsWithIdentifiers:idsToRemove]; + } else { + NSArray *notifications = + [UIApplication sharedApplication].scheduledLocalNotifications; + for (int i = 0; i < [notifications count]; i++) { + UILocalNotification *localNotification = [notifications objectAtIndex:i]; + NSNumber *userInfoNotificationId = + localNotification.userInfo[NOTIFICATION_ID]; + if ([userInfoNotificationId longValue] == [id longValue]) { + [[UIApplication sharedApplication] + cancelLocalNotification:localNotification]; + break; + } } - if([self containsKey:BODY forDictionary:arguments]) { - content.body = arguments[BODY]; + } + result(nil); +} + +- (void)cancelAll:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllPendingNotificationRequests]; + [center removeAllDeliveredNotifications]; + } else { + [[UIApplication sharedApplication] cancelAllLocalNotifications]; + } + result(nil); +} + +- (UNMutableNotificationContent *) + buildStandardNotificationContent:(NSDictionary *)arguments + result:(FlutterResult _Nonnull)result + API_AVAILABLE(ios(10.0)) { + UNMutableNotificationContent *content = + [[UNMutableNotificationContent alloc] init]; + if ([self containsKey:TITLE forDictionary:arguments]) { + content.title = arguments[TITLE]; + } + if ([self containsKey:BODY forDictionary:arguments]) { + content.body = arguments[BODY]; + } + bool presentAlert = _displayAlert; + bool presentSound = _playSound; + bool presentBadge = _updateBadge; + if (arguments[PLATFORM_SPECIFICS] != [NSNull null]) { + NSDictionary *platformSpecifics = arguments[PLATFORM_SPECIFICS]; + if ([self containsKey:PRESENT_ALERT forDictionary:platformSpecifics]) { + presentAlert = [[platformSpecifics objectForKey:PRESENT_ALERT] boolValue]; } - bool presentAlert = _displayAlert; - bool presentSound = _playSound; - bool presentBadge = _updateBadge; - if(arguments[PLATFORM_SPECIFICS] != [NSNull null]) { - NSDictionary *platformSpecifics = arguments[PLATFORM_SPECIFICS]; - if([self containsKey:PRESENT_ALERT forDictionary:platformSpecifics]) { - presentAlert = [[platformSpecifics objectForKey:PRESENT_ALERT] boolValue]; - } - if([self containsKey:PRESENT_SOUND forDictionary:platformSpecifics]) { - presentSound = [[platformSpecifics objectForKey:PRESENT_SOUND] boolValue]; - } - if([self containsKey:PRESENT_BADGE forDictionary:platformSpecifics]) { - presentBadge = [[platformSpecifics objectForKey:PRESENT_BADGE] boolValue]; - } - if([self containsKey:BADGE_NUMBER forDictionary:platformSpecifics]) { - content.badge = [platformSpecifics objectForKey:BADGE_NUMBER]; - } - if([self containsKey:ATTACHMENTS forDictionary:platformSpecifics]) { - NSArray *attachments = platformSpecifics[ATTACHMENTS]; - if(attachments.count > 0) { - NSMutableArray *notificationAttachments = [NSMutableArray arrayWithCapacity:attachments.count]; - for (NSDictionary *attachment in attachments) { - NSError *error; - UNNotificationAttachment *notificationAttachment = [UNNotificationAttachment attachmentWithIdentifier:attachment[ATTACHMENT_IDENTIFIER] - URL:[NSURL fileURLWithPath:attachment[ATTACHMENT_FILE_PATH]] - options:nil error:&error]; - if(error) { - result(getFlutterError(error)); - return nil; - } - [notificationAttachments addObject:notificationAttachment]; - } - content.attachments = notificationAttachments; - } - } - if([self containsKey:SOUND forDictionary:platformSpecifics]) { - content.sound = [UNNotificationSound soundNamed:platformSpecifics[SOUND]]; - } - if([self containsKey:SUBTITLE forDictionary:platformSpecifics]) { - content.subtitle = platformSpecifics[SUBTITLE]; - } - if([self containsKey:@"categoryIdentifier" forDictionary:platformSpecifics]) { - content.categoryIdentifier = platformSpecifics[@"categoryIdentifier"]; + if ([self containsKey:PRESENT_SOUND forDictionary:platformSpecifics]) { + presentSound = [[platformSpecifics objectForKey:PRESENT_SOUND] boolValue]; + } + if ([self containsKey:PRESENT_BADGE forDictionary:platformSpecifics]) { + presentBadge = [[platformSpecifics objectForKey:PRESENT_BADGE] boolValue]; + } + if ([self containsKey:BADGE_NUMBER forDictionary:platformSpecifics]) { + content.badge = [platformSpecifics objectForKey:BADGE_NUMBER]; + } + if ([self containsKey:THREAD_IDENTIFIER forDictionary:platformSpecifics]) { + content.threadIdentifier = platformSpecifics[THREAD_IDENTIFIER]; + } + if ([self containsKey:ATTACHMENTS forDictionary:platformSpecifics]) { + NSArray *attachments = platformSpecifics[ATTACHMENTS]; + if (attachments.count > 0) { + NSMutableArray *notificationAttachments = + [NSMutableArray arrayWithCapacity:attachments.count]; + for (NSDictionary *attachment in attachments) { + NSError *error; + UNNotificationAttachment *notificationAttachment = + [UNNotificationAttachment + attachmentWithIdentifier:attachment[ATTACHMENT_IDENTIFIER] + URL:[NSURL + fileURLWithPath: + attachment + [ATTACHMENT_FILE_PATH]] + options:nil + error:&error]; + if (error) { + result(getFlutterError(error)); + return nil; + } + [notificationAttachments addObject:notificationAttachment]; } + content.attachments = notificationAttachments; + } } - - - if(presentSound && content.sound == nil) { - content.sound = UNNotificationSound.defaultSound; + if ([self containsKey:SOUND forDictionary:platformSpecifics]) { + content.sound = [UNNotificationSound soundNamed:platformSpecifics[SOUND]]; } - content.userInfo = [self buildUserDict:arguments[ID] title:content.title presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge payload:arguments[PAYLOAD]]; - return content; + if ([self containsKey:SUBTITLE forDictionary:platformSpecifics]) { + content.subtitle = platformSpecifics[SUBTITLE]; + } + if ([self containsKey:@"categoryIdentifier" + forDictionary:platformSpecifics]) { + content.categoryIdentifier = platformSpecifics[@"categoryIdentifier"]; + } + } + + if (presentSound && content.sound == nil) { + content.sound = UNNotificationSound.defaultSound; + } + content.userInfo = [self buildUserDict:arguments[ID] + title:content.title + presentAlert:presentAlert + presentSound:presentSound + presentBadge:presentBadge + payload:arguments[PAYLOAD]]; + return content; } -- (UNCalendarNotificationTrigger *) buildUserNotificationCalendarTrigger:(id) arguments NS_AVAILABLE_IOS(10.0) { - NSString *scheduledDateTime = arguments[SCHEDULED_DATE_TIME]; - NSString *timeZoneName = arguments[TIME_ZONE_NAME]; - - NSNumber *matchDateComponents = arguments[MATCH_DATE_TIME_COMPONENTS]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - NSTimeZone *timezone = [NSTimeZone timeZoneWithName:timeZoneName]; - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; +- (UNCalendarNotificationTrigger *)buildUserNotificationCalendarTrigger: + (id)arguments NS_AVAILABLE_IOS(10.0) { + NSString *scheduledDateTime = arguments[SCHEDULED_DATE_TIME]; + NSString *timeZoneName = arguments[TIME_ZONE_NAME]; - // Needed for some countries, when phone DateTime format is 12H - NSLocale *posix = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + NSNumber *matchDateComponents = arguments[MATCH_DATE_TIME_COMPONENTS]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSTimeZone *timezone = [NSTimeZone timeZoneWithName:timeZoneName]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"]; - [dateFormatter setTimeZone:timezone]; - [dateFormatter setLocale:posix]; + // Needed for some countries, when phone DateTime format is 12H + NSLocale *posix = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; - NSDate *date = [dateFormatter dateFromString:scheduledDateTime]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"]; + [dateFormatter setTimeZone:timezone]; + [dateFormatter setLocale:posix]; - calendar.timeZone = timezone; - if(matchDateComponents != nil) { - if([matchDateComponents integerValue] == Time) { - NSDateComponents *dateComponents = [calendar components:( - NSCalendarUnitHour | - NSCalendarUnitMinute| - NSCalendarUnitSecond | NSCalendarUnitTimeZone) fromDate:date]; - return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:YES]; - - } else if([matchDateComponents integerValue] == DayOfWeekAndTime) { - NSDateComponents *dateComponents = [calendar components:( NSCalendarUnitWeekday | - NSCalendarUnitHour | - NSCalendarUnitMinute| - NSCalendarUnitSecond | NSCalendarUnitTimeZone) fromDate:date]; - return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:YES]; - } - return nil; - } - NSDateComponents *dateComponents = [calendar components:(NSCalendarUnitYear | - NSCalendarUnitMonth | - NSCalendarUnitDay | - NSCalendarUnitHour | - NSCalendarUnitMinute| - NSCalendarUnitSecond | NSCalendarUnitTimeZone) fromDate:date]; - return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:NO]; -} - - -- (UNTimeIntervalNotificationTrigger *)buildUserNotificationTimeIntervalTrigger:(id)arguments API_AVAILABLE(ios(10.0)){ - switch([arguments[REPEAT_INTERVAL] integerValue]) { - case EveryMinute: - return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 - repeats:YES]; - case Hourly: - return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 * 60 - repeats:YES]; - case Daily: - return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 * 60 * 24 - repeats:YES]; - break; - case Weekly: - return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 * 60 * 24 * 7 - repeats:YES]; + NSDate *date = [dateFormatter dateFromString:scheduledDateTime]; + + calendar.timeZone = timezone; + if (matchDateComponents != nil) { + if ([matchDateComponents integerValue] == Time) { + NSDateComponents *dateComponents = + [calendar components:(NSCalendarUnitHour | NSCalendarUnitMinute | + NSCalendarUnitSecond | NSCalendarUnitTimeZone) + fromDate:date]; + return [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:YES]; + + } else if ([matchDateComponents integerValue] == DayOfWeekAndTime) { + NSDateComponents *dateComponents = + [calendar components:(NSCalendarUnitWeekday | NSCalendarUnitHour | + NSCalendarUnitMinute | NSCalendarUnitSecond | + NSCalendarUnitTimeZone) + fromDate:date]; + return [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:YES]; + } else if ([matchDateComponents integerValue] == DayOfMonthAndTime) { + NSDateComponents *dateComponents = + [calendar components:(NSCalendarUnitDay | NSCalendarUnitHour | + NSCalendarUnitMinute | NSCalendarUnitSecond | + NSCalendarUnitTimeZone) + fromDate:date]; + return [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:YES]; + } else if ([matchDateComponents integerValue] == DateAndTime) { + NSDateComponents *dateComponents = + [calendar components:(NSCalendarUnitMonth | NSCalendarUnitDay | + NSCalendarUnitHour | NSCalendarUnitMinute | + NSCalendarUnitSecond | NSCalendarUnitTimeZone) + fromDate:date]; + return [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:YES]; } return nil; + } + NSDateComponents *dateComponents = [calendar + components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | + NSCalendarUnitHour | NSCalendarUnitMinute | + NSCalendarUnitSecond | NSCalendarUnitTimeZone) + fromDate:date]; + return [UNCalendarNotificationTrigger + triggerWithDateMatchingComponents:dateComponents + repeats:NO]; } +- (UNTimeIntervalNotificationTrigger *)buildUserNotificationTimeIntervalTrigger: + (id)arguments API_AVAILABLE(ios(10.0)) { + switch ([arguments[REPEAT_INTERVAL] integerValue]) { + case EveryMinute: + return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 + repeats:YES]; + case Hourly: + return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 * 60 + repeats:YES]; + case Daily: + return + [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 * 60 * 24 + repeats:YES]; + break; + case Weekly: + return [UNTimeIntervalNotificationTrigger + triggerWithTimeInterval:60 * 60 * 24 * 7 + repeats:YES]; + } + return nil; +} -- (NSDictionary*)buildUserDict:(NSNumber *)id title:(NSString *)title presentAlert:(bool)presentAlert presentSound:(bool)presentSound presentBadge:(bool)presentBadge payload:(NSString *)payload { - NSMutableDictionary *userDict = [[NSMutableDictionary alloc] init]; - userDict[NOTIFICATION_ID] = id; - if(title) { - userDict[TITLE] = title; - } - userDict[PRESENT_ALERT] = [NSNumber numberWithBool:presentAlert]; - userDict[PRESENT_SOUND] = [NSNumber numberWithBool:presentSound]; - userDict[PRESENT_BADGE] = [NSNumber numberWithBool:presentBadge]; - userDict[PAYLOAD] = payload; - return userDict; +- (NSDictionary *)buildUserDict:(NSNumber *)id + title:(NSString *)title + presentAlert:(bool)presentAlert + presentSound:(bool)presentSound + presentBadge:(bool)presentBadge + payload:(NSString *)payload { + NSMutableDictionary *userDict = [[NSMutableDictionary alloc] init]; + userDict[NOTIFICATION_ID] = id; + if (title) { + userDict[TITLE] = title; + } + userDict[PRESENT_ALERT] = [NSNumber numberWithBool:presentAlert]; + userDict[PRESENT_SOUND] = [NSNumber numberWithBool:presentSound]; + userDict[PRESENT_BADGE] = [NSNumber numberWithBool:presentBadge]; + userDict[PAYLOAD] = payload; + return userDict; } -- (void)addNotificationRequest:(NSString*)identifier content:(UNMutableNotificationContent *)content result:(FlutterResult _Nonnull)result trigger:(UNNotificationTrigger *)trigger API_AVAILABLE(ios(10.0)){ - UNNotificationRequest *notificationRequest = [UNNotificationRequest - requestWithIdentifier:identifier content:content trigger:trigger]; - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - [center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) { - if (error == nil) { - result(nil); - return; - } - result(getFlutterError(error)); - }]; +- (void)addNotificationRequest:(NSString *)identifier + content:(UNMutableNotificationContent *)content + result:(FlutterResult _Nonnull)result + trigger:(UNNotificationTrigger *)trigger + API_AVAILABLE(ios(10.0)) { + UNNotificationRequest *notificationRequest = + [UNNotificationRequest requestWithIdentifier:identifier + content:content + trigger:trigger]; + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:notificationRequest + withCompletionHandler:^(NSError *_Nullable error) { + if (error == nil) { + result(nil); + return; + } + result(getFlutterError(error)); + }]; } - (BOOL)isAFlutterLocalNotification:(NSDictionary *)userInfo { - return userInfo != nil && userInfo[NOTIFICATION_ID] && userInfo[PRESENT_ALERT] && userInfo[PRESENT_SOUND] && userInfo[PRESENT_BADGE] && userInfo[PAYLOAD]; + return userInfo != nil && userInfo[NOTIFICATION_ID] && + userInfo[PRESENT_ALERT] && userInfo[PRESENT_SOUND] && + userInfo[PRESENT_BADGE] && userInfo[PAYLOAD]; } - (void)handleSelectNotification:(NSString *)payload { - [_channel invokeMethod:@"selectNotification" arguments:payload]; + [_channel invokeMethod:@"selectNotification" arguments:payload]; } -- (BOOL)containsKey:(NSString *)key forDictionary:(NSDictionary *)dictionary{ - return dictionary[key] != [NSNull null] && dictionary[key] != nil; +- (BOOL)containsKey:(NSString *)key forDictionary:(NSDictionary *)dictionary { + return dictionary[key] != [NSNull null] && dictionary[key] != nil; } - (void)startEngineIfNeeded { - if (backgroundEngine) { - return; - } - - NSNumber* dispatcherHandle = [_persistentState objectForKey:@"dispatcher_handle"]; - - backgroundEngine = [[FlutterEngine alloc] initWithName:@"FlutterLocalNotificationsIsolate" - project:nil - allowHeadlessExecution:true]; - - FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:[dispatcherHandle longValue]]; - - if (!info) { - NSLog(@"callback information could not be retrieved"); - abort(); - } - - NSString* entryPoint = info.callbackName; - NSString* uri = info.callbackLibraryPath; - - dispatch_async(dispatch_get_main_queue(), ^{ - FlutterEventChannel* channel = [FlutterEventChannel eventChannelWithName:@"dexterous.com/flutter/local_notifications/actions" binaryMessenger:backgroundEngine.binaryMessenger]; - - [backgroundEngine runWithEntrypoint:entryPoint libraryURI:uri]; - [channel setStreamHandler:actionEventSink]; - - NSAssert(registerPlugins != nil, @"failed to set registerPlugins"); - registerPlugins(backgroundEngine); - }); + if (backgroundEngine) { + return; + } + + NSNumber *dispatcherHandle = + [_persistentState objectForKey:@"dispatcher_handle"]; + + backgroundEngine = + [[FlutterEngine alloc] initWithName:@"FlutterLocalNotificationsIsolate" + project:nil + allowHeadlessExecution:true]; + + FlutterCallbackInformation *info = [FlutterCallbackCache + lookupCallbackInformation:[dispatcherHandle longValue]]; + + if (!info) { + NSLog(@"callback information could not be retrieved"); + abort(); + } + + NSString *entryPoint = info.callbackName; + NSString *uri = info.callbackLibraryPath; + + dispatch_async(dispatch_get_main_queue(), ^{ + FlutterEventChannel *channel = [FlutterEventChannel + eventChannelWithName: + @"dexterous.com/flutter/local_notifications/actions" + binaryMessenger:backgroundEngine.binaryMessenger]; + + [backgroundEngine runWithEntrypoint:entryPoint libraryURI:uri]; + [channel setStreamHandler:actionEventSink]; + + NSAssert(registerPlugins != nil, @"failed to set registerPlugins"); + registerPlugins(backgroundEngine); + }); } #pragma mark - UNUserNotificationCenterDelegate -- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification :(UNNotification *)notification withCompletionHandler :(void (^)(UNNotificationPresentationOptions))completionHandler NS_AVAILABLE_IOS(10.0) { - if(![self isAFlutterLocalNotification:notification.request.content.userInfo]) { - return; - } - UNNotificationPresentationOptions presentationOptions = 0; - NSNumber *presentAlertValue = (NSNumber*)notification.request.content.userInfo[PRESENT_ALERT]; - NSNumber *presentSoundValue = (NSNumber*)notification.request.content.userInfo[PRESENT_SOUND]; - NSNumber *presentBadgeValue = (NSNumber*)notification.request.content.userInfo[PRESENT_BADGE]; - bool presentAlert = [presentAlertValue boolValue]; - bool presentSound = [presentSoundValue boolValue]; - bool presentBadge = [presentBadgeValue boolValue]; - if(presentAlert) { - presentationOptions |= UNNotificationPresentationOptionAlert; - } - if(presentSound){ - presentationOptions |= UNNotificationPresentationOptionSound; - } - if(presentBadge) { - presentationOptions |= UNNotificationPresentationOptionBadge; - } - completionHandler(presentationOptions); +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler: + (void (^)(UNNotificationPresentationOptions))completionHandler + NS_AVAILABLE_IOS(10.0) { + if (![self + isAFlutterLocalNotification:notification.request.content.userInfo]) { + return; + } + UNNotificationPresentationOptions presentationOptions = 0; + NSNumber *presentAlertValue = + (NSNumber *)notification.request.content.userInfo[PRESENT_ALERT]; + NSNumber *presentSoundValue = + (NSNumber *)notification.request.content.userInfo[PRESENT_SOUND]; + NSNumber *presentBadgeValue = + (NSNumber *)notification.request.content.userInfo[PRESENT_BADGE]; + bool presentAlert = [presentAlertValue boolValue]; + bool presentSound = [presentSoundValue boolValue]; + bool presentBadge = [presentBadgeValue boolValue]; + if (presentAlert) { + presentationOptions |= UNNotificationPresentationOptionAlert; + } + if (presentSound) { + presentationOptions |= UNNotificationPresentationOptionSound; + } + if (presentBadge) { + presentationOptions |= UNNotificationPresentationOptionBadge; + } + completionHandler(presentationOptions); } - (void)userNotificationCenter:(UNUserNotificationCenter *)center -didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)(void))completionHandler NS_AVAILABLE_IOS(10.0) { - if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier] && [self isAFlutterLocalNotification:response.notification.request.content.userInfo]) { - NSString *payload = (NSString *) response.notification.request.content.userInfo[PAYLOAD]; - if (_initialized) { - [self handleSelectNotification:payload]; - } else { - _launchPayload = payload; - _launchingAppFromNotification = true; - } - completionHandler(); - } else if (response.actionIdentifier != nil) { - if (!actionEventSink) { - actionEventSink = [[ActionEventSink alloc] init]; - } - - NSString *text = @""; - if ([response respondsToSelector:@selector(userText)]) { - text = [(UNTextInputNotificationResponse*) response userText]; - } - - [actionEventSink addItem:@{ - @"id": response.actionIdentifier, - @"input": text, - @"payload": response.notification.request.content.userInfo[@"payload"], - }]; - - - [self startEngineIfNeeded]; - - completionHandler(); + didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler + NS_AVAILABLE_IOS(10.0) { + if ([response.actionIdentifier + isEqualToString:UNNotificationDefaultActionIdentifier] && + [self isAFlutterLocalNotification:response.notification.request.content + .userInfo]) { + NSString *payload = + (NSString *)response.notification.request.content.userInfo[PAYLOAD]; + if (_initialized) { + [self handleSelectNotification:payload]; + } else { + _launchPayload = payload; + _launchingAppFromNotification = true; + } + completionHandler(); + } else if (response.actionIdentifier != nil) { + if (!actionEventSink) { + actionEventSink = [[ActionEventSink alloc] init]; } + + NSString *text = @""; + if ([response respondsToSelector:@selector(userText)]) { + text = [(UNTextInputNotificationResponse *)response userText]; + } + + [actionEventSink addItem:@{ + @"id" : response.actionIdentifier, + @"input" : text, + @"payload" : response.notification.request.content.userInfo[@"payload"], + }]; + + [self startEngineIfNeeded]; + + completionHandler(); + } } #pragma mark - AppDelegate - (BOOL)application:(UIApplication *)application -didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - if (launchOptions != nil) { - UILocalNotification *launchNotification = (UILocalNotification *)[launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; - _launchingAppFromNotification = launchNotification != nil && [self isAFlutterLocalNotification:launchNotification.userInfo]; - if (_launchingAppFromNotification) { - _launchNotification = launchNotification; - } + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + if (launchOptions != nil) { + UILocalNotification *launchNotification = + (UILocalNotification *)[launchOptions + objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; + _launchingAppFromNotification = + launchNotification != nil && + [self isAFlutterLocalNotification:launchNotification.userInfo]; + if (_launchingAppFromNotification) { + _launchNotification = launchNotification; } - - return YES; + } + + return YES; } -- (void)application:(UIApplication*)application -didReceiveLocalNotification:(UILocalNotification*)notification { - if(@available(iOS 10.0, *)) { - return; - } - if(![self isAFlutterLocalNotification:notification.userInfo]) { - return; - } - - NSMutableDictionary *arguments = [[NSMutableDictionary alloc] init]; - arguments[ID]= notification.userInfo[NOTIFICATION_ID]; - if (notification.userInfo[TITLE] != [NSNull null]) { - arguments[TITLE] = notification.userInfo[TITLE]; - } - if (notification.alertBody != nil) { - arguments[BODY] = notification.alertBody; - } - if (notification.userInfo[PAYLOAD] != [NSNull null]) { - arguments[PAYLOAD] =notification.userInfo[PAYLOAD]; - } - [_channel invokeMethod:DID_RECEIVE_LOCAL_NOTIFICATION arguments:arguments]; +- (void)application:(UIApplication *)application + didReceiveLocalNotification:(UILocalNotification *)notification { + if (@available(iOS 10.0, *)) { + return; + } + if (![self isAFlutterLocalNotification:notification.userInfo]) { + return; + } + + NSMutableDictionary *arguments = [[NSMutableDictionary alloc] init]; + arguments[ID] = notification.userInfo[NOTIFICATION_ID]; + if (notification.userInfo[TITLE] != [NSNull null]) { + arguments[TITLE] = notification.userInfo[TITLE]; + } + if (notification.alertBody != nil) { + arguments[BODY] = notification.alertBody; + } + if (notification.userInfo[PAYLOAD] != [NSNull null]) { + arguments[PAYLOAD] = notification.userInfo[PAYLOAD]; + } + [_channel invokeMethod:DID_RECEIVE_LOCAL_NOTIFICATION arguments:arguments]; } @end diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart index e3e77f605..f22582e22 100644 --- a/flutter_local_notifications/lib/flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/flutter_local_notifications.dart @@ -1,5 +1,7 @@ +export 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart' show + SelectNotificationCallback, PendingNotificationRequest, RepeatInterval, NotificationAppLaunchDetails; @@ -32,6 +34,7 @@ export 'src/platform_specifics/ios/enums.dart'; export 'src/platform_specifics/ios/initialization_settings.dart'; export 'src/platform_specifics/ios/notification_attachment.dart'; export 'src/platform_specifics/ios/notification_details.dart'; +export 'src/platform_specifics/ios/notification_details.dart'; export 'src/platform_specifics/macos/initialization_settings.dart'; export 'src/platform_specifics/macos/notification_attachment.dart'; export 'src/platform_specifics/macos/notification_details.dart'; diff --git a/flutter_local_notifications/lib/src/callback_dispatcher.dart b/flutter_local_notifications/lib/src/callback_dispatcher.dart index 415ad5926..9854e6472 100644 --- a/flutter_local_notifications/lib/src/callback_dispatcher.dart +++ b/flutter_local_notifications/lib/src/callback_dispatcher.dart @@ -2,8 +2,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; - -import '../flutter_local_notifications.dart'; +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; // ignore_for_file: public_member_api_docs @@ -17,9 +16,11 @@ void callbackDispatcher() { MethodChannel('dexterous.com/flutter/local_notifications'); channel.invokeMethod('getCallbackHandle').then((handle) { - final NotificationActionCallback callback = - PluginUtilities.getCallbackFromHandle( - CallbackHandle.fromRawHandle(handle)); + final NotificationActionCallback? callback = handle == null + ? null + : PluginUtilities.getCallbackFromHandle( + CallbackHandle.fromRawHandle(handle)) + as NotificationActionCallback?; backgroundChannel .receiveBroadcastStream() @@ -27,7 +28,7 @@ void callbackDispatcher() { .map>( (Map event) => Map.castFrom(event)) .listen((Map event) { - callback(event['id'], event['input'], event['payload']); + callback?.call(event['id'], event['input'], event['payload']); }); }); } diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 91b234903..023378ff3 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -1,16 +1,14 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; -import 'package:platform/platform.dart'; import 'package:timezone/timezone.dart'; -import '../flutter_local_notifications.dart'; import 'initialization_settings.dart'; import 'notification_details.dart'; import 'platform_flutter_local_notifications.dart'; -import 'typedefs.dart'; +import 'platform_specifics/ios/enums.dart'; import 'types.dart'; /// Provides cross-platform functionality for displaying local notifications. @@ -26,36 +24,27 @@ class FlutterLocalNotificationsPlugin { /// Factory for create an instance of [FlutterLocalNotificationsPlugin]. factory FlutterLocalNotificationsPlugin() => _instance; - /// Used internally for creating the appropriate platform-specific - /// implementation of the plugin. - /// - /// This can be used for tests as well. For example, the following code - /// - /// ``` - /// FlutterLocalNotificationsPlugin.private(FakePlatform(operatingSystem: - /// 'android')) - /// ``` - /// - /// could be used in a test needs the plugin to use Android implementation - @visibleForTesting - FlutterLocalNotificationsPlugin.private(Platform platform) - : _platform = platform { - if (platform.isAndroid) { + FlutterLocalNotificationsPlugin._() { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { FlutterLocalNotificationsPlatform.instance = AndroidFlutterLocalNotificationsPlugin(); - } else if (platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { FlutterLocalNotificationsPlatform.instance = IOSFlutterLocalNotificationsPlugin(); - } else if (platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { FlutterLocalNotificationsPlatform.instance = MacOSFlutterLocalNotificationsPlugin(); + } else if (defaultTargetPlatform == TargetPlatform.linux) { + FlutterLocalNotificationsPlatform.instance = + LinuxFlutterLocalNotificationsPlugin(); } } static final FlutterLocalNotificationsPlugin _instance = - FlutterLocalNotificationsPlugin.private(const LocalPlatform()); - - final Platform _platform; + FlutterLocalNotificationsPlugin._(); /// Returns the underlying platform-specific implementation of given type [T], /// which must be a concrete subclass of [FlutterLocalNotificationsPlatform](https://pub.dev/documentation/flutter_local_notifications_platform_interface/latest/flutter_local_notifications_platform_interface/FlutterLocalNotificationsPlatform-class.html) @@ -64,7 +53,7 @@ class FlutterLocalNotificationsPlugin { /// type for a result to be returned. For example, when the specified type /// argument is of type [AndroidFlutterLocalNotificationsPlugin], this will /// only return a result of that type when running on Android. - T resolvePlatformSpecificImplementation< + T? resolvePlatformSpecificImplementation< T extends FlutterLocalNotificationsPlatform>() { if (T == FlutterLocalNotificationsPlatform) { throw ArgumentError.value( @@ -72,21 +61,30 @@ class FlutterLocalNotificationsPlugin { 'The type argument must be a concrete subclass of ' 'FlutterLocalNotificationsPlatform'); } - if (_platform.isAndroid && + if (kIsWeb) { + return null; + } + + if (defaultTargetPlatform == TargetPlatform.android && T == AndroidFlutterLocalNotificationsPlugin && FlutterLocalNotificationsPlatform.instance is AndroidFlutterLocalNotificationsPlugin) { - return FlutterLocalNotificationsPlatform.instance; - } else if (_platform.isIOS && + return FlutterLocalNotificationsPlatform.instance as T?; + } else if (defaultTargetPlatform == TargetPlatform.iOS && T == IOSFlutterLocalNotificationsPlugin && FlutterLocalNotificationsPlatform.instance is IOSFlutterLocalNotificationsPlugin) { - return FlutterLocalNotificationsPlatform.instance; - } else if (_platform.isMacOS && + return FlutterLocalNotificationsPlatform.instance as T?; + } else if (defaultTargetPlatform == TargetPlatform.macOS && T == MacOSFlutterLocalNotificationsPlugin && FlutterLocalNotificationsPlatform.instance is MacOSFlutterLocalNotificationsPlugin) { - return FlutterLocalNotificationsPlatform.instance; + return FlutterLocalNotificationsPlatform.instance as T?; + } else if (defaultTargetPlatform == TargetPlatform.linux && + T == LinuxFlutterLocalNotificationsPlugin && + FlutterLocalNotificationsPlatform.instance + is LinuxFlutterLocalNotificationsPlugin) { + return FlutterLocalNotificationsPlatform.instance as T?; } return null; @@ -95,9 +93,6 @@ class FlutterLocalNotificationsPlugin { /// Initializes the plugin. /// /// Call this method on application before using the plugin further. - /// This should only be done once. When a notification created by this plugin - /// was used to launch the app, calling `initialize` is what will trigger - /// to the `onSelectNotification` callback to be fire. /// /// Will return a [bool] value to indicate if initialization succeeded. /// On iOS this is dependent on if permissions have been granted to show @@ -113,27 +108,38 @@ class FlutterLocalNotificationsPlugin { /// [IOSInitializationSettings.requestSoundPermission] values to false. /// [IOSFlutterLocalNotificationsPlugin.requestPermissions] can then be called /// to request permissions when needed. - Future initialize( + /// + /// To handle when a notification launched an application, use + /// [getNotificationAppLaunchDetails]. + Future initialize( InitializationSettings initializationSettings, { - SelectNotificationCallback onSelectNotification, - NotificationActionCallback backgroundHandler, + SelectNotificationCallback? onSelectNotification, + NotificationActionCallback? backgroundHandler, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return true; + } + if (defaultTargetPlatform == TargetPlatform.android) { return resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() - ?.initialize(initializationSettings?.android, + ?.initialize(initializationSettings.android!, onSelectNotification: onSelectNotification, backgroundHandler: backgroundHandler); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { return await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() - ?.initialize(initializationSettings?.iOS, + ?.initialize(initializationSettings.iOS!, onSelectNotification: onSelectNotification, backgroundHandler: backgroundHandler); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { return await resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() - ?.initialize(initializationSettings?.macOS, + ?.initialize(initializationSettings.macOS!, + onSelectNotification: onSelectNotification); + } else if (defaultTargetPlatform == TargetPlatform.linux) { + return await resolvePlatformSpecificImplementation< + LinuxFlutterLocalNotificationsPlugin>() + ?.initialize(initializationSettings.linux!, onSelectNotification: onSelectNotification); } return true; @@ -151,22 +157,26 @@ class FlutterLocalNotificationsPlugin { /// Note that this will return null for applications running on macOS /// versions older than 10.14. This is because there's currently no mechanism /// for plugins to receive information on lifecycle events. - Future getNotificationAppLaunchDetails() async { - if (_platform.isAndroid) { + Future + getNotificationAppLaunchDetails() async { + if (kIsWeb) { + return null; + } + if (defaultTargetPlatform == TargetPlatform.android) { return await resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.getNotificationAppLaunchDetails(); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { return await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.getNotificationAppLaunchDetails(); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { return await resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.getNotificationAppLaunchDetails(); } else { return await FlutterLocalNotificationsPlatform.instance - ?.getNotificationAppLaunchDetails() ?? + .getNotificationAppLaunchDetails() ?? const NotificationAppLaunchDetails(false, null); } } @@ -175,30 +185,39 @@ class FlutterLocalNotificationsPlugin { /// the app when a notification is tapped. Future show( int id, - String title, - String body, - NotificationDetails notificationDetails, { - String payload, + String? title, + String? body, + NotificationDetails? notificationDetails, { + String? payload, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { await resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.show(id, title, body, notificationDetails: notificationDetails?.android, payload: payload); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.show(id, title, body, notificationDetails: notificationDetails?.iOS, payload: payload); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { await resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.show(id, title, body, notificationDetails: notificationDetails?.macOS, payload: payload); + } else if (defaultTargetPlatform == TargetPlatform.linux) { + await resolvePlatformSpecificImplementation< + LinuxFlutterLocalNotificationsPlugin>() + ?.show(id, title, body, + notificationDetails: notificationDetails?.linux, + payload: payload); } else { - await FlutterLocalNotificationsPlatform.instance?.show(id, title, body); + await FlutterLocalNotificationsPlatform.instance.show(id, title, body); } } @@ -206,8 +225,21 @@ class FlutterLocalNotificationsPlugin { /// /// This applies to notifications that have been scheduled and those that /// have already been presented. - Future cancel(int id) async { - await FlutterLocalNotificationsPlatform.instance?.cancel(id); + /// + /// The `tag` parameter specifies the Android tag. If it is provided, + /// then the notification that matches both the id and the tag will + /// be canceled. `tag` has no effect on other platforms. + Future cancel(int id, {String? tag}) async { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { + await resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.cancel(id, tag: tag); + } else { + await FlutterLocalNotificationsPlatform.instance.cancel(id); + } } /// Cancels/removes all notifications. @@ -215,7 +247,7 @@ class FlutterLocalNotificationsPlugin { /// This applies to notifications that have been scheduled and those that /// have already been presented. Future cancelAll() async { - await FlutterLocalNotificationsPlatform.instance?.cancelAll(); + await FlutterLocalNotificationsPlatform.instance.cancelAll(); } /// Schedules a notification to be shown at the specified date and time. @@ -227,25 +259,27 @@ class FlutterLocalNotificationsPlugin { 'instead.') Future schedule( int id, - String title, - String body, + String? title, + String? body, DateTime scheduledDate, NotificationDetails notificationDetails, { - String payload, + String? payload, bool androidAllowWhileIdle = false, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { await resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - .schedule( - id, title, body, scheduledDate, notificationDetails?.android, + AndroidFlutterLocalNotificationsPlugin>()! + .schedule(id, title, body, scheduledDate, notificationDetails.android, payload: payload, androidAllowWhileIdle: androidAllowWhileIdle); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() - ?.schedule(id, title, body, scheduledDate, notificationDetails?.iOS, + ?.schedule(id, title, body, scheduledDate, notificationDetails.iOS, payload: payload); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { throw UnimplementedError(); } } @@ -283,39 +317,41 @@ class FlutterLocalNotificationsPlugin { /// appear is 2020-10-20 10:00. Future zonedSchedule( int id, - String title, - String body, + String? title, + String? body, TZDateTime scheduledDate, NotificationDetails notificationDetails, { - @required - UILocalNotificationDateInterpretation - uiLocalNotificationDateInterpretation, - @required bool androidAllowWhileIdle, - String payload, - DateTimeComponents matchDateTimeComponents, + required UILocalNotificationDateInterpretation + uiLocalNotificationDateInterpretation, + required bool androidAllowWhileIdle, + String? payload, + DateTimeComponents? matchDateTimeComponents, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { await resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .zonedSchedule( - id, title, body, scheduledDate, notificationDetails?.android, + id, title, body, scheduledDate, notificationDetails.android, payload: payload, androidAllowWhileIdle: androidAllowWhileIdle, - matchDateComponents: matchDateTimeComponents); - } else if (_platform.isIOS) { + matchDateTimeComponents: matchDateTimeComponents); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.zonedSchedule( - id, title, body, scheduledDate, notificationDetails?.iOS, + id, title, body, scheduledDate, notificationDetails.iOS, uiLocalNotificationDateInterpretation: uiLocalNotificationDateInterpretation, payload: payload, matchDateTimeComponents: matchDateTimeComponents); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { await resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.zonedSchedule( - id, title, body, scheduledDate, notificationDetails?.macOS, + id, title, body, scheduledDate, notificationDetails.macOS, payload: payload, matchDateTimeComponents: matchDateTimeComponents); } @@ -337,34 +373,36 @@ class FlutterLocalNotificationsPlugin { /// repeat. Future periodicallyShow( int id, - String title, - String body, + String? title, + String? body, RepeatInterval repeatInterval, NotificationDetails notificationDetails, { - String payload, + String? payload, bool androidAllowWhileIdle = false, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { await resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.periodicallyShow(id, title, body, repeatInterval, - notificationDetails: notificationDetails?.android, + notificationDetails: notificationDetails.android, payload: payload, androidAllowWhileIdle: androidAllowWhileIdle); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.periodicallyShow(id, title, body, repeatInterval, - notificationDetails: notificationDetails?.iOS, payload: payload); - } else if (_platform.isMacOS) { + notificationDetails: notificationDetails.iOS, payload: payload); + } else if (defaultTargetPlatform == TargetPlatform.macOS) { await resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.periodicallyShow(id, title, body, repeatInterval, - notificationDetails: notificationDetails?.macOS, - payload: payload); + notificationDetails: notificationDetails.macOS, payload: payload); } else { await FlutterLocalNotificationsPlatform.instance - ?.periodicallyShow(id, title, body, repeatInterval); + .periodicallyShow(id, title, body, repeatInterval); } } @@ -376,25 +414,28 @@ class FlutterLocalNotificationsPlugin { 'matchDateTimeComponents parameter.') Future showDailyAtTime( int id, - String title, - String body, + String? title, + String? body, Time notificationTime, NotificationDetails notificationDetails, { - String payload, + String? payload, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { await resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.showDailyAtTime( - id, title, body, notificationTime, notificationDetails?.android, + id, title, body, notificationTime, notificationDetails.android, payload: payload); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.showDailyAtTime( - id, title, body, notificationTime, notificationDetails?.iOS, + id, title, body, notificationTime, notificationDetails.iOS, payload: payload); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { throw UnimplementedError(); } } @@ -407,31 +448,34 @@ class FlutterLocalNotificationsPlugin { 'the matchDateTimeComponents parameter.') Future showWeeklyAtDayAndTime( int id, - String title, - String body, + String? title, + String? body, Day day, Time notificationTime, NotificationDetails notificationDetails, { - String payload, + String? payload, }) async { - if (_platform.isAndroid) { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { await resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.showWeeklyAtDayAndTime(id, title, body, day, notificationTime, - notificationDetails?.android, + notificationDetails.android, payload: payload); - } else if (_platform.isIOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS) { await resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.showWeeklyAtDayAndTime( - id, title, body, day, notificationTime, notificationDetails?.iOS, + id, title, body, day, notificationTime, notificationDetails.iOS, payload: payload); - } else if (_platform.isMacOS) { + } else if (defaultTargetPlatform == TargetPlatform.macOS) { throw UnimplementedError(); } } /// Returns a list of notifications pending to be delivered/shown. Future> pendingNotificationRequests() => - FlutterLocalNotificationsPlatform.instance?.pendingNotificationRequests(); + FlutterLocalNotificationsPlatform.instance.pendingNotificationRequests(); } diff --git a/flutter_local_notifications/lib/src/helpers.dart b/flutter_local_notifications/lib/src/helpers.dart index 3f67d9ecd..cb09dbfbc 100644 --- a/flutter_local_notifications/lib/src/helpers.dart +++ b/flutter_local_notifications/lib/src/helpers.dart @@ -1,19 +1,18 @@ +import 'package:clock/clock.dart'; import 'package:timezone/timezone.dart'; -/// Helper method for validating notification IDs. -/// Ensures IDs are valid 32-bit integers. -void validateId(int id) { - ArgumentError.checkNotNull(id, 'id'); - if (id > 0x7FFFFFFF || id < -0x80000000) { - throw ArgumentError.value(id, 'id', - 'must fit within the size of a 32-bit integer i.e. in the range [-2^31, 2^31 - 1]'); // ignore: lines_longer_than_80_chars - } -} +import 'types.dart'; -/// Helper method for validating a date/time value represents a -/// future point in time. -void validateDateIsInTheFuture(TZDateTime scheduledDate) { - if (scheduledDate.isBefore(DateTime.now())) { +/// Helper method for validating a date/time value represents a future point in +/// time where `matchDateTimeComponents` is null. +void validateDateIsInTheFuture( + TZDateTime scheduledDate, + DateTimeComponents? matchDateTimeComponents, +) { + if (matchDateTimeComponents != null) { + return; + } + if (scheduledDate.isBefore(clock.now())) { throw ArgumentError.value( scheduledDate, 'scheduledDate', 'Must be a date in the future'); } diff --git a/flutter_local_notifications/lib/src/initialization_settings.dart b/flutter_local_notifications/lib/src/initialization_settings.dart index 0dd4e6e6e..a33f4059d 100644 --- a/flutter_local_notifications/lib/src/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/initialization_settings.dart @@ -1,3 +1,5 @@ +import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; + import 'platform_specifics/android/initialization_settings.dart'; import 'platform_specifics/ios/initialization_settings.dart'; import 'platform_specifics/macos/initialization_settings.dart'; @@ -9,14 +11,18 @@ class InitializationSettings { this.android, this.iOS, this.macOS, + this.linux, }); /// Settings for Android. - final AndroidInitializationSettings android; + final AndroidInitializationSettings? android; /// Settings for iOS. - final IOSInitializationSettings iOS; + final IOSInitializationSettings? iOS; - /// Settings for iOS. - final MacOSInitializationSettings macOS; + /// Settings for macOS. + final MacOSInitializationSettings? macOS; + + /// Settings for Linux. + final LinuxInitializationSettings? linux; } diff --git a/flutter_local_notifications/lib/src/notification_details.dart b/flutter_local_notifications/lib/src/notification_details.dart index 1f2933e3f..577df685c 100644 --- a/flutter_local_notifications/lib/src/notification_details.dart +++ b/flutter_local_notifications/lib/src/notification_details.dart @@ -1,3 +1,5 @@ +import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; + import 'platform_specifics/android/notification_details.dart'; import 'platform_specifics/ios/notification_details.dart'; import 'platform_specifics/macos/notification_details.dart'; @@ -9,14 +11,18 @@ class NotificationDetails { this.android, this.iOS, this.macOS, + this.linux, }); /// Notification details for Android. - final AndroidNotificationDetails android; + final AndroidNotificationDetails? android; /// Notification details for iOS. - final IOSNotificationDetails iOS; + final IOSNotificationDetails? iOS; /// Notification details for macOS. - final MacOSNotificationDetails macOS; + final MacOSNotificationDetails? macOS; + + /// Notification details for Linux. + final LinuxNotificationDetails? linux; } diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 05ea57e22..77b8d9565 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -1,19 +1,23 @@ import 'dart:async'; import 'dart:ui'; -import 'package:flutter/foundation.dart'; +import 'package:clock/clock.dart'; + import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; import 'package:timezone/timezone.dart'; import 'callback_dispatcher.dart'; import 'helpers.dart'; import 'platform_specifics/android/active_notification.dart'; +import 'platform_specifics/android/enums.dart'; import 'platform_specifics/android/initialization_settings.dart'; import 'platform_specifics/android/method_channel_mappers.dart'; import 'platform_specifics/android/notification_channel.dart'; import 'platform_specifics/android/notification_channel_group.dart'; import 'platform_specifics/android/notification_details.dart'; +import 'platform_specifics/android/notification_sound.dart'; import 'platform_specifics/ios/enums.dart'; import 'platform_specifics/ios/initialization_settings.dart'; import 'platform_specifics/ios/method_channel_mappers.dart'; @@ -42,8 +46,9 @@ class MethodChannelFlutterLocalNotificationsPlugin Future cancelAll() => _channel.invokeMethod('cancelAll'); @override - Future getNotificationAppLaunchDetails() async { - final Map result = + Future + getNotificationAppLaunchDetails() async { + final Map? result = await _channel.invokeMethod('getNotificationAppLaunchDetails'); return result != null ? NotificationAppLaunchDetails(result['notificationLaunchedApp'], @@ -53,34 +58,36 @@ class MethodChannelFlutterLocalNotificationsPlugin @override Future> pendingNotificationRequests() async { - final List> pendingNotifications = + final List>? pendingNotifications = await _channel.invokeListMethod('pendingNotificationRequests'); return pendingNotifications - // ignore: always_specify_types - .map((p) => PendingNotificationRequest( - p['id'], p['title'], p['body'], p['payload'])) - .toList(); + // ignore: always_specify_types + ?.map((p) => PendingNotificationRequest( + p['id'], p['title'], p['body'], p['payload'])) + .toList() ?? + []; } } /// Android implementation of the local notifications plugin. class AndroidFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { - SelectNotificationCallback _onSelectNotification; + SelectNotificationCallback? _onSelectNotification; - /// Initializes the plugin. Call this method on application before using the + /// Initializes the plugin. + /// + /// Call this method on application before using the /// plugin further. /// - /// This should only be done once. When a notification created by this plugin - /// was used to launch the app, calling `initialize` is what will trigger to - /// the `onSelectNotification` callback to be fire. + /// To handle when a notification launched an application, use + /// [getNotificationAppLaunchDetails]. /// /// [backgroundHandler] specifies a callback handler which receives /// notification action IDs. Future initialize( AndroidInitializationSettings initializationSettings, { - SelectNotificationCallback onSelectNotification, - NotificationActionCallback backgroundHandler, + SelectNotificationCallback? onSelectNotification, + NotificationActionCallback? backgroundHandler, }) async { _onSelectNotification = onSelectNotification; _channel.setMethodCallHandler(_handleMethod); @@ -101,18 +108,18 @@ class AndroidFlutterLocalNotificationsPlugin 'Deprecated due to problems with time zones. Use zonedSchedule instead.') Future schedule( int id, - String title, - String body, + String? title, + String? body, DateTime scheduledDate, - AndroidNotificationDetails notificationDetails, { - String payload, + AndroidNotificationDetails? notificationDetails, { + String? payload, bool androidAllowWhileIdle = false, }) async { validateId(id); - final Map serializedPlatformSpecifics = + final Map serializedPlatformSpecifics = notificationDetails?.toMap() ?? {}; serializedPlatformSpecifics['allowWhileIdle'] = androidAllowWhileIdle; - await _channel.invokeMethod('schedule', { + await _channel.invokeMethod('schedule', { 'id': id, 'title': title, 'body': body, @@ -126,23 +133,23 @@ class AndroidFlutterLocalNotificationsPlugin /// relative to a specific time zone. Future zonedSchedule( int id, - String title, - String body, + String? title, + String? body, TZDateTime scheduledDate, - AndroidNotificationDetails notificationDetails, { - @required bool androidAllowWhileIdle, - String payload, - DateTimeComponents matchDateComponents, + AndroidNotificationDetails? notificationDetails, { + required bool androidAllowWhileIdle, + String? payload, + DateTimeComponents? matchDateTimeComponents, }) async { validateId(id); - validateDateIsInTheFuture(scheduledDate); + validateDateIsInTheFuture(scheduledDate, matchDateTimeComponents); ArgumentError.checkNotNull(androidAllowWhileIdle, 'androidAllowWhileIdle'); - final Map serializedPlatformSpecifics = + final Map serializedPlatformSpecifics = notificationDetails?.toMap() ?? {}; serializedPlatformSpecifics['allowWhileIdle'] = androidAllowWhileIdle; await _channel.invokeMethod( 'zonedSchedule', - { + { 'id': id, 'title': title, 'body': body, @@ -150,10 +157,10 @@ class AndroidFlutterLocalNotificationsPlugin 'payload': payload ?? '' } ..addAll(scheduledDate.toMap()) - ..addAll(matchDateComponents == null + ..addAll(matchDateTimeComponents == null ? {} : { - 'matchDateTimeComponents': matchDateComponents.index + 'matchDateTimeComponents': matchDateTimeComponents.index })); } @@ -162,18 +169,18 @@ class AndroidFlutterLocalNotificationsPlugin 'Deprecated due to problems with time zones. Use zonedSchedule instead.') Future showDailyAtTime( int id, - String title, - String body, + String? title, + String? body, Time notificationTime, - AndroidNotificationDetails notificationDetails, { - String payload, + AndroidNotificationDetails? notificationDetails, { + String? payload, }) async { validateId(id); - await _channel.invokeMethod('showDailyAtTime', { + await _channel.invokeMethod('showDailyAtTime', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': RepeatInterval.daily.index, 'repeatTime': notificationTime.toMap(), 'platformSpecifics': notificationDetails?.toMap(), @@ -186,20 +193,20 @@ class AndroidFlutterLocalNotificationsPlugin 'Deprecated due to problems with time zones. Use zonedSchedule instead.') Future showWeeklyAtDayAndTime( int id, - String title, - String body, + String? title, + String? body, Day day, Time notificationTime, - AndroidNotificationDetails notificationDetails, { - String payload, + AndroidNotificationDetails? notificationDetails, { + String? payload, }) async { validateId(id); - await _channel.invokeMethod('showWeeklyAtDayAndTime', { + await _channel.invokeMethod('showWeeklyAtDayAndTime', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': RepeatInterval.weekly.index, 'repeatTime': notificationTime.toMap(), 'day': day.value, @@ -208,18 +215,107 @@ class AndroidFlutterLocalNotificationsPlugin }); } + /// Starts an Android foreground service with the given notification. + /// + /// The `id` must not be 0, since Android itself does not allow starting + /// a foreground service with a notification id of 0. + /// + /// Since not all users of this plugin need such a service, it was not + /// added to this plugins Android manifest. Thie means you have to add + /// it if you want to use the foreground service functionality. Add the + /// foreground service permission to your apps `AndroidManifest.xml` like + /// described in the [official Android documentation](https://developer.android.com/guide/components/foreground-services#request-foreground-service-permissions): + /// ```xml + /// + /// ``` + /// Furthermore, add the `service` itself to your `AndroidManifest.xml` + /// (inside the `` tag): + /// ```xml + /// + /// + /// ``` + /// While the `android:name` must exactly match this value, you can configure + /// the other parameters as you like, although it is recommended to copy the + /// value for `android:exported`. Suitable values for + /// `foregroundServiceType` can be found [here](https://developer.android.com/reference/android/app/Service#startForeground(int,%20android.app.Notification,%20int)). + /// + /// The notification of the foreground service can be updated by + /// simply calling this method multiple times. + /// + /// Information on selecting an appropriate `startType` for your app's usecase + /// should be taken from the official Android documentation, check [`Service.onStartCommand`](https://developer.android.com/reference/android/app/Service#onStartCommand(android.content.Intent,%20int,%20int)). + /// The there mentioned constants can be found in [AndroidServiceStartType]. + /// + /// The notification for the foreground service will not be dismissable + /// and automatically removed when using [stopForegroundService]. + /// + /// `foregroundServiceType` is a set of foreground service types to apply to + /// the service start. It might be `null` or omitted, but it must never + /// be empty! + /// If `foregroundServiceType` is set, [`Service.startForeground(int id, Notification notification, int foregroundServiceType)`](https://developer.android.com/reference/android/app/Service#startForeground(int,%20android.app.Notification,%20int)) + /// will be invoked , else [`Service.startForeground(int id, Notification notification)`](https://developer.android.com/reference/android/app/Service#startForeground(int,%20android.app.Notification)) is used. + /// On devices older than [`Build.VERSION_CODES.Q`](https://developer.android.com/reference/android/os/Build.VERSION_CODES#Q), `foregroundServiceType` will be ignored. + /// Note that `foregroundServiceType` (the parameter in this method) + /// must be a subset of the `android:foregroundServiceType` + /// defined in your `AndroidManifest.xml` (the one from the section above)! + Future startForegroundService(int id, String? title, String? body, + {AndroidNotificationDetails? notificationDetails, + String? payload, + AndroidServiceStartType startType = AndroidServiceStartType.startSticky, + Set? foregroundServiceTypes}) { + validateId(id); + if (id == 0) { + throw ArgumentError.value(id, 'id', + 'The id of a notification used for an Android foreground service must not be 0!'); // ignore: lines_longer_than_80_chars + } + if (foregroundServiceTypes?.isEmpty ?? false) { + throw ArgumentError.value(foregroundServiceTypes, 'foregroundServiceType', + 'foregroundServiceType may be null but it must never be empty!'); + } + return _channel.invokeMethod('startForegroundService', { + 'notificationData': { + 'id': id, + 'title': title, + 'body': body, + 'payload': payload ?? '', + 'platformSpecifics': notificationDetails?.toMap(), + }, + 'startType': startType.value, + 'foregroundServiceTypes': foregroundServiceTypes + ?.map((AndroidServiceForegroundType type) => type.value) + .toList() + }); + } + + /// Stops a foreground service. + /// + /// If the foreground service was not started, this function + /// does nothing. + /// + /// It is sufficient to call this method once to stop the + /// foreground service, even if [startForegroundService] was called + /// multiple times. + Future stopForegroundService() => + _channel.invokeMethod('stopForegroundService'); + @override Future show( int id, - String title, - String body, { - AndroidNotificationDetails notificationDetails, - String payload, + String? title, + String? body, { + AndroidNotificationDetails? notificationDetails, + String? payload, }) { validateId(id); return _channel.invokeMethod( 'show', - { + { 'id': id, 'title': title, 'body': body, @@ -232,28 +328,46 @@ class AndroidFlutterLocalNotificationsPlugin @override Future periodicallyShow( int id, - String title, - String body, + String? title, + String? body, RepeatInterval repeatInterval, { - AndroidNotificationDetails notificationDetails, - String payload, + AndroidNotificationDetails? notificationDetails, + String? payload, bool androidAllowWhileIdle = false, }) async { validateId(id); - final Map serializedPlatformSpecifics = + final Map serializedPlatformSpecifics = notificationDetails?.toMap() ?? {}; serializedPlatformSpecifics['allowWhileIdle'] = androidAllowWhileIdle; - await _channel.invokeMethod('periodicallyShow', { + await _channel.invokeMethod('periodicallyShow', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': repeatInterval.index, 'platformSpecifics': serializedPlatformSpecifics, 'payload': payload ?? '', }); } + /// Cancel/remove the notification with the specified id. + /// + /// This applies to notifications that have been scheduled and those that + /// have already been presented. + /// + /// The `tag` parameter specifies the Android tag. If it is provided, + /// then the notification that matches both the id and the tag will + /// be canceled. `tag` has no effect on other platforms. + @override + Future cancel(int id, {String? tag}) async { + validateId(id); + + return _channel.invokeMethod('cancel', { + 'id': id, + 'tag': tag, + }); + } + /// Creates a notification channel group. /// /// This method is only applicable to Android versions 8.0 or newer. @@ -289,8 +403,8 @@ class AndroidFlutterLocalNotificationsPlugin /// This method is only applicable to Android 6.0 or newer and will throw an /// [PlatformException] when called on a device with an incompatible Android /// version. - Future> getActiveNotifications() async { - final List> activeNotifications = + Future?> getActiveNotifications() async { + final List>? activeNotifications = await _channel.invokeListMethod('getActiveNotifications'); return activeNotifications // ignore: always_specify_types @@ -300,15 +414,58 @@ class AndroidFlutterLocalNotificationsPlugin a['title'], a['body'], )) - ?.toList(); + .toList(); + } + + /// Returns the list of all notification channels. + /// + /// This method is only applicable on Android 8.0 or newer. On older versions, + /// it will return an empty list. + Future?> getNotificationChannels() async { + final List>? notificationChannels = + await _channel.invokeListMethod('getNotificationChannels'); + + return notificationChannels + // ignore: always_specify_types + ?.map((a) => AndroidNotificationChannel( + a['id'], + a['name'], + description: a['description'], + groupId: a['groupId'], + showBadge: a['showBadge'], + importance: Importance(a['importance']), + playSound: a['playSound'], + sound: _getNotificationChannelSound(a), + enableLights: a['enableLights'], + enableVibration: a['enableVibration'], + vibrationPattern: a['vibrationPattern'], + ledColor: Color(a['ledColor']), + )) + .toList(); } - Future _handleMethod(MethodCall call) { + AndroidNotificationSound? _getNotificationChannelSound( + Map channelMap) { + final int? soundSourceIndex = channelMap['soundSource']; + AndroidNotificationSound? sound; + if (soundSourceIndex != null) { + if (soundSourceIndex == + AndroidNotificationSoundSource.rawResource.index) { + sound = RawResourceAndroidNotificationSound(channelMap['sound']); + } else if (soundSourceIndex == AndroidNotificationSoundSource.uri.index) { + sound = UriAndroidNotificationSound(channelMap['sound']); + } + } + return sound; + } + + Future _handleMethod(MethodCall call) async { switch (call.method) { case 'selectNotification': - return _onSelectNotification(call.arguments); + _onSelectNotification?.call(call.arguments); + break; default: - return Future.error('Method not defined'); + return await Future.error('Method not defined'); } } } @@ -316,16 +473,13 @@ class AndroidFlutterLocalNotificationsPlugin /// iOS implementation of the local notifications plugin. class IOSFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { - SelectNotificationCallback _onSelectNotification; + SelectNotificationCallback? _onSelectNotification; - DidReceiveLocalNotificationCallback _onDidReceiveLocalNotification; + DidReceiveLocalNotificationCallback? _onDidReceiveLocalNotification; /// Initializes the plugin. /// /// Call this method on application before using the plugin further. - /// This should only be done once. When a notification created by this plugin - /// was used to launch the app, calling `initialize` is what will trigger to - /// the `onSelectNotification` callback to be fire. /// /// Initialisation may also request notification permissions where users will /// see a permissions prompt. This may be fine in cases where it's acceptable @@ -336,10 +490,13 @@ class IOSFlutterLocalNotificationsPlugin /// [IOSInitializationSettings.requestSoundPermission] values to false. /// [requestPermissions] can then be called to request permissions when /// needed. - Future initialize( + /// + /// To handle when a notification launched an application, use + /// [getNotificationAppLaunchDetails]. + Future initialize( IOSInitializationSettings initializationSettings, { - SelectNotificationCallback onSelectNotification, - NotificationActionCallback backgroundHandler, + SelectNotificationCallback? onSelectNotification, + NotificationActionCallback? backgroundHandler, }) async { _onSelectNotification = onSelectNotification; _onDidReceiveLocalNotification = @@ -355,12 +512,12 @@ class IOSFlutterLocalNotificationsPlugin /// Requests the specified permission(s) from user and returns current /// permission status. - Future requestPermissions({ - bool sound, - bool alert, - bool badge, + Future requestPermissions({ + bool sound = false, + bool alert = false, + bool badge = false, }) => - _channel.invokeMethod('requestPermissions', { + _channel.invokeMethod('requestPermissions', { 'sound': sound, 'alert': alert, 'badge': badge, @@ -372,14 +529,14 @@ class IOSFlutterLocalNotificationsPlugin 'Deprecated due to problems with time zones. Use zonedSchedule instead.') Future schedule( int id, - String title, - String body, + String? title, + String? body, DateTime scheduledDate, - IOSNotificationDetails notificationDetails, { - String payload, + IOSNotificationDetails? notificationDetails, { + String? payload, }) async { validateId(id); - await _channel.invokeMethod('schedule', { + await _channel.invokeMethod('schedule', { 'id': id, 'title': title, 'body': body, @@ -404,25 +561,24 @@ class IOSFlutterLocalNotificationsPlugin /// [UILocalNotificationDateInterpretation.wallClockTime]. Future zonedSchedule( int id, - String title, - String body, + String? title, + String? body, TZDateTime scheduledDate, - IOSNotificationDetails notificationDetails, { - @required - UILocalNotificationDateInterpretation - uiLocalNotificationDateInterpretation, - String payload, - DateTimeComponents matchDateTimeComponents, + IOSNotificationDetails? notificationDetails, { + required UILocalNotificationDateInterpretation + uiLocalNotificationDateInterpretation, + String? payload, + DateTimeComponents? matchDateTimeComponents, }) async { validateId(id); - validateDateIsInTheFuture(scheduledDate); + validateDateIsInTheFuture(scheduledDate, matchDateTimeComponents); ArgumentError.checkNotNull(uiLocalNotificationDateInterpretation, 'uiLocalNotificationDateInterpretation'); - final Map serializedPlatformSpecifics = + final Map serializedPlatformSpecifics = notificationDetails?.toMap() ?? {}; await _channel.invokeMethod( 'zonedSchedule', - { + { 'id': id, 'title': title, 'body': body, @@ -444,18 +600,18 @@ class IOSFlutterLocalNotificationsPlugin 'Deprecated due to problems with time zones. Use zonedSchedule instead.') Future showDailyAtTime( int id, - String title, - String body, + String? title, + String? body, Time notificationTime, - IOSNotificationDetails notificationDetails, { - String payload, + IOSNotificationDetails? notificationDetails, { + String? payload, }) async { validateId(id); - await _channel.invokeMethod('showDailyAtTime', { + await _channel.invokeMethod('showDailyAtTime', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': RepeatInterval.daily.index, 'repeatTime': notificationTime.toMap(), 'platformSpecifics': notificationDetails?.toMap(), @@ -468,19 +624,19 @@ class IOSFlutterLocalNotificationsPlugin 'Deprecated due to problems with time zones. Use zonedSchedule instead.') Future showWeeklyAtDayAndTime( int id, - String title, - String body, + String? title, + String? body, Day day, Time notificationTime, - IOSNotificationDetails notificationDetails, { - String payload, + IOSNotificationDetails? notificationDetails, { + String? payload, }) async { validateId(id); - await _channel.invokeMethod('showWeeklyAtDayAndTime', { + await _channel.invokeMethod('showWeeklyAtDayAndTime', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': RepeatInterval.weekly.index, 'repeatTime': notificationTime.toMap(), 'day': day.value, @@ -492,15 +648,15 @@ class IOSFlutterLocalNotificationsPlugin @override Future show( int id, - String title, - String body, { - IOSNotificationDetails notificationDetails, - String payload, + String? title, + String? body, { + IOSNotificationDetails? notificationDetails, + String? payload, }) { validateId(id); return _channel.invokeMethod( 'show', - { + { 'id': id, 'title': title, 'body': body, @@ -513,37 +669,38 @@ class IOSFlutterLocalNotificationsPlugin @override Future periodicallyShow( int id, - String title, - String body, + String? title, + String? body, RepeatInterval repeatInterval, { - IOSNotificationDetails notificationDetails, - String payload, + IOSNotificationDetails? notificationDetails, + String? payload, }) async { validateId(id); - await _channel.invokeMethod('periodicallyShow', { + await _channel.invokeMethod('periodicallyShow', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': repeatInterval.index, 'platformSpecifics': notificationDetails?.toMap(), 'payload': payload ?? '' }); } - Future _handleMethod(MethodCall call) { + Future _handleMethod(MethodCall call) async { switch (call.method) { case 'selectNotification': - return _onSelectNotification(call.arguments); - + _onSelectNotification?.call(call.arguments); + break; case 'didReceiveLocalNotification': - return _onDidReceiveLocalNotification( + _onDidReceiveLocalNotification!( call.arguments['id'], call.arguments['title'], call.arguments['body'], call.arguments['payload']); + break; default: - return Future.error('Method not defined'); + return await Future.error('Method not defined'); } } } @@ -551,7 +708,7 @@ class IOSFlutterLocalNotificationsPlugin /// macOS implementation of the local notifications plugin. class MacOSFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { - SelectNotificationCallback _onSelectNotification; + SelectNotificationCallback? _onSelectNotification; /// Initializes the plugin. /// @@ -569,9 +726,12 @@ class MacOSFlutterLocalNotificationsPlugin /// [MacOSInitializationSettings.requestSoundPermission] values to false. /// [requestPermissions] can then be called to request permissions when /// needed. - Future initialize( + /// + /// To handle when a notification launched an application, use + /// [getNotificationAppLaunchDetails]. + Future initialize( MacOSInitializationSettings initializationSettings, { - SelectNotificationCallback onSelectNotification, + SelectNotificationCallback? onSelectNotification, }) async { _onSelectNotification = onSelectNotification; _channel.setMethodCallHandler(_handleMethod); @@ -581,12 +741,12 @@ class MacOSFlutterLocalNotificationsPlugin /// Requests the specified permission(s) from user and returns current /// permission status. - Future requestPermissions({ - bool sound, - bool alert, - bool badge, + Future requestPermissions({ + bool? sound, + bool? alert, + bool? badge, }) => - _channel.invokeMethod('requestPermissions', { + _channel.invokeMethod('requestPermissions', { 'sound': sound, 'alert': alert, 'badge': badge, @@ -596,20 +756,20 @@ class MacOSFlutterLocalNotificationsPlugin /// relative to a specific time zone. Future zonedSchedule( int id, - String title, - String body, + String? title, + String? body, TZDateTime scheduledDate, - MacOSNotificationDetails notificationDetails, { - String payload, - DateTimeComponents matchDateTimeComponents, + MacOSNotificationDetails? notificationDetails, { + String? payload, + DateTimeComponents? matchDateTimeComponents, }) async { validateId(id); - validateDateIsInTheFuture(scheduledDate); - final Map serializedPlatformSpecifics = + validateDateIsInTheFuture(scheduledDate, matchDateTimeComponents); + final Map serializedPlatformSpecifics = notificationDetails?.toMap() ?? {}; await _channel.invokeMethod( 'zonedSchedule', - { + { 'id': id, 'title': title, 'body': body, @@ -627,15 +787,15 @@ class MacOSFlutterLocalNotificationsPlugin @override Future show( int id, - String title, - String body, { - MacOSNotificationDetails notificationDetails, - String payload, + String? title, + String? body, { + MacOSNotificationDetails? notificationDetails, + String? payload, }) { validateId(id); return _channel.invokeMethod( 'show', - { + { 'id': id, 'title': title, 'body': body, @@ -648,30 +808,31 @@ class MacOSFlutterLocalNotificationsPlugin @override Future periodicallyShow( int id, - String title, - String body, + String? title, + String? body, RepeatInterval repeatInterval, { - MacOSNotificationDetails notificationDetails, - String payload, + MacOSNotificationDetails? notificationDetails, + String? payload, }) async { validateId(id); - await _channel.invokeMethod('periodicallyShow', { + await _channel.invokeMethod('periodicallyShow', { 'id': id, 'title': title, 'body': body, - 'calledAt': DateTime.now().millisecondsSinceEpoch, + 'calledAt': clock.now().millisecondsSinceEpoch, 'repeatInterval': repeatInterval.index, 'platformSpecifics': notificationDetails?.toMap(), 'payload': payload ?? '' }); } - Future _handleMethod(MethodCall call) { + Future _handleMethod(MethodCall call) async { switch (call.method) { case 'selectNotification': - return _onSelectNotification(call.arguments); + _onSelectNotification?.call(call.arguments); + break; default: - return Future.error('Method not defined'); + return await Future.error('Method not defined'); } } } @@ -684,20 +845,20 @@ class MacOSFlutterLocalNotificationsPlugin /// This will add a `dispatcher_handle` and `callback_handle` argument to the /// [arguments] map when the config is correct. void _evaluateBackgroundHandler( - NotificationActionCallback backgroundHandler, + NotificationActionCallback? backgroundHandler, Map arguments, ) { if (backgroundHandler != null) { - final CallbackHandle callback = + final CallbackHandle? callback = PluginUtilities.getCallbackHandle(backgroundHandler); assert(callback != null, ''' The backgroundHandler needs to be either a static function or a top level function to be accessible as a Flutter entry point.'''); - final CallbackHandle dispatcher = + final CallbackHandle? dispatcher = PluginUtilities.getCallbackHandle(callbackDispatcher); - arguments['dispatcher_handle'] = dispatcher.toRawHandle(); - arguments['callback_handle'] = callback.toRawHandle(); + arguments['dispatcher_handle'] = dispatcher!.toRawHandle(); + arguments['callback_handle'] = callback!.toRawHandle(); } } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/active_notification.dart b/flutter_local_notifications/lib/src/platform_specifics/android/active_notification.dart index 235eee0e5..bd2b2f869 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/active_notification.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/active_notification.dart @@ -15,11 +15,11 @@ class ActiveNotification { /// The notification channel's id. /// /// Returned only on Android 8.0 or newer. - final String channelId; + final String? channelId; /// The notification's title. - final String title; + final String? title; /// The notification's content. - final String body; + final String? body; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/bitmap.dart b/flutter_local_notifications/lib/src/platform_specifics/android/bitmap.dart index a9be70d7d..c769a6050 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/bitmap.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/bitmap.dart @@ -1,12 +1,20 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'enums.dart'; + /// Represents a bitmap on Android. -abstract class AndroidBitmap { +abstract class AndroidBitmap { /// The location of the bitmap. - String get bitmap; + T get data; + + /// The subclass source type + AndroidBitmapSource get source; } /// Represents a drawable resource belonging to the Android application that /// should be used as a bitmap on Android. -class DrawableResourceAndroidBitmap implements AndroidBitmap { +class DrawableResourceAndroidBitmap implements AndroidBitmap { /// Constructs an instance of [DrawableResourceAndroidBitmap]. const DrawableResourceAndroidBitmap(this._bitmap); @@ -16,11 +24,14 @@ class DrawableResourceAndroidBitmap implements AndroidBitmap { /// /// For example if the drawable resource is located at `res/drawable/app_icon.png`, the bitmap should be `app_icon` @override - String get bitmap => _bitmap; + String get data => _bitmap; + + @override + AndroidBitmapSource get source => AndroidBitmapSource.drawable; } /// Represents a file path that should be used for a bitmap on Android. -class FilePathAndroidBitmap implements AndroidBitmap { +class FilePathAndroidBitmap implements AndroidBitmap { /// Constructs an instance of [FilePathAndroidBitmap]. const FilePathAndroidBitmap(this._bitmap); @@ -29,5 +40,27 @@ class FilePathAndroidBitmap implements AndroidBitmap { /// A file path on the Android device that refers to the location of the /// bitmap. @override - String get bitmap => _bitmap; + String get data => _bitmap; + + @override + AndroidBitmapSource get source => AndroidBitmapSource.filePath; +} + +/// Represents a base64 encoded AndroidBitmap. +class ByteArrayAndroidBitmap implements AndroidBitmap { + /// Constructs an instance of [ByteArrayAndroidBitmap]. + const ByteArrayAndroidBitmap(this._bitmap); + + /// Constructs an instance of [ByteArrayAndroidBitmap] from a base64 string. + factory ByteArrayAndroidBitmap.fromBase64String(String base64Image) => + ByteArrayAndroidBitmap(base64Decode(base64Image)); + + final Uint8List _bitmap; + + /// A base64 encoded Bitmap string. + @override + Uint8List get data => _bitmap; + + @override + AndroidBitmapSource get source => AndroidBitmapSource.byteArray; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/enums.dart b/flutter_local_notifications/lib/src/platform_specifics/android/enums.dart index 6939f85f1..4336bffbc 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/enums.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/enums.dart @@ -1,10 +1,15 @@ +import 'package:flutter/cupertino.dart'; + /// Specifies the source for a bitmap used by Android notifications. enum AndroidBitmapSource { /// A drawable. drawable, /// A file path. - filePath + filePath, + + /// A byte array bitmap. + byteArray, } /// Specifies the source for icons. @@ -19,7 +24,10 @@ enum AndroidIconSource { contentUri, /// A Flutter asset that is a bitmap. - flutterBitmapAsset + flutterBitmapAsset, + + /// A byte array bitmap. + byteArray, } /// The available notification styles on Android. @@ -62,9 +70,99 @@ enum AndroidNotificationChannelAction { update } +/// The available foreground types for an Android service. +@immutable +class AndroidServiceForegroundType { + /// Constructs an instance of [AndroidServiceForegroundType]. + const AndroidServiceForegroundType(this.value); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST). + static const AndroidServiceForegroundType foregroundServiceTypeManifest = + AndroidServiceForegroundType(-1); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE). + static const AndroidServiceForegroundType foregroundServiceTypeNone = + AndroidServiceForegroundType(0); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC). + static const AndroidServiceForegroundType foregroundServiceTypeDataSync = + AndroidServiceForegroundType(1); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK). + static const AndroidServiceForegroundType foregroundServiceTypeMediaPlayback = + AndroidServiceForegroundType(2); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_PHONE_CALL). + static const AndroidServiceForegroundType foregroundServiceTypePhoneCall = + AndroidServiceForegroundType(4); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_LOCATION). + static const AndroidServiceForegroundType foregroundServiceTypeLocation = + AndroidServiceForegroundType(8); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE). + static const AndroidServiceForegroundType + foregroundServiceTypeConnectedDevice = AndroidServiceForegroundType(16); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION). + static const AndroidServiceForegroundType + foregroundServiceTypeMediaProjection = AndroidServiceForegroundType(32); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_CAMERA). + static const AndroidServiceForegroundType foregroundServiceTypeCamera = + AndroidServiceForegroundType(64); + + /// Corresponds to [`ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_MICROPHONE). + static const AndroidServiceForegroundType foregroundServiceTypeMicrophone = + AndroidServiceForegroundType(128); + + /// The integer representation. + final int value; + + @override + int get hashCode => value; + + @override + bool operator ==(Object other) => + other is AndroidServiceForegroundType && other.value == value; +} + +/// The available start types for an Android service. +@immutable +class AndroidServiceStartType { + /// Constructs an instance of [AndroidServiceStartType]. + const AndroidServiceStartType(this.value); + + /// Corresponds to [`Service.START_STICKY_COMPATIBILITY`](https://developer.android.com/reference/android/app/Service#START_STICKY_COMPATIBILITY). + static const AndroidServiceStartType startStickyCompatibility = + AndroidServiceStartType(0); + + /// Corresponds to [`Service.START_STICKY`](https://developer.android.com/reference/android/app/Service#START_STICKY). + static const AndroidServiceStartType startSticky = AndroidServiceStartType(1); + + /// Corresponds to [`Service.START_NOT_STICKY`](https://developer.android.com/reference/android/app/Service#START_NOT_STICKY). + static const AndroidServiceStartType startNotSticky = + AndroidServiceStartType(2); + + /// Corresponds to [`Service.START_REDELIVER_INTENT`](https://developer.android.com/reference/android/app/Service#START_REDELIVER_INTENT). + static const AndroidServiceStartType startRedeliverIntent = + AndroidServiceStartType(3); + + /// The integer representation. + final int value; + + @override + int get hashCode => value; + + @override + bool operator ==(Object other) => + other is AndroidServiceStartType && other.value == value; +} + /// The available importance levels for Android notifications. /// /// Required for Android 8.0 or newer. +@immutable class Importance { /// Constructs an instance of [Importance]. const Importance(this.value); @@ -96,9 +194,16 @@ class Importance { /// The integer representation. final int value; + + @override + int get hashCode => value; + + @override + bool operator ==(Object other) => other is Importance && other.value == value; } /// Priority for notifications on Android 7.1 and lower. +@immutable class Priority { /// Constructs an instance of [Priority]. const Priority(this.value); @@ -124,6 +229,12 @@ class Priority { /// The integer representation. final int value; + + @override + int get hashCode => value; + + @override + bool operator ==(Object other) => other is Priority && other.value == value; } /// The available alert behaviours for grouped notifications. diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/icon.dart b/flutter_local_notifications/lib/src/platform_specifics/android/icon.dart index f9931f547..25f66fa9e 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/icon.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/icon.dart @@ -1,12 +1,20 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'enums.dart'; + /// Represents an icon on Android. -abstract class AndroidIcon { +abstract class AndroidIcon { /// The location to the icon; - String get icon; + T get data; + + /// The subclass source type + AndroidIconSource get source; } /// Represents a drawable resource belonging to the Android application that /// should be used as an icon on Android. -class DrawableResourceAndroidIcon implements AndroidIcon { +class DrawableResourceAndroidIcon implements AndroidIcon { /// Constructs an instance of [DrawableResourceAndroidIcon]. const DrawableResourceAndroidIcon(this._icon); @@ -16,12 +24,15 @@ class DrawableResourceAndroidIcon implements AndroidIcon { /// /// For example if the drawable resource is located at `res/drawable/app_icon.png`, the icon should be `app_icon` @override - String get icon => _icon; + String get data => _icon; + + @override + AndroidIconSource get source => AndroidIconSource.drawableResource; } /// Represents a file path to a bitmap that should be used for as an icon on /// Android. -class BitmapFilePathAndroidIcon implements AndroidIcon { +class BitmapFilePathAndroidIcon implements AndroidIcon { /// Constructs an instance of [BitmapFilePathAndroidIcon]. const BitmapFilePathAndroidIcon(this._icon); @@ -29,11 +40,14 @@ class BitmapFilePathAndroidIcon implements AndroidIcon { /// A file path on the Android device that refers to the location of the icon. @override - String get icon => _icon; + String get data => _icon; + + @override + AndroidIconSource get source => AndroidIconSource.bitmapFilePath; } /// Represents a content URI that should be used for as an icon on Android. -class ContentUriAndroidIcon implements AndroidIcon { +class ContentUriAndroidIcon implements AndroidIcon { /// Constructs an instance of [ContentUriAndroidIcon]. const ContentUriAndroidIcon(this._icon); @@ -41,12 +55,15 @@ class ContentUriAndroidIcon implements AndroidIcon { /// A content URI that refers to the location of the icon. @override - String get icon => _icon; + String get data => _icon; + + @override + AndroidIconSource get source => AndroidIconSource.contentUri; } /// Represents a bitmap asset belonging to the Flutter application that should /// be used for as an icon on Android. -class FlutterBitmapAssetAndroidIcon implements AndroidIcon { +class FlutterBitmapAssetAndroidIcon implements AndroidIcon { /// Constructs an instance of [FlutterBitmapAssetAndroidIcon]. const FlutterBitmapAssetAndroidIcon(this._icon); @@ -64,5 +81,28 @@ class FlutterBitmapAssetAndroidIcon implements AndroidIcon { /// /// then the path to the asset would be `icons/coworker.png`. @override - String get icon => _icon; + String get data => _icon; + + @override + AndroidIconSource get source => AndroidIconSource.flutterBitmapAsset; +} + +/// Represents a bitmap asset belonging to the Flutter application that should +/// be used for as an icon on Android. +class ByteArrayAndroidIcon implements AndroidIcon { + /// Constructs an instance of [FlutterBitmapAssetAndroidIcon]. + const ByteArrayAndroidIcon(this._icon); + + /// Constructs an instance of [ByteArrayAndroidIcon] from a base64 string. + factory ByteArrayAndroidIcon.fromBase64String(String base64Image) => + ByteArrayAndroidIcon(base64Decode(base64Image)); + + final Uint8List _icon; + + /// Byte array data of the icon. + @override + Uint8List get data => _icon; + + @override + AndroidIconSource get source => AndroidIconSource.byteArray; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/message.dart b/flutter_local_notifications/lib/src/platform_specifics/android/message.dart index cf5827124..4d42e2c49 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/message.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/message.dart @@ -9,11 +9,7 @@ class Message { this.person, { this.dataMimeType, this.dataUri, - }) : assert( - timestamp != null, - 'timestamp must be provided', - ), - assert( + }) : assert( (dataMimeType == null && dataUri == null) || (dataMimeType != null && dataUri != null), 'Must provide both dataMimeType and dataUri together or not at all.', @@ -33,13 +29,13 @@ class Message { /// When this is set to `null` the `Person` given to /// [MessagingStyleInformation.person] i.e. this would indicate that the /// message was sent from the user. - final Person person; + final Person? person; /// MIME type for this message context when the [dataUri] is provided. - final String dataMimeType; + final String? dataMimeType; /// Uri containing the content. /// /// The original text will be used if the content or MIME type isn't supported - final String dataUri; + final String? dataUri; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart index f6388cd59..0475fcaab 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart @@ -1,6 +1,6 @@ -import 'bitmap.dart'; +import 'package:flutter_local_notifications/src/platform_specifics/android/bitmap.dart'; + import 'enums.dart'; -import 'icon.dart'; import 'initialization_settings.dart'; import 'message.dart'; import 'notification_channel.dart'; @@ -21,7 +21,7 @@ extension AndroidInitializationSettingsMapper on AndroidInitializationSettings { } extension MessageMapper on Message { - Map toMap() => { + Map toMap() => { 'text': text, 'timestamp': timestamp.millisecondsSinceEpoch, 'person': person?.toMap(), @@ -32,7 +32,7 @@ extension MessageMapper on Message { extension AndroidNotificationChannelGroupMapper on AndroidNotificationChannelGroup { - Map toMap() => { + Map toMap() => { 'id': id, 'name': name, 'description': description, @@ -40,7 +40,7 @@ extension AndroidNotificationChannelGroupMapper } extension AndroidNotificationChannelMapper on AndroidNotificationChannel { - Map toMap() => { + Map toMap() => { 'id': id, 'name': name, 'description': description, @@ -56,12 +56,12 @@ extension AndroidNotificationChannelMapper on AndroidNotificationChannel { 'ledColorGreen': ledColor?.green, 'ledColorBlue': ledColor?.blue, 'channelAction': - AndroidNotificationChannelAction.createIfNotExists?.index, + AndroidNotificationChannelAction.createIfNotExists.index, }..addAll(_convertNotificationSoundToMap(sound)); } Map _convertNotificationSoundToMap( - AndroidNotificationSound sound) { + AndroidNotificationSound? sound) { if (sound is RawResourceAndroidNotificationSound) { return { 'sound': sound.sound, @@ -78,7 +78,7 @@ Map _convertNotificationSoundToMap( } extension PersonMapper on Person { - Map toMap() => { + Map toMap() => { 'bot': bot, 'important': important, 'key': key, @@ -87,48 +87,32 @@ extension PersonMapper on Person { }..addAll(_convertIconToMap()); Map _convertIconToMap() { - if (icon is DrawableResourceAndroidIcon) { - return { - 'icon': icon.icon, - 'iconSource': AndroidIconSource.drawableResource.index, - }; - } else if (icon is BitmapFilePathAndroidIcon) { - return { - 'icon': icon.icon, - 'iconSource': AndroidIconSource.bitmapFilePath.index, - }; - } else if (icon is ContentUriAndroidIcon) { - return { - 'icon': icon.icon, - 'iconSource': AndroidIconSource.contentUri.index, - }; - } else if (icon is FlutterBitmapAssetAndroidIcon) { - return { - 'icon': icon.icon, - 'iconSource': AndroidIconSource.flutterBitmapAsset.index, - }; - } else { + if (icon == null) { return {}; } + return { + 'icon': icon!.data, + 'iconSource': icon!.source.index, + }; } } extension DefaultStyleInformationMapper on DefaultStyleInformation { - Map toMap() => _convertDefaultStyleInformationToMap(this); + Map toMap() => _convertDefaultStyleInformationToMap(this); } -Map _convertDefaultStyleInformationToMap( +Map _convertDefaultStyleInformationToMap( DefaultStyleInformation styleInformation) => - { + { 'htmlFormatContent': styleInformation.htmlFormatContent, 'htmlFormatTitle': styleInformation.htmlFormatTitle }; extension BigPictureStyleInformationMapper on BigPictureStyleInformation { - Map toMap() => _convertDefaultStyleInformationToMap(this) + Map toMap() => _convertDefaultStyleInformationToMap(this) ..addAll(_convertBigPictureToMap()) ..addAll(_convertLargeIconToMap()) - ..addAll({ + ..addAll({ 'contentTitle': contentTitle, 'summaryText': summaryText, 'htmlFormatContentTitle': htmlFormatContentTitle, @@ -136,42 +120,25 @@ extension BigPictureStyleInformationMapper on BigPictureStyleInformation { 'hideExpandedLargeIcon': hideExpandedLargeIcon }); - Map _convertBigPictureToMap() { - if (bigPicture is DrawableResourceAndroidBitmap) { - return { - 'bigPicture': bigPicture.bitmap, - 'bigPictureBitmapSource': AndroidBitmapSource.drawable.index, - }; - } else if (bigPicture is FilePathAndroidBitmap) { - return { - 'bigPicture': bigPicture.bitmap, - 'bigPictureBitmapSource': AndroidBitmapSource.filePath.index, + Map _convertBigPictureToMap() => { + 'bigPicture': bigPicture.data, + 'bigPictureBitmapSource': bigPicture.source.index, }; - } else { - return {}; - } - } Map _convertLargeIconToMap() { - if (largeIcon is DrawableResourceAndroidBitmap) { - return { - 'largeIcon': largeIcon.bitmap, - 'largeIconBitmapSource': AndroidBitmapSource.drawable.index, - }; - } else if (largeIcon is FilePathAndroidBitmap) { - return { - 'largeIcon': largeIcon.bitmap, - 'largeIconBitmapSource': AndroidBitmapSource.filePath.index, - }; - } else { + if (largeIcon == null) { return {}; } + return { + 'largeIcon': largeIcon!.data, + 'largeIconBitmapSource': largeIcon!.source.index, + }; } } extension BigTexStyleInformationMapper on BigTextStyleInformation { - Map toMap() => _convertDefaultStyleInformationToMap(this) - ..addAll({ + Map toMap() => _convertDefaultStyleInformationToMap(this) + ..addAll({ 'bigText': bigText, 'htmlFormatBigText': htmlFormatBigText, 'contentTitle': contentTitle, @@ -182,37 +149,37 @@ extension BigTexStyleInformationMapper on BigTextStyleInformation { } extension InboxStyleInformationMapper on InboxStyleInformation { - Map toMap() => _convertDefaultStyleInformationToMap(this) - ..addAll({ + Map toMap() => _convertDefaultStyleInformationToMap(this) + ..addAll({ 'contentTitle': contentTitle, 'htmlFormatContentTitle': htmlFormatContentTitle, 'summaryText': summaryText, 'htmlFormatSummaryText': htmlFormatSummaryText, - 'lines': lines ?? [], + 'lines': lines, 'htmlFormatLines': htmlFormatLines }); } extension MessagingStyleInformationMapper on MessagingStyleInformation { - Map toMap() => _convertDefaultStyleInformationToMap(this) - ..addAll({ - 'person': person?.toMap(), + Map toMap() => _convertDefaultStyleInformationToMap(this) + ..addAll({ + 'person': person.toMap(), 'conversationTitle': conversationTitle, 'groupConversation': groupConversation, 'messages': messages - ?.map((m) => m?.toMap()) // ignore: always_specify_types - ?.toList() + ?.map((m) => m.toMap()) // ignore: always_specify_types + .toList() }); } extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { - Map toMap() => { + Map toMap() => { 'icon': icon, 'channelId': channelId, 'channelName': channelName, 'channelDescription': channelDescription, 'channelShowBadge': channelShowBadge, - 'channelAction': channelAction?.index, + 'channelAction': channelAction.index, 'importance': importance.value, 'priority': priority.value, 'playSound': playSound, @@ -249,48 +216,50 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'fullScreenIntent': fullScreenIntent, 'shortcutId': shortcutId, 'additionalFlags': additionalFlags, + 'subText': subText, + 'tag': tag, } ..addAll(_convertActionsToMap(actions)) ..addAll(_convertStyleInformationToMap()) ..addAll(_convertNotificationSoundToMap(sound)) ..addAll(_convertLargeIconToMap()); - Map _convertStyleInformationToMap() { + Map _convertStyleInformationToMap() { if (styleInformation is BigPictureStyleInformation) { - return { + return { 'style': AndroidNotificationStyle.bigPicture.index, 'styleInformation': - (styleInformation as BigPictureStyleInformation)?.toMap(), + (styleInformation as BigPictureStyleInformation?)?.toMap(), }; } else if (styleInformation is BigTextStyleInformation) { - return { + return { 'style': AndroidNotificationStyle.bigText.index, 'styleInformation': - (styleInformation as BigTextStyleInformation)?.toMap(), + (styleInformation as BigTextStyleInformation?)?.toMap(), }; } else if (styleInformation is InboxStyleInformation) { - return { + return { 'style': AndroidNotificationStyle.inbox.index, 'styleInformation': - (styleInformation as InboxStyleInformation)?.toMap(), + (styleInformation as InboxStyleInformation?)?.toMap(), }; } else if (styleInformation is MessagingStyleInformation) { - return { + return { 'style': AndroidNotificationStyle.messaging.index, 'styleInformation': - (styleInformation as MessagingStyleInformation)?.toMap(), + (styleInformation as MessagingStyleInformation?)?.toMap(), }; } else if (styleInformation is MediaStyleInformation) { - return { + return { 'style': AndroidNotificationStyle.media.index, 'styleInformation': - (styleInformation as MediaStyleInformation)?.toMap(), + (styleInformation as MediaStyleInformation?)?.toMap(), }; } else if (styleInformation is DefaultStyleInformation) { - return { + return { 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': - (styleInformation as DefaultStyleInformation)?.toMap(), + (styleInformation as DefaultStyleInformation?)?.toMap(), }; } else { return { @@ -301,49 +270,30 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { } Map _convertLargeIconToMap() { - if (largeIcon is DrawableResourceAndroidBitmap) { - return { - 'largeIcon': largeIcon.bitmap, - 'largeIconBitmapSource': AndroidBitmapSource.drawable.index, - }; - } else if (largeIcon is FilePathAndroidBitmap) { - return { - 'largeIcon': largeIcon.bitmap, - 'largeIconBitmapSource': AndroidBitmapSource.filePath.index, - }; - } else { + if (largeIcon == null) { return {}; } + return { + 'largeIcon': largeIcon!.data, + 'largeIconBitmapSource': largeIcon!.source.index, + }; } Map _convertActionsToMap( - List actions) { - Map _convertActionIconToMap(AndroidBitmap icon) { - if (icon is DrawableResourceAndroidBitmap) { - return { - 'icon': icon.bitmap, - 'iconBitmapSource': AndroidBitmapSource.drawable.index, - }; - } else if (icon is FilePathAndroidBitmap) { - return { - 'icon': icon.bitmap, - 'iconBitmapSource': AndroidBitmapSource.filePath.index, - }; - } else { - return {}; - } - } - + List? actions) { if (actions == null) { return {}; } - return { + return { 'actions': actions .map( (AndroidNotificationAction e) => { 'id': e.id, 'title': e.title, - ..._convertActionIconToMap(e.icon), + if (e.icon != null) ...{ + 'icon': e.icon!.data, + 'iconBitmapSource': e.icon!.source.index, + }, 'contextual': e.contextual, 'showsUserInterface': e.showsUserInterface, 'allowGeneratedReplies': e.allowGeneratedReplies, diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel.dart index 95c95b26a..f580efca0 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel.dart @@ -9,8 +9,8 @@ class AndroidNotificationChannel { /// Constructs an instance of [AndroidNotificationChannel]. const AndroidNotificationChannel( this.id, - this.name, - this.description, { + this.name, { + this.description, this.groupId, this.importance = Importance.defaultImportance, this.playSound = true, @@ -29,10 +29,10 @@ class AndroidNotificationChannel { final String name; /// The channel's description. - final String description; + final String? description; /// The id of the group that the channel belongs to. - final String groupId; + final String? groupId; /// The importance of the notification. final Importance importance; @@ -49,7 +49,7 @@ class AndroidNotificationChannel { /// If [playSound] is set to true but this is not specified then the default /// sound is played. Tied to the specified channel and cannot be changed /// after the channel has been created for the first time. - final AndroidNotificationSound sound; + final AndroidNotificationSound? sound; /// Indicates if vibration should be enabled when the notification is /// displayed. @@ -69,13 +69,13 @@ class AndroidNotificationChannel { /// Requires setting [enableVibration] to true for it to work. /// Tied to the specified channel and cannot be changed after the channel has /// been created for the first time. - final Int64List vibrationPattern; + final Int64List? vibrationPattern; /// Specifies the light color of the notification. /// /// Tied to the specified channel and cannot be changed after the channel has /// been created for the first time. - final Color ledColor; + final Color? ledColor; /// Whether notifications posted to this channel can appear as application /// icon badges in a Launcher diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel_group.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel_group.dart index da9e7171b..7a0559e71 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel_group.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_channel_group.dart @@ -16,5 +16,5 @@ class AndroidNotificationChannelGroup { /// The description of this group. /// /// Only applicable to Android 9.0 or newer. - final String description; + final String? description; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart index 855a049d7..a7d082740 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart @@ -30,7 +30,7 @@ class AndroidNotificationActionInput { final bool allowFreeFormInput; /// Set a label to be displayed to the user when collecting this input. - final String label; + final String? label; /// Specifies whether the user can provide arbitrary values. final Set allowedMimeTypes; @@ -50,9 +50,9 @@ class AndroidNotificationAction { this.id, this.title, { this.icon, - this.contextual, - this.showsUserInterface, - this.allowGeneratedReplies, + this.contextual = false, + this.showsUserInterface = false, + this.allowGeneratedReplies = false, this.inputs = const [], }); @@ -64,7 +64,7 @@ class AndroidNotificationAction { final String title; /// Icon to show for this action. - final AndroidBitmap icon; + final AndroidBitmap? icon; /// Sets whether this Action is a contextual action, i.e. whether the action /// is dependent on the notification message body. An example of a contextual @@ -91,8 +91,8 @@ class AndroidNotificationDetails { /// Constructs an instance of [AndroidNotificationDetails]. const AndroidNotificationDetails( this.channelId, - this.channelName, - this.channelDescription, { + this.channelName, { + this.channelDescription, this.icon, this.importance = Importance.defaultImportance, this.priority = Priority.defaultPriority, @@ -102,13 +102,13 @@ class AndroidNotificationDetails { this.enableVibration = true, this.vibrationPattern, this.groupKey, - this.setAsGroupSummary, + this.setAsGroupSummary = false, this.groupAlertBehavior = GroupAlertBehavior.all, this.autoCancel = true, - this.ongoing, + this.ongoing = false, this.color, this.largeIcon, - this.onlyAlertOnce, + this.onlyAlertOnce = false, this.showWhen = true, this.when, this.usesChronometer = false, @@ -129,6 +129,8 @@ class AndroidNotificationDetails { this.fullScreenIntent = false, this.shortcutId, this.additionalFlags, + this.subText, + this.tag, this.actions, }); @@ -136,7 +138,7 @@ class AndroidNotificationDetails { /// /// When this is set to `null`, the default icon given to /// [AndroidInitializationSettings.defaultIcon] will be used. - final String icon; + final String? icon; /// The channel's id. /// @@ -150,8 +152,8 @@ class AndroidNotificationDetails { /// The channel's description. /// - /// Required for Android 8.0 or newer. - final String channelDescription; + /// This property is only applicable to Android versions 8.0 or newer. + final String? channelDescription; /// Whether notifications posted to this channel can appear as application /// icon badges in a Launcher @@ -177,7 +179,7 @@ class AndroidNotificationDetails { /// /// For Android 8.0 or newer, this is tied to the specified channel and cannot /// be changed after the channel has been created for the first time. - final AndroidNotificationSound sound; + final AndroidNotificationSound? sound; /// Indicates if vibration should be enabled when the notification is /// displayed. @@ -197,16 +199,16 @@ class AndroidNotificationDetails { /// Requires setting [enableVibration] to true for it to work. /// For Android 8.0 or newer, this is tied to the specified channel and cannot /// be changed after the channel has been created for the first time. - final Int64List vibrationPattern; + final Int64List? vibrationPattern; /// Specifies the information of the rich notification style to apply to the /// notification. - final StyleInformation styleInformation; + final StyleInformation? styleInformation; /// Specifies the group that this notification belongs to. /// /// For Android 7.0 or newer. - final String groupKey; + final String? groupKey; /// Specifies if this notification will function as the summary for grouped /// notifications. @@ -226,10 +228,10 @@ class AndroidNotificationDetails { final bool ongoing; /// Specifies the color. - final Color color; + final Color? color; /// Specifics the large icon to use. - final AndroidBitmap largeIcon; + final AndroidBitmap? largeIcon; /// Specifies if you would only like the sound, vibrate and ticker to be /// played if the notification is not already showing. @@ -250,7 +252,7 @@ class AndroidNotificationDetails { /// "Unix epoch" 1970-01-01T00:00:00Z (UTC). If it's not specified but a /// timestamp should be shown (i.e. [showWhen] is set to `true`), /// then Android will default to showing when the notification occurred. - final int when; + final int? when; /// Show [when] as a stopwatch. /// @@ -275,20 +277,20 @@ class AndroidNotificationDetails { /// /// For Android 8.0 or newer, this is tied to the specified channel and cannot /// be changed after the channel has been created for the first time. - final Color ledColor; + final Color? ledColor; /// Specifies how long the light colour will remain on. /// /// This property is only applicable to Android versions older than 8.0. - final int ledOnMs; + final int? ledOnMs; /// Specifies how long the light colour will remain off. /// /// This property is only applicable to Android versions older than 8.0. - final int ledOffMs; + final int? ledOffMs; /// Specifies the "ticker" text which is sent to accessibility services. - final String ticker; + final String? ticker; /// The action to take for managing notification channels. /// @@ -297,16 +299,16 @@ class AndroidNotificationDetails { final AndroidNotificationChannelAction channelAction; /// Defines the notification visibility on the lockscreen. - final NotificationVisibility visibility; + final NotificationVisibility? visibility; /// The duration in milliseconds after which the notification will be /// cancelled if it hasn't already. - final int timeoutAfter; + final int? timeoutAfter; /// The notification category. /// /// Refer to Android notification API documentation at https://developer.android.com/reference/androidx/core/app/NotificationCompat.html#constants_2 for the available categories - final String category; + final String? category; /// Specifies whether the notification should launch a full-screen intent as /// soon as it triggers. @@ -324,19 +326,46 @@ class AndroidNotificationDetails { /// /// From Android 11, this affects if a messaging-style notification appears /// in the conversation space. - final String shortcutId; + final String? shortcutId; /// Specifies the additional flags. /// /// These flags will get added to the native Android notification's flags field: https://developer.android.com/reference/android/app/Notification#flags /// For a list of a values, refer to the documented constants prefixed with "FLAG_" (without the quotes) at https://developer.android.com/reference/android/app/Notification.html#constants_1. /// For example, use a value of 4 to allow the audio to repeat as documented at https://developer.android.com/reference/android/app/Notification.html#FLAG_INSISTEN - final Int32List additionalFlags; + final Int32List? additionalFlags; /// Specify a list of actions associated with this notifications. /// /// Users will be able tap on the actions without actually launching the App. /// Note that tapping a action will spawn a separate isolate that runs /// **independently** from the main app. - final List actions; + final List? actions; + + /// Provides some additional information that is displayed in the + /// notification. + /// + /// No guarantees are given where exactly it is displayed. This information + /// should only be provided if it provides an essential benefit to the + /// understanding of the notification. The more text you provide the less + /// readable it becomes. For example, an email client should only provide the + /// account name here if more than one email account has been added. + /// + /// As of Android 7.0 this information is displayed in the notification header + /// area. On Android versions before 7.0 this will be shown in the third line + /// of text in the platform notification template. You should not be using + /// setProgress(int, int, boolean) at the same time on those versions; they + /// occupy the same place. + final String? subText; + + /// The notification tag. + /// + /// Showing notification with the same (tag, id) pair as a currently visible + /// notification will replace the old notification with the new one, provided + /// the old notification was one that was not one that was scheduled. In other + /// words, the (tag, id) pair is only applicable for notifications that were + /// requested to be shown immediately. This is because the Android + /// AlarmManager APIs used for scheduling notifications only allow for using + /// the id to uniquely identify alarms. + final String? tag; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/notification_sound.dart b/flutter_local_notifications/lib/src/platform_specifics/android/notification_sound.dart index 0d629ee5d..a43af0f89 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/notification_sound.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/notification_sound.dart @@ -12,11 +12,11 @@ class RawResourceAndroidNotificationSound implements AndroidNotificationSound { /// Constructs an instance of [RawResourceAndroidNotificationSound]. const RawResourceAndroidNotificationSound(this._sound); - final String _sound; + final String? _sound; /// The name of the raw resource for the notification sound. @override - String get sound => _sound; + String get sound => _sound!; } /// Represents a URI on the Android device that should be used for the diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/person.dart b/flutter_local_notifications/lib/src/platform_specifics/android/person.dart index 2e289defa..c57933c94 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/person.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/person.dart @@ -4,9 +4,9 @@ import 'icon.dart'; class Person { /// Constructs an instance of [Person]. const Person({ - this.bot, + this.bot = false, this.icon, - this.important, + this.important = false, this.key, this.name, this.uri, @@ -16,17 +16,17 @@ class Person { final bool bot; /// Icon for this person. - final AndroidIcon icon; + final AndroidIcon? icon; /// Whether or not this is an important person. final bool important; /// Unique identifier for this person. - final String key; + final String? key; /// Name of this person. - final String name; + final String? name; /// Uri for this person. - final String uri; + final String? uri; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_picture_style_information.dart b/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_picture_style_information.dart index e49cec4a1..57c965e78 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_picture_style_information.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_picture_style_information.dart @@ -18,11 +18,11 @@ class BigPictureStyleInformation extends DefaultStyleInformation { }) : super(htmlFormatContent, htmlFormatTitle); /// Overrides ContentTitle in the big form of the template. - final String contentTitle; + final String? contentTitle; /// Set the first line of text after the detail section in the big form of /// the template. - final String summaryText; + final String? summaryText; /// Specifies if the overridden ContentTitle should have formatting applied /// through HTML markup. @@ -34,10 +34,10 @@ class BigPictureStyleInformation extends DefaultStyleInformation { /// The bitmap that will override the large icon when the big notification is /// shown. - final AndroidBitmap largeIcon; + final AndroidBitmap? largeIcon; /// The bitmap to be used as the payload for the BigPicture notification. - final AndroidBitmap bigPicture; + final AndroidBitmap bigPicture; /// Hides the large icon when showing the expanded notification. final bool hideExpandedLargeIcon; diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_text_style_information.dart b/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_text_style_information.dart index 8cd161083..ccd2be6f8 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_text_style_information.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/styles/big_text_style_information.dart @@ -20,11 +20,11 @@ class BigTextStyleInformation extends DefaultStyleInformation { final String bigText; /// Overrides ContentTitle in the big form of the template. - final String contentTitle; + final String? contentTitle; /// Set the first line of text after the detail section in the big form of /// the template. - final String summaryText; + final String? summaryText; /// Specifies if formatting should be applied to the longer text through /// HTML markup. diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/styles/inbox_style_information.dart b/flutter_local_notifications/lib/src/platform_specifics/android/styles/inbox_style_information.dart index 1d146b304..a20e4d358 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/styles/inbox_style_information.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/styles/inbox_style_information.dart @@ -16,11 +16,11 @@ class InboxStyleInformation extends DefaultStyleInformation { }) : super(htmlFormatContent, htmlFormatTitle); /// Overrides ContentTitle in the big form of the template. - final String contentTitle; + final String? contentTitle; /// Set the first line of text after the detail section in the big form of /// the template. - final String summaryText; + final String? summaryText; /// The lines that form part of the digest section for inbox-style /// notifications. diff --git a/flutter_local_notifications/lib/src/platform_specifics/android/styles/messaging_style_information.dart b/flutter_local_notifications/lib/src/platform_specifics/android/styles/messaging_style_information.dart index b0bf81af5..1e4bdaf6a 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/styles/messaging_style_information.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/styles/messaging_style_information.dart @@ -13,18 +13,17 @@ class MessagingStyleInformation extends DefaultStyleInformation { this.messages, bool htmlFormatContent = false, bool htmlFormatTitle = false, - }) : assert(person?.name != null, 'Must provide the details of the person'), - super(htmlFormatContent, htmlFormatTitle); + }) : super(htmlFormatContent, htmlFormatTitle); /// The person displayed for any messages that are sent by the user. final Person person; /// The title to be displayed on this conversation. - final String conversationTitle; + final String? conversationTitle; /// Whether this conversation notification represents a group. - final bool groupConversation; + final bool? groupConversation; /// Messages to be displayed by this notification - final List messages; + final List? messages; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/ios/initialization_settings.dart b/flutter_local_notifications/lib/src/platform_specifics/ios/initialization_settings.dart index da23d14f3..c4449b25a 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/ios/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/ios/initialization_settings.dart @@ -40,8 +40,8 @@ class IOSNotificationAction { factory IOSNotificationAction.text( String identifier, String title, { - @required String buttonTitle, - String placeholder, + required String buttonTitle, + String? placeholder, Set options = const {}, }) => @@ -79,10 +79,10 @@ class IOSNotificationAction { /// The localized title of the text input button that is displayed to the /// user. - final String buttonTitle; + final String? buttonTitle; /// The localized placeholder text to display in the text input field. - final String placeholder; + final String? placeholder; } /// Corresponds to the `UNNotificationCategory` type which is used to configure @@ -121,12 +121,7 @@ class IOSInitializationSettings { this.defaultPresentBadge = true, this.onDidReceiveLocalNotification, this.notificationCategories = const [], - }) : assert(requestAlertPermission != null), - assert(requestSoundPermission != null), - assert(requestBadgePermission != null), - assert(defaultPresentAlert != null), - assert(defaultPresentBadge != null), - assert(defaultPresentSound != null); + }); /// Request permission to display an alert. /// @@ -172,7 +167,7 @@ class IOSInitializationSettings { /// in the foreground. /// /// This property is only applicable to iOS versions older than 10. - final DidReceiveLocalNotificationCallback onDidReceiveLocalNotification; + final DidReceiveLocalNotificationCallback? onDidReceiveLocalNotification; /// Configure the notification categories ([IOSNotificationCategory]) /// available. This allows for fine-tuning of preview display. diff --git a/flutter_local_notifications/lib/src/platform_specifics/ios/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/ios/method_channel_mappers.dart index ce77f0675..e9a4658dc 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/ios/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/ios/method_channel_mappers.dart @@ -14,8 +14,8 @@ extension IOSNotificationActionMapper on IOSNotificationAction { .map((e) => e.index + 1) // ignore: always_specify_types .toList(), 'type': describeEnum(type), - if (buttonTitle != null) 'buttonTitle': buttonTitle, - if (placeholder != null) 'placeholder': placeholder, + if (buttonTitle != null) 'buttonTitle': buttonTitle!, + if (placeholder != null) 'placeholder': placeholder!, }; } @@ -40,8 +40,8 @@ extension IOSInitializationSettingsMapper on IOSInitializationSettings { 'defaultPresentSound': defaultPresentSound, 'defaultPresentBadge': defaultPresentBadge, 'notificationCategories': notificationCategories - ?.map((e) => e.toMap()) // ignore: always_specify_types - ?.toList(), + .map((e) => e.toMap()) // ignore: always_specify_types + .toList(), }; } @@ -53,16 +53,17 @@ extension IOSNotificationAttachmentMapper on IOSNotificationAttachment { } extension IOSNotificationDetailsMapper on IOSNotificationDetails { - Map toMap() => { + Map toMap() => { 'presentAlert': presentAlert, 'presentSound': presentSound, 'presentBadge': presentBadge, 'subtitle': subtitle, 'sound': sound, 'badgeNumber': badgeNumber, + 'threadIdentifier': threadIdentifier, 'attachments': attachments ?.map((a) => a.toMap()) // ignore: always_specify_types - ?.toList(), + .toList(), 'categoryIdentifier': categoryIdentifier, }; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/ios/notification_attachment.dart b/flutter_local_notifications/lib/src/platform_specifics/ios/notification_attachment.dart index c4a153f62..617146f15 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/ios/notification_attachment.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/ios/notification_attachment.dart @@ -4,7 +4,7 @@ class IOSNotificationAttachment { const IOSNotificationAttachment( this.filePath, { this.identifier, - }) : assert(filePath != null); + }); /// The local file path to the attachment. /// @@ -15,5 +15,5 @@ class IOSNotificationAttachment { /// The unique identifier for the attachment. /// /// When left empty, the iOS APIs will generate a unique identifier - final String identifier; + final String? identifier; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/ios/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/ios/notification_details.dart index 1f4b9635b..1a92fee69 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/ios/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/ios/notification_details.dart @@ -11,6 +11,7 @@ class IOSNotificationDetails { this.badgeNumber, this.attachments, this.subtitle, + this.threadIdentifier, this.categoryIdentifier, }); @@ -21,7 +22,7 @@ class IOSNotificationDetails { /// to [IOSInitializationSettings.defaultPresentAlert]. /// /// This property is only applicable to iOS 10 or newer. - final bool presentAlert; + final bool? presentAlert; /// Play a sound when the notification is triggered while app is in /// the foreground. @@ -30,7 +31,7 @@ class IOSNotificationDetails { /// [IOSInitializationSettings.defaultPresentSound]. /// /// This property is only applicable to iOS 10 or newer. - final bool presentSound; + final bool? presentSound; /// Apply the badge value when the notification is triggered while app is in /// the foreground. @@ -39,14 +40,14 @@ class IOSNotificationDetails { /// [IOSInitializationSettings.defaultPresentBadge]. /// /// This property is only applicable to iOS 10 or newer. - final bool presentBadge; + final bool? presentBadge; /// Specifies the name of the file to play for the notification. /// /// Requires setting [presentSound] to true. If [presentSound] is set to true /// but [sound] isn't specified then it will use the default notification /// sound. - final String sound; + final String? sound; /// Specify the number to display as the app icon's badge when the /// notification arrives. @@ -54,21 +55,27 @@ class IOSNotificationDetails { /// Specify the number `0` to remove the current badge, if present. Greater /// than `0` to display a badge with that number. /// Specify `null` to leave the current badge unchanged. - final int badgeNumber; + final int? badgeNumber; /// Specifies the list of attachments included with the notification. /// /// This property is only applicable to iOS 10 or newer. - final List attachments; + final List? attachments; /// Specifies the secondary description. /// /// This property is only applicable to iOS 10 or newer. - final String subtitle; + final String? subtitle; + + /// Specifies the thread identifier that can be used to group + /// notifications together. + /// + /// This property is only applicable to iOS 10 or newer. + final String? threadIdentifier; /// The identifier of the app-defined category object. /// /// This must refer to a [IOSNotificationCategory] identifier configured via /// [InitializationSettings]. - final String categoryIdentifier; + final String? categoryIdentifier; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/macos/initialization_settings.dart b/flutter_local_notifications/lib/src/platform_specifics/macos/initialization_settings.dart index 0019e0f20..64770a821 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/macos/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/macos/initialization_settings.dart @@ -8,12 +8,7 @@ class MacOSInitializationSettings { this.defaultPresentAlert = true, this.defaultPresentSound = true, this.defaultPresentBadge = true, - }) : assert(requestAlertPermission != null), - assert(requestSoundPermission != null), - assert(requestBadgePermission != null), - assert(defaultPresentAlert != null), - assert(defaultPresentBadge != null), - assert(defaultPresentSound != null); + }); /// Request permission to display an alert. /// diff --git a/flutter_local_notifications/lib/src/platform_specifics/macos/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/macos/method_channel_mappers.dart index 227c5532e..f942c0032 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/macos/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/macos/method_channel_mappers.dart @@ -22,15 +22,16 @@ extension MacOSNotificationAttachmentMapper on MacOSNotificationAttachment { } extension MacOSNotificationDetailsMapper on MacOSNotificationDetails { - Map toMap() => { + Map toMap() => { 'presentAlert': presentAlert, 'presentSound': presentSound, 'presentBadge': presentBadge, 'subtitle': subtitle, 'sound': sound, 'badgeNumber': badgeNumber, + 'threadIdentifier': threadIdentifier, 'attachments': attachments ?.map((a) => a.toMap()) // ignore: always_specify_types - ?.toList() + .toList() }; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/macos/notification_attachment.dart b/flutter_local_notifications/lib/src/platform_specifics/macos/notification_attachment.dart index 8786ce927..09db0315a 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/macos/notification_attachment.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/macos/notification_attachment.dart @@ -4,7 +4,7 @@ class MacOSNotificationAttachment { const MacOSNotificationAttachment( this.filePath, { this.identifier, - }) : assert(filePath != null); + }); /// The local file path to the attachment. /// @@ -15,5 +15,5 @@ class MacOSNotificationAttachment { /// The unique identifier for the attachment. /// /// When left empty, the macOS APIs will generate a unique identifier - final String identifier; + final String? identifier; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/macos/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/macos/notification_details.dart index d750a2d9c..6d3e54a00 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/macos/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/macos/notification_details.dart @@ -11,6 +11,7 @@ class MacOSNotificationDetails { this.badgeNumber, this.attachments, this.subtitle, + this.threadIdentifier, }); /// Display an alert when the notification is triggered while app is @@ -20,7 +21,7 @@ class MacOSNotificationDetails { /// to [MacOSInitializationSettings.defaultPresentAlert]. /// /// This property is only applicable to macOS 10.14 or newer. - final bool presentAlert; + final bool? presentAlert; /// Play a sound when the notification is triggered while app is in /// the foreground. @@ -29,7 +30,7 @@ class MacOSNotificationDetails { /// [MacOSInitializationSettings.defaultPresentSound]. /// /// This property is only applicable to macOS 10.14 or newer. - final bool presentSound; + final bool? presentSound; /// Apply the badge value when the notification is triggered while app is in /// the foreground. @@ -38,14 +39,14 @@ class MacOSNotificationDetails { /// [MacOSInitializationSettings.defaultPresentBadge]. /// /// This property is only applicable to macOS 10.14 or newer. - final bool presentBadge; + final bool? presentBadge; /// Specifies the name of the file to play for the notification. /// /// Requires setting [presentSound] to true. If [presentSound] is set to true /// but [sound] isn't specified then it will use the default notification /// sound. - final String sound; + final String? sound; /// Specify the number to display as the app icon's badge when the /// notification arrives. @@ -53,13 +54,19 @@ class MacOSNotificationDetails { /// Specify the number `0` to remove the current badge, if present. Greater /// than `0` to display a badge with that number. /// Specify `null` to leave the current badge unchanged. - final int badgeNumber; + final int? badgeNumber; /// Specifies the list of attachments included with the notification. /// /// This property is only applicable to macOS 10.14 or newer. - final List attachments; + final List? attachments; /// Specifies the secondary description. - final String subtitle; + final String? subtitle; + + /// Specifies the thread identifier that can be used to group + /// notifications together. + /// + /// This property is only applicable to macOS 10.14 or newer. + final String? threadIdentifier; } diff --git a/flutter_local_notifications/lib/src/typedefs.dart b/flutter_local_notifications/lib/src/typedefs.dart index 722fcfaae..636c6fa3a 100644 --- a/flutter_local_notifications/lib/src/typedefs.dart +++ b/flutter_local_notifications/lib/src/typedefs.dart @@ -1,16 +1,6 @@ -import 'dart:async'; - -/// Callback function when a notification is received. -typedef NotificationActionCallback = Function( - String id, String input, String payload); - -/// Signature of callback passed to [initialize] that is triggered when user -/// taps on a notification. -typedef SelectNotificationCallback = Future Function(String payload); - /// Signature of the callback that is triggered when a notification is shown /// whilst the app is in the foreground. /// /// This property is only applicable to iOS versions older than 10. -typedef DidReceiveLocalNotificationCallback = Future Function( - int id, String title, String body, String payload); +typedef DidReceiveLocalNotificationCallback = void Function( + int id, String? title, String? body, String? payload); diff --git a/flutter_local_notifications/lib/src/types.dart b/flutter_local_notifications/lib/src/types.dart index 121f4c192..e0748d417 100644 --- a/flutter_local_notifications/lib/src/types.dart +++ b/flutter_local_notifications/lib/src/types.dart @@ -66,4 +66,10 @@ enum DateTimeComponents { /// The day of the week and time. dayOfWeekAndTime, + + /// The day of the month and time. + dayOfMonthAndTime, + + /// The date and time. + dateAndTime, } diff --git a/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift b/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift index 64b5544fb..aa86d3826 100644 --- a/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift +++ b/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift @@ -3,7 +3,7 @@ import FlutterMacOS import UserNotifications public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNotificationCenterDelegate, NSUserNotificationCenterDelegate { - + struct MethodCallArguments { static let presentAlert = "presentAlert" static let presentSound = "presentSound" @@ -32,29 +32,30 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot static let attachments = "attachments" static let identifier = "identifier" static let filePath = "filePath" + static let threadIdentifier = "threadIdentifier" } - + struct DateFormatStrings { static let isoFormat = "yyyy-MM-dd'T'HH:mm:ss" } - - enum ScheduledNotificationRepeatFrequency : Int { + + enum ScheduledNotificationRepeatFrequency: Int { case daily case weekly } - - enum DateTimeComponents : Int { + + enum DateTimeComponents: Int { case time case dayOfWeekAndTime } - + enum RepeatInterval: Int { case everyMinute case hourly case daily case weekly } - + var channel: FlutterMethodChannel var initialized = false var defaultPresentAlert = false @@ -62,12 +63,11 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot var defaultPresentBadge = false var launchPayload: String? var launchingAppFromNotification = false - + init(fromChannel channel: FlutterMethodChannel) { self.channel = channel } - - + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "dexterous.com/flutter/local_notifications", binaryMessenger: registrar.messenger) let instance = FlutterLocalNotificationsPlugin.init(fromChannel: channel) @@ -80,10 +80,10 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } registrar.addMethodCallDelegate(instance, channel: channel) } - + @available(OSX 10.14, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - var options:UNNotificationPresentationOptions = [] + var options: UNNotificationPresentationOptions = [] let presentAlert = notification.request.content.userInfo[MethodCallArguments.presentAlert] as! Bool let presentSound = notification.request.content.userInfo[MethodCallArguments.presentSound] as! Bool let presentBadge = notification.request.content.userInfo[MethodCallArguments.presentBadge] as! Bool @@ -98,28 +98,28 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } completionHandler(options) } - + @available(OSX 10.14, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let payload = response.notification.request.content.userInfo[MethodCallArguments.payload] as? String - if(initialized) { + if initialized { handleSelectNotification(payload: payload) } else { launchPayload = payload launchingAppFromNotification = true } } - + public func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { - if(notification.activationType == .contentsClicked) { + if notification.activationType == .contentsClicked { handleSelectNotification(payload: notification.userInfo![MethodCallArguments.payload] as? String) } } - + public func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool { return true } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "initialize": @@ -144,9 +144,9 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(FlutterMethodNotImplemented) } } - + func initialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] defaultPresentAlert = arguments[MethodCallArguments.defaultPresentAlert] as! Bool defaultPresentSound = arguments[MethodCallArguments.defaultPresentSound] as! Bool defaultPresentBadge = arguments[MethodCallArguments.defaultPresentBadge] as! Bool @@ -155,20 +155,16 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot let requestedSoundPermission = arguments[MethodCallArguments.requestSoundPermission] as! Bool let requestedBadgePermission = arguments[MethodCallArguments.requestBadgePermission] as! Bool requestPermissionsImpl(soundPermission: requestedSoundPermission, alertPermission: requestedAlertPermission, badgePermission: requestedBadgePermission, result: result) - if(launchingAppFromNotification) { - handleSelectNotification(payload: launchPayload) - } initialized = true - } - else { + } else { result(true) initialized = true } } - + func requestPermissions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let requestedAlertPermission = arguments[MethodCallArguments.alert] as! Bool let requestedSoundPermission = arguments[MethodCallArguments.sound] as! Bool let requestedBadgePermission = arguments[MethodCallArguments.badge] as! Bool @@ -177,16 +173,16 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(nil) } } - + func getNotificationAppLaunchDetails(_ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { - let appLaunchDetails : [String: Any?] = [MethodCallArguments.notificationLaunchedApp : launchingAppFromNotification, MethodCallArguments.payload: launchPayload] + let appLaunchDetails: [String: Any?] = [MethodCallArguments.notificationLaunchedApp: launchingAppFromNotification, MethodCallArguments.payload: launchPayload] result(appLaunchDetails) } else { result(nil) } } - + func cancel(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { let center = UNUserNotificationCenter.current() @@ -194,12 +190,11 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot center.removePendingNotificationRequests(withIdentifiers: idsToRemove) center.removeDeliveredNotifications(withIdentifiers: idsToRemove) result(nil) - } - else { + } else { let id = String(call.arguments as! Int) let center = NSUserNotificationCenter.default for scheduledNotification in center.scheduledNotifications { - if (scheduledNotification.identifier == id) { + if scheduledNotification.identifier == id { center.removeScheduledNotification(scheduledNotification) break } @@ -210,7 +205,7 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(nil) } } - + func cancelAll(_ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { let center = UNUserNotificationCenter.current() @@ -226,30 +221,30 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(nil) } } - + func pendingNotificationRequests(_ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { UNUserNotificationCenter.current().getPendingNotificationRequests { (requests) in - var requestDictionaries:[Dictionary] = [] + var requestDictionaries: [[String: Any?]] = [] for request in requests { - requestDictionaries.append([MethodCallArguments.id:Int(request.identifier) as Any, MethodCallArguments.title: request.content.title, MethodCallArguments.body: request.content.body, MethodCallArguments.payload: request.content.userInfo[MethodCallArguments.payload]]) + requestDictionaries.append([MethodCallArguments.id: Int(request.identifier) as Any, MethodCallArguments.title: request.content.title, MethodCallArguments.body: request.content.body, MethodCallArguments.payload: request.content.userInfo[MethodCallArguments.payload]]) } result(requestDictionaries) } } else { - var requestDictionaries:[Dictionary] = [] + var requestDictionaries: [[String: Any?]] = [] let center = NSUserNotificationCenter.default for scheduledNotification in center.scheduledNotifications { - requestDictionaries.append([MethodCallArguments.id:Int(scheduledNotification.identifier!) as Any, MethodCallArguments.title: scheduledNotification.title, MethodCallArguments.body: scheduledNotification.informativeText, MethodCallArguments.payload: scheduledNotification.userInfo![MethodCallArguments.payload]]) + requestDictionaries.append([MethodCallArguments.id: Int(scheduledNotification.identifier!) as Any, MethodCallArguments.title: scheduledNotification.title, MethodCallArguments.body: scheduledNotification.informativeText, MethodCallArguments.payload: scheduledNotification.userInfo![MethodCallArguments.payload]]) } result(requestDictionaries) } } - + func show(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { do { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let content = try buildUserNotificationContent(fromArguments: arguments) let center = UNUserNotificationCenter.current() let request = UNNotificationRequest(identifier: getIdentifier(fromArguments: arguments), content: content, trigger: nil) @@ -259,17 +254,17 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(buildFlutterError(forMethodCallName: call.method, withError: error)) } } else { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let notification = buildNSUserNotification(fromArguments: arguments) NSUserNotificationCenter.default.deliver(notification) result(nil) } } - + func zonedSchedule(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { do { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let content = try buildUserNotificationContent(fromArguments: arguments) let trigger = buildUserNotificationCalendarTrigger(fromArguments: arguments) let center = UNUserNotificationCenter.current() @@ -280,7 +275,7 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(buildFlutterError(forMethodCallName: call.method, withError: error)) } } else { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let notification = buildNSUserNotification(fromArguments: arguments) let scheduledDateTime = arguments[MethodCallArguments.scheduledDateTime] as! String let timeZoneName = arguments[MethodCallArguments.timeZoneName] as! String @@ -303,11 +298,11 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(nil) } } - + func periodicallyShow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { if #available(OSX 10.14, *) { do { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let content = try buildUserNotificationContent(fromArguments: arguments) let trigger = buildUserNotificationTimeIntervalTrigger(fromArguments: arguments) let center = UNUserNotificationCenter.current() @@ -318,7 +313,7 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(buildFlutterError(forMethodCallName: call.method, withError: error)) } } else { - let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! [String: AnyObject] let notification = buildNSUserNotification(fromArguments: arguments) let rawRepeatInterval = arguments[MethodCallArguments.repeatInterval] as! Int let repeatInterval = RepeatInterval.init(rawValue: rawRepeatInterval)! @@ -340,9 +335,9 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot result(nil) } } - + @available(OSX 10.14, *) - func buildUserNotificationContent(fromArguments arguments: Dictionary) throws -> UNNotificationContent { + func buildUserNotificationContent(fromArguments arguments: [String: AnyObject]) throws -> UNNotificationContent { let content = UNMutableNotificationContent() if let title = arguments[MethodCallArguments.title] as? String { content.title = title @@ -356,7 +351,7 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot var presentSound = defaultPresentSound var presentBadge = defaultPresentBadge var presentAlert = defaultPresentAlert - if let platformSpecifics = arguments[MethodCallArguments.platformSpecifics] as? Dictionary { + if let platformSpecifics = arguments[MethodCallArguments.platformSpecifics] as? [String: AnyObject] { if let sound = platformSpecifics[MethodCallArguments.sound] as? String { content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init(sound)) } @@ -372,7 +367,10 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot if !(platformSpecifics[MethodCallArguments.presentBadge] is NSNull) && platformSpecifics[MethodCallArguments.presentBadge] != nil { presentBadge = platformSpecifics[MethodCallArguments.presentBadge] as! Bool } - if let attachments = platformSpecifics[MethodCallArguments.attachments] as? [Dictionary] { + if let threadIdentifier = platformSpecifics[MethodCallArguments.threadIdentifier] as? String { + content.threadIdentifier = threadIdentifier + } + if let attachments = platformSpecifics[MethodCallArguments.attachments] as? [[String: AnyObject]] { content.attachments = [] for attachment in attachments { let identifier = attachment[MethodCallArguments.identifier] as! String @@ -388,14 +386,13 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } return content } - - + func buildFlutterError(forMethodCallName methodCallName: String, withError error: Error) -> FlutterError { return FlutterError.init(code: "\(methodCallName)_error", message: error.localizedDescription, details: "\(error)") } - + @available(OSX 10.14, *) - func buildUserNotificationCalendarTrigger(fromArguments arguments:Dictionary) -> UNCalendarNotificationTrigger { + func buildUserNotificationCalendarTrigger(fromArguments arguments: [String: AnyObject]) -> UNCalendarNotificationTrigger { let scheduledDateTime = arguments[MethodCallArguments.scheduledDateTime] as! String let timeZoneName = arguments[MethodCallArguments.timeZoneName] as! String let timeZone = TimeZone.init(identifier: timeZoneName) @@ -413,16 +410,16 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot let dateComponents = calendar.dateComponents([.day, .hour, .minute, .second, .timeZone], from: date) return UNCalendarNotificationTrigger.init(dateMatching: dateComponents, repeats: true) case .dayOfWeekAndTime: - let dateComponents = calendar.dateComponents([ .weekday,.hour, .minute, .second, .timeZone], from: date) + let dateComponents = calendar.dateComponents([ .weekday, .hour, .minute, .second, .timeZone], from: date) return UNCalendarNotificationTrigger.init(dateMatching: dateComponents, repeats: true) } } let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .timeZone], from: date) return UNCalendarNotificationTrigger.init(dateMatching: dateComponents, repeats: false) } - + @available(OSX 10.14, *) - func buildUserNotificationTimeIntervalTrigger(fromArguments arguments:Dictionary) -> UNTimeIntervalNotificationTrigger { + func buildUserNotificationTimeIntervalTrigger(fromArguments arguments: [String: AnyObject]) -> UNTimeIntervalNotificationTrigger { let rawRepeatInterval = arguments[MethodCallArguments.repeatInterval] as! Int let repeatInterval = RepeatInterval.init(rawValue: rawRepeatInterval)! switch repeatInterval { @@ -435,27 +432,31 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot case .weekly: return UNTimeIntervalNotificationTrigger.init(timeInterval: 60 * 60 * 24 * 7, repeats: true) } - + } - + @available(OSX 10.14, *) func requestPermissionsImpl(soundPermission: Bool, alertPermission: Bool, badgePermission: Bool, result: @escaping FlutterResult) { + if !soundPermission && !alertPermission && !badgePermission { + result(false) + return + } var options: UNAuthorizationOptions = [] - if(soundPermission) { + if soundPermission { options.insert(.sound) } - if(alertPermission) { + if alertPermission { options.insert(.alert) } - if(badgePermission) { + if badgePermission { options.insert(.badge) } UNUserNotificationCenter.current().requestAuthorization(options: options) { (granted, _) in result(granted) } } - - func buildNSUserNotification(fromArguments arguments: Dictionary) -> NSUserNotification { + + func buildNSUserNotification(fromArguments arguments: [String: AnyObject]) -> NSUserNotification { let notification = NSUserNotification.init() notification.identifier = getIdentifier(fromArguments: arguments) if let title = arguments[MethodCallArguments.title] as? String { @@ -468,15 +469,15 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot notification.informativeText = body } var presentSound = defaultPresentSound - if let platformSpecifics = arguments[MethodCallArguments.platformSpecifics] as? Dictionary { + if let platformSpecifics = arguments[MethodCallArguments.platformSpecifics] as? [String: AnyObject] { if let sound = platformSpecifics[MethodCallArguments.sound] as? String { notification.soundName = sound } - + if !(platformSpecifics[MethodCallArguments.presentSound] is NSNull) && platformSpecifics[MethodCallArguments.presentSound] != nil { presentSound = platformSpecifics[MethodCallArguments.presentSound] as! Bool } - + } notification.userInfo = [MethodCallArguments.payload: arguments[MethodCallArguments.payload] as Any] if presentSound && notification.soundName == nil { @@ -484,11 +485,11 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } return notification } - - func getIdentifier(fromArguments arguments: Dictionary) -> String { + + func getIdentifier(fromArguments arguments: [String: AnyObject]) -> String { return String(arguments[MethodCallArguments.id] as! Int) } - + func handleSelectNotification(payload: String?) { channel.invokeMethod("selectNotification", arguments: payload) } diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index e86b734e5..53f853927 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -1,22 +1,25 @@ name: flutter_local_notifications -description: A cross platform plugin for displaying and scheduling local notifications for Flutter applications with the ability to customise for each platform. -version: 3.0.2 +description: A cross platform plugin for displaying and scheduling local + notifications for Flutter applications with the ability to customise for each + platform. +version: 9.1.4 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications dependencies: + clock: ^1.1.0 flutter: sdk: flutter - platform: ">=2.0.0 <4.0.0" - flutter_local_notifications_platform_interface: ^2.0.0+1 - timezone: ^0.5.6 + flutter_local_notifications_linux: ^0.3.0 + flutter_local_notifications_platform_interface: ^5.0.0 + timezone: ^0.8.0 dev_dependencies: flutter_driver: sdk: flutter flutter_test: sdk: flutter - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.1 + mockito: ^5.0.8 + plugin_platform_interface: ^2.0.0 flutter: plugin: @@ -28,8 +31,9 @@ flutter: pluginClass: FlutterLocalNotificationsPlugin macos: pluginClass: FlutterLocalNotificationsPlugin - + linux: + default_package: flutter_local_notifications_linux environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" \ No newline at end of file + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.2.0' diff --git a/flutter_local_notifications/test/platform_flutter_local_notifications_test.dart b/flutter_local_notifications/test/android_flutter_local_notifications_test.dart similarity index 64% rename from flutter_local_notifications/test/platform_flutter_local_notifications_test.dart rename to flutter_local_notifications/test/android_flutter_local_notifications_test.dart index 5a5aa4677..8a8189502 100644 --- a/flutter_local_notifications/test/platform_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/android_flutter_local_notifications_test.dart @@ -1,18 +1,19 @@ import 'dart:typed_data'; import 'dart:ui'; +import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/src/platform_specifics/android/enums.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:platform/platform.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; +import 'utils/date_formatter.dart'; + void main() { - // TODO(maikub): add tests for `periodicallyShow` after https://github.com/dart-lang/sdk/issues/28985 is resolved TestWidgetsFlutterBinding.ensureInitialized(); - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; group('Android', () { const MethodChannel channel = @@ -20,21 +21,18 @@ void main() { final List log = []; setUp(() { - flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin.private( - FakePlatform(operatingSystem: 'android')); + debugDefaultTargetPlatformOverride = TargetPlatform.android; + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // ignore: always_specify_types channel.setMockMethodCallHandler((methodCall) async { log.add(methodCall); if (methodCall.method == 'pendingNotificationRequests') { - return Future>>.value( - >[]); + return >[]; } else if (methodCall.method == 'getNotificationAppLaunchDetails') { - return Future>.value({}); + return null; } else if (methodCall.method == 'getActiveNotifications') { - return Future>>.value( - >[]); + return >[]; } - return Future.value(); }); }); @@ -65,7 +63,7 @@ void main() { 1, 'notification title', 'notification body', null); expect( log.last, - isMethodCall('show', arguments: { + isMethodCall('show', arguments: { 'id': 1, 'title': 'notification title', 'body': 'notification body', @@ -81,8 +79,8 @@ void main() { InitializationSettings(android: androidInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription'); + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription'); await flutterLocalNotificationsPlugin.show( 1, @@ -96,7 +94,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -110,15 +108,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -140,11 +138,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -157,8 +157,8 @@ void main() { InitializationSettings(android: androidInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); final AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription', + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription', additionalFlags: Int32List.fromList([4, 32])); await flutterLocalNotificationsPlugin.show( @@ -173,7 +173,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -187,15 +187,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -216,12 +216,14 @@ void main() { 'category': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'additionalFlags': [4, 32], 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -234,12 +236,11 @@ void main() { const InitializationSettings initializationSettings = InitializationSettings(android: androidInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); - final int timestamp = DateTime.now().millisecondsSinceEpoch; + final int timestamp = clock.now().millisecondsSinceEpoch; final AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription', - when: timestamp); + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription', when: timestamp); await flutterLocalNotificationsPlugin.show( 1, 'notification title', @@ -252,7 +253,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -266,15 +267,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': timestamp, 'usesChronometer': false, @@ -296,11 +297,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -312,12 +315,13 @@ void main() { const InitializationSettings initializationSettings = InitializationSettings(android: androidInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); - final int timestamp = DateTime.now().millisecondsSinceEpoch; + final int timestamp = clock.now().millisecondsSinceEpoch; final AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription', - when: timestamp, usesChronometer: true); + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription', + when: timestamp, + usesChronometer: true); await flutterLocalNotificationsPlugin.show( 1, 'notification title', @@ -330,7 +334,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -344,15 +348,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': timestamp, 'usesChronometer': true, @@ -374,11 +378,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -395,7 +401,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', sound: RawResourceAndroidNotificationSound('sound.mp3'), ); @@ -411,7 +417,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -427,15 +433,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -457,11 +463,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -477,7 +485,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', sound: UriAndroidNotificationSound('uri'), ); @@ -493,7 +501,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -509,15 +517,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -539,11 +547,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -560,7 +570,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: DefaultStyleInformation(true, true), ); @@ -576,7 +586,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -590,15 +600,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -620,11 +630,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': true, 'htmlFormatTitle': true, }, + 'tag': null, }, })); }); @@ -641,7 +653,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: BigPictureStyleInformation( DrawableResourceAndroidBitmap('bigPictureDrawable'), ), @@ -659,7 +671,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -673,15 +685,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -703,8 +715,9 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, - 'styleInformation': { + 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, 'bigPicture': 'bigPictureDrawable', @@ -715,6 +728,7 @@ void main() { 'htmlFormatSummaryText': false, 'hideExpandedLargeIcon': false, }, + 'tag': null, }, })); }); @@ -731,7 +745,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: BigPictureStyleInformation( DrawableResourceAndroidBitmap('bigPictureDrawable'), contentTitle: 'contentTitle', @@ -757,7 +771,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -771,15 +785,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -801,6 +815,7 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, 'styleInformation': { 'htmlFormatContent': true, @@ -815,6 +830,7 @@ void main() { 'htmlFormatSummaryText': true, 'hideExpandedLargeIcon': true, }, + 'tag': null, }, })); }); @@ -831,7 +847,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: BigPictureStyleInformation( FilePathAndroidBitmap('bigPictureFilePath'), ), @@ -849,7 +865,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -863,15 +879,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -893,8 +909,9 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, - 'styleInformation': { + 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, 'bigPicture': 'bigPictureFilePath', @@ -905,6 +922,7 @@ void main() { 'htmlFormatSummaryText': false, 'hideExpandedLargeIcon': false, }, + 'tag': null, }, })); }); @@ -921,7 +939,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: BigPictureStyleInformation( FilePathAndroidBitmap('bigPictureFilePath'), contentTitle: 'contentTitle', @@ -947,7 +965,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -961,15 +979,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -991,6 +1009,7 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, 'styleInformation': { 'htmlFormatContent': true, @@ -1005,6 +1024,7 @@ void main() { 'htmlFormatSummaryText': true, 'hideExpandedLargeIcon': true, }, + 'tag': null, }, })); }); @@ -1019,7 +1039,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: InboxStyleInformation( ['line1'], ), @@ -1037,7 +1057,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -1051,15 +1071,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1081,8 +1101,9 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.inbox.index, - 'styleInformation': { + 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, 'lines': ['line1'], @@ -1092,6 +1113,7 @@ void main() { 'htmlFormatSummaryText': false, 'htmlFormatLines': false, }, + 'tag': null, }, })); }); @@ -1106,7 +1128,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: InboxStyleInformation( ['line1'], htmlFormatLines: true, @@ -1131,7 +1153,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -1145,15 +1167,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1175,6 +1197,7 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.inbox.index, 'styleInformation': { 'htmlFormatContent': true, @@ -1186,6 +1209,7 @@ void main() { 'htmlFormatSummaryText': true, 'htmlFormatLines': true, }, + 'tag': null, }, })); }); @@ -1200,7 +1224,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: MediaStyleInformation(), ); @@ -1216,7 +1240,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -1230,15 +1254,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1260,11 +1284,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.media.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -1279,7 +1305,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: MediaStyleInformation( htmlFormatTitle: true, htmlFormatContent: true, @@ -1298,7 +1324,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -1312,15 +1338,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1342,17 +1368,19 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.media.index, 'styleInformation': { 'htmlFormatContent': true, 'htmlFormatTitle': true, }, + 'tag': null, }, })); }); test('show with default Android messaging style settings', () async { - final DateTime messageDateTime = DateTime.now(); + final DateTime messageDateTime = clock.now(); const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('app_icon'); const InitializationSettings initializationSettings = @@ -1362,7 +1390,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: MessagingStyleInformation( const Person(name: 'name'), messages: [ @@ -1387,7 +1415,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -1401,15 +1429,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1431,21 +1459,22 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.messaging.index, - 'styleInformation': { + 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, - 'person': { - 'bot': null, - 'important': null, + 'person': { + 'bot': false, + 'important': false, 'key': null, 'name': 'name', 'uri': null, }, 'conversationTitle': null, 'groupConversation': null, - 'messages': >[ - { + 'messages': >[ + { 'text': 'message 1', 'timestamp': messageDateTime.millisecondsSinceEpoch, 'person': null, @@ -1454,12 +1483,13 @@ void main() { } ], }, + 'tag': null, }, })); }); test('show with non-default Android messaging style settings', () async { - final DateTime messageDateTime = DateTime.now(); + final DateTime messageDateTime = clock.now(); const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('app_icon'); const InitializationSettings initializationSettings = @@ -1469,7 +1499,7 @@ void main() { AndroidNotificationDetails( 'channelId', 'channelName', - 'channelDescription', + channelDescription: 'channelDescription', styleInformation: MessagingStyleInformation( const Person( bot: true, @@ -1505,7 +1535,7 @@ void main() { 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'platformSpecifics': { + 'platformSpecifics': { 'icon': null, 'channelId': 'channelId', 'channelName': 'channelName', @@ -1519,15 +1549,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1549,6 +1579,7 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.messaging.index, 'styleInformation': { 'htmlFormatContent': false, @@ -1564,8 +1595,8 @@ void main() { }, 'conversationTitle': 'conversationTitle', 'groupConversation': true, - 'messages': >[ - { + 'messages': >[ + { 'text': 'message 1', 'timestamp': messageDateTime.millisecondsSinceEpoch, 'person': null, @@ -1574,10 +1605,102 @@ void main() { } ], }, + 'tag': null, }, })); }); + group('periodicallyShow', () { + final DateTime now = DateTime(2020, 10, 9); + for (final RepeatInterval repeatInterval in RepeatInterval.values) { + test('$repeatInterval', () async { + await withClock(Clock.fixed(now), () async { + const AndroidInitializationSettings androidInitializationSettings = + AndroidInitializationSettings('app_icon'); + const InitializationSettings initializationSettings = + InitializationSettings(android: androidInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription'); + await flutterLocalNotificationsPlugin.periodicallyShow( + 1, + 'notification title', + 'notification body', + repeatInterval, + const NotificationDetails(android: androidNotificationDetails), + ); + + expect( + log.last, + isMethodCall('periodicallyShow', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'calledAt': now.millisecondsSinceEpoch, + 'repeatInterval': repeatInterval.index, + 'platformSpecifics': { + 'allowWhileIdle': false, + 'icon': null, + 'channelId': 'channelId', + 'channelName': 'channelName', + 'channelDescription': 'channelDescription', + 'channelShowBadge': true, + 'channelAction': AndroidNotificationChannelAction + .createIfNotExists.index, + 'importance': Importance.defaultImportance.value, + 'priority': Priority.defaultPriority.value, + 'playSound': true, + 'enableVibration': true, + 'vibrationPattern': null, + 'groupKey': null, + 'setAsGroupSummary': false, + 'groupAlertBehavior': GroupAlertBehavior.all.index, + 'autoCancel': true, + 'ongoing': false, + 'colorAlpha': null, + 'colorRed': null, + 'colorGreen': null, + 'colorBlue': null, + 'onlyAlertOnce': false, + 'showWhen': true, + 'when': null, + 'usesChronometer': false, + 'showProgress': false, + 'maxProgress': 0, + 'progress': 0, + 'indeterminate': false, + 'enableLights': false, + 'ledColorAlpha': null, + 'ledColorRed': null, + 'ledColorGreen': null, + 'ledColorBlue': null, + 'ledOnMs': null, + 'ledOffMs': null, + 'ticker': null, + 'visibility': null, + 'timeoutAfter': null, + 'category': null, + 'additionalFlags': null, + 'fullScreenIntent': false, + 'shortcutId': null, + 'subText': null, + 'style': AndroidNotificationStyle.defaultStyle.index, + 'styleInformation': { + 'htmlFormatContent': false, + 'htmlFormatTitle': false, + }, + 'tag': null, + }, + })); + }); + }); + } + }); + group('zonedSchedule', () { test('no repeat frequency', () async { const AndroidInitializationSettings androidInitializationSettings = @@ -1591,8 +1714,8 @@ void main() { final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription'); + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription'); await flutterLocalNotificationsPlugin.zonedSchedule( 1, 'notification title', @@ -1610,8 +1733,8 @@ void main() { 'body': 'notification body', 'payload': '', 'timeZoneName': 'Australia/Sydney', - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'platformSpecifics': { + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'platformSpecifics': { 'allowWhileIdle': true, 'icon': null, 'channelId': 'channelId', @@ -1626,15 +1749,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1656,11 +1779,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -1678,8 +1803,8 @@ void main() { final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription'); + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription'); await flutterLocalNotificationsPlugin.zonedSchedule( 1, 'notification title', @@ -1698,9 +1823,9 @@ void main() { 'body': 'notification body', 'payload': '', 'timeZoneName': 'Australia/Sydney', - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), 'matchDateTimeComponents': DateTimeComponents.time.index, - 'platformSpecifics': { + 'platformSpecifics': { 'allowWhileIdle': true, 'icon': null, 'channelId': 'channelId', @@ -1715,15 +1840,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1745,11 +1870,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -1767,8 +1894,8 @@ void main() { final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channelId', 'channelName', 'channelDescription'); + AndroidNotificationDetails('channelId', 'channelName', + channelDescription: 'channelDescription'); await flutterLocalNotificationsPlugin.zonedSchedule( 1, 'notification title', @@ -1787,10 +1914,10 @@ void main() { 'body': 'notification body', 'payload': '', 'timeZoneName': 'Australia/Sydney', - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), 'matchDateTimeComponents': DateTimeComponents.dayOfWeekAndTime.index, - 'platformSpecifics': { + 'platformSpecifics': { 'allowWhileIdle': true, 'icon': null, 'channelId': 'channelId', @@ -1805,15 +1932,15 @@ void main() { 'enableVibration': true, 'vibrationPattern': null, 'groupKey': null, - 'setAsGroupSummary': null, + 'setAsGroupSummary': false, 'groupAlertBehavior': GroupAlertBehavior.all.index, 'autoCancel': true, - 'ongoing': null, + 'ongoing': false, 'colorAlpha': null, 'colorRed': null, 'colorGreen': null, 'colorBlue': null, - 'onlyAlertOnce': null, + 'onlyAlertOnce': false, 'showWhen': true, 'when': null, 'usesChronometer': false, @@ -1835,11 +1962,13 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { 'htmlFormatContent': false, 'htmlFormatTitle': false, }, + 'tag': null, }, })); }); @@ -1849,12 +1978,12 @@ void main() { test('without description', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannelGroup( const AndroidNotificationChannelGroup('groupId', 'groupName')); expect(log, [ isMethodCall('createNotificationChannelGroup', - arguments: { + arguments: { 'id': 'groupId', 'name': 'groupName', 'description': null, @@ -1864,7 +1993,7 @@ void main() { test('with description', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannelGroup( const AndroidNotificationChannelGroup('groupId', 'groupName', description: 'groupDescription')); @@ -1882,11 +2011,12 @@ void main() { test('createNotificationChannel with default settings', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannel(const AndroidNotificationChannel( - 'channelId', 'channelName', 'channelDescription')); + 'channelId', 'channelName', + description: 'channelDescription')); expect(log, [ - isMethodCall('createNotificationChannel', arguments: { + isMethodCall('createNotificationChannel', arguments: { 'id': 'channelId', 'name': 'channelName', 'description': 'channelDescription', @@ -1902,7 +2032,7 @@ void main() { 'ledColorGreen': null, 'ledColorBlue': null, 'channelAction': - AndroidNotificationChannelAction.createIfNotExists?.index, + AndroidNotificationChannelAction.createIfNotExists.index, }) ]); }); @@ -1910,11 +2040,11 @@ void main() { test('createNotificationChannel with non-default settings', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .createNotificationChannel(const AndroidNotificationChannel( 'channelId', 'channelName', - 'channelDescription', + description: 'channelDescription', groupId: 'channelGroupId', showBadge: false, importance: Importance.max, @@ -1924,7 +2054,7 @@ void main() { ledColor: Color.fromARGB(255, 255, 0, 0), )); expect(log, [ - isMethodCall('createNotificationChannel', arguments: { + isMethodCall('createNotificationChannel', arguments: { 'id': 'channelId', 'name': 'channelName', 'description': 'channelDescription', @@ -1940,7 +2070,7 @@ void main() { 'ledColorGreen': 0, 'ledColorBlue': 0, 'channelAction': - AndroidNotificationChannelAction.createIfNotExists?.index, + AndroidNotificationChannelAction.createIfNotExists.index, }) ]); }); @@ -1948,7 +2078,7 @@ void main() { test('deleteNotificationChannel', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .deleteNotificationChannel('channelId'); expect(log, [ isMethodCall('deleteNotificationChannel', arguments: 'channelId') @@ -1958,7 +2088,7 @@ void main() { test('getActiveNotifications', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .getActiveNotifications(); expect(log, [isMethodCall('getActiveNotifications', arguments: null)]); @@ -1966,7 +2096,22 @@ void main() { test('cancel', () async { await flutterLocalNotificationsPlugin.cancel(1); - expect(log, [isMethodCall('cancel', arguments: 1)]); + expect(log, [ + isMethodCall('cancel', arguments: { + 'id': 1, + 'tag': null, + }) + ]); + }); + + test('cancel with tag', () async { + await flutterLocalNotificationsPlugin.cancel(1, tag: 'tag'); + expect(log, [ + isMethodCall('cancel', arguments: { + 'id': 1, + 'tag': 'tag', + }) + ]); }); test('cancelAll', () async { @@ -1985,7 +2130,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin>()! .getActiveNotifications(); expect(log, [isMethodCall('getActiveNotifications', arguments: null)]); @@ -1997,878 +2142,43 @@ void main() { isMethodCall('getNotificationAppLaunchDetails', arguments: null) ]); }); - }); - - group('iOS', () { - const MethodChannel channel = - MethodChannel('dexterous.com/flutter/local_notifications'); - final List log = []; - - setUp(() { - flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin.private( - FakePlatform(operatingSystem: 'ios')); - // ignore: always_specify_types - channel.setMockMethodCallHandler((methodCall) { - log.add(methodCall); - if (methodCall.method == 'pendingNotificationRequests') { - return Future>>.value( - >[]); - } else if (methodCall.method == 'getNotificationAppLaunchDetails') { - return Future>.value({}); - } - return Future.value(); - }); - }); - - tearDown(() { - log.clear(); - }); - - test('initialize with default parameter values', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - expect(log, [ - isMethodCall('initialize', arguments: { - 'requestAlertPermission': true, - 'requestSoundPermission': true, - 'requestBadgePermission': true, - 'defaultPresentAlert': true, - 'defaultPresentSound': true, - 'defaultPresentBadge': true, - 'notificationCategories': [], - }) - ]); - }); - - test('initialize with notification categories', () async { - final IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings( - notificationCategories: [ - IOSNotificationCategory( - 'category1', - actions: [ - IOSNotificationAction.plain( - 'action1', - 'Action 1', - options: { - IOSNotificationActionOption.destructive, - }, - ), - ], - options: { - IOSNotificationCategoryOption.allowAnnouncement, - }, - ), - IOSNotificationCategory( - 'category2', - actions: [ - IOSNotificationAction.plain('action2', 'Action 2'), - IOSNotificationAction.plain('action3', 'Action 3'), - ], - ), - IOSNotificationCategory( - 'category3', - actions: [ - IOSNotificationAction.text( - 'action4', - 'Action 4', - buttonTitle: 'Send', - placeholder: 'Placeholder', - ), - ], - ) - ], - ); - final InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - expect(log, [ - isMethodCall('initialize', arguments: { - 'requestAlertPermission': true, - 'requestSoundPermission': true, - 'requestBadgePermission': true, - 'defaultPresentAlert': true, - 'defaultPresentSound': true, - 'defaultPresentBadge': true, - 'notificationCategories': >[ - { - 'identifier': 'category1', - 'actions': >[ - { - 'type': 'plain', - 'identifier': 'action1', - 'title': 'Action 1', - 'options': [2], - } - ], - 'options': [5], - }, - { - 'identifier': 'category2', - 'actions': >[ - { - 'type': 'plain', - 'identifier': 'action2', - 'title': 'Action 2', - 'options': [], - }, - { - 'type': 'plain', - 'identifier': 'action3', - 'title': 'Action 3', - 'options': [], - }, - ], - 'options': [], - }, - { - 'identifier': 'category3', - 'actions': >[ - { - 'type': 'text', - 'identifier': 'action4', - 'title': 'Action 4', - 'options': [], - 'buttonTitle': 'Send', - 'placeholder': 'Placeholder', - }, - ], - 'options': [], - } - ], - }) - ]); - }); - test('initialize with all settings off', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings( - requestAlertPermission: false, - requestBadgePermission: false, - requestSoundPermission: false, - defaultPresentAlert: false, - defaultPresentBadge: false, - defaultPresentSound: false); - const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - expect(log, [ - isMethodCall('initialize', arguments: { - 'requestAlertPermission': false, - 'requestSoundPermission': false, - 'requestBadgePermission': false, - 'defaultPresentAlert': false, - 'defaultPresentSound': false, - 'defaultPresentBadge': false, - 'notificationCategories': [], - }) - ]); - }); - - test('show without iOS-specific details', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - await flutterLocalNotificationsPlugin.show( - 1, 'notification title', 'notification body', null); - expect( - log.last, - isMethodCall('show', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'platformSpecifics': null, - })); - }); - test('show with iOS-specific details', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings(); + test('startForegroundService', () async { + const AndroidInitializationSettings androidInitializationSettings = + AndroidInitializationSettings('app_icon'); const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); + InitializationSettings(android: androidInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); - const NotificationDetails notificationDetails = NotificationDetails( - iOS: IOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - subtitle: 'a subtitle', - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - IOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373'), - ], - categoryIdentifier: 'category1', - ), - ); - - await flutterLocalNotificationsPlugin.show( - 1, 'notification title', 'notification body', notificationDetails); - - expect( - log.last, - isMethodCall('show', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'platformSpecifics': { - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'subtitle': 'a subtitle', - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - 'categoryIdentifier': 'category1', - }, - })); - }); - - group('zonedSchedule', () { - test('no repeat frequency', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin - .initialize(initializationSettings); - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Australia/Sydney')); - final tz.TZDateTime scheduledDate = - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); - const NotificationDetails notificationDetails = NotificationDetails( - iOS: IOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - IOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') - ])); - - await flutterLocalNotificationsPlugin.zonedSchedule( - 1, - 'notification title', - 'notification body', - scheduledDate, - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime); - - expect( - log.last, - isMethodCall('zonedSchedule', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'uiLocalNotificationDateInterpretation': - UILocalNotificationDateInterpretation.absoluteTime.index, - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'timeZoneName': 'Australia/Sydney', - 'platformSpecifics': { - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'subtitle': null, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - 'categoryIdentifier': null, - }, - })); - }); - - test('match time components', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin - .initialize(initializationSettings); - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Australia/Sydney')); - final tz.TZDateTime scheduledDate = - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); - const NotificationDetails notificationDetails = NotificationDetails( - iOS: IOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - IOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') - ])); - - await flutterLocalNotificationsPlugin.zonedSchedule( - 1, - 'notification title', - 'notification body', - scheduledDate, - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time); - - expect( - log.last, - isMethodCall('zonedSchedule', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'uiLocalNotificationDateInterpretation': - UILocalNotificationDateInterpretation.absoluteTime.index, - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'timeZoneName': 'Australia/Sydney', - 'matchDateTimeComponents': DateTimeComponents.time.index, - 'platformSpecifics': { - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'subtitle': null, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - 'categoryIdentifier': null, - }, - })); - }); - - test('match day of week and time components', () async { - const IOSInitializationSettings iosInitializationSettings = - IOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(iOS: iosInitializationSettings); - await flutterLocalNotificationsPlugin - .initialize(initializationSettings); - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Australia/Sydney')); - final tz.TZDateTime scheduledDate = - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); - const NotificationDetails notificationDetails = NotificationDetails( - iOS: IOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - IOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') - ])); - - await flutterLocalNotificationsPlugin.zonedSchedule( - 1, - 'notification title', - 'notification body', - scheduledDate, - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); - - expect( - log.last, - isMethodCall('zonedSchedule', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'uiLocalNotificationDateInterpretation': - UILocalNotificationDateInterpretation.absoluteTime.index, - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'timeZoneName': 'Australia/Sydney', - 'matchDateTimeComponents': - DateTimeComponents.dayOfWeekAndTime.index, - 'platformSpecifics': { - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'subtitle': null, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - 'categoryIdentifier': null, - }, - })); - }); - }); - test('requestPermissions with default settings', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() - .requestPermissions(); - expect(log, [ - isMethodCall('requestPermissions', arguments: { - 'sound': null, - 'badge': null, - 'alert': null, - }) - ]); - }); - test('requestPermissions with all settings requested', () async { - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() - .requestPermissions(sound: true, badge: true, alert: true); - expect(log, [ - isMethodCall('requestPermissions', arguments: { - 'sound': true, - 'badge': true, - 'alert': true, - }) - ]); - }); - test('cancel', () async { - await flutterLocalNotificationsPlugin.cancel(1); - expect(log, [isMethodCall('cancel', arguments: 1)]); - }); - - test('cancelAll', () async { - await flutterLocalNotificationsPlugin.cancelAll(); - expect(log, [isMethodCall('cancelAll', arguments: null)]); - }); - - test('pendingNotificationRequests', () async { - await flutterLocalNotificationsPlugin.pendingNotificationRequests(); - expect(log, [ - isMethodCall('pendingNotificationRequests', arguments: null) - ]); - }); - - test('getNotificationAppLaunchDetails', () async { - await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); - expect(log, [ - isMethodCall('getNotificationAppLaunchDetails', arguments: null) - ]); - }); - }); - - group('macOS', () { - const MethodChannel channel = - MethodChannel('dexterous.com/flutter/local_notifications'); - final List log = []; - - setUp(() { - flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin.private( - FakePlatform(operatingSystem: 'macos')); - // ignore: always_specify_types - channel.setMockMethodCallHandler((methodCall) { - log.add(methodCall); - if (methodCall.method == 'pendingNotificationRequests') { - return Future>>.value( - >[]); - } else if (methodCall.method == 'getNotificationAppLaunchDetails') { - return Future>.value({}); - } - return Future.value(); - }); - }); - - tearDown(() { - log.clear(); - }); - - test('initialize with default parameter values', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - expect(log, [ - isMethodCall('initialize', arguments: { - 'requestAlertPermission': true, - 'requestSoundPermission': true, - 'requestBadgePermission': true, - 'defaultPresentAlert': true, - 'defaultPresentSound': true, - 'defaultPresentBadge': true, - }) - ]); - }); - - test('initialize with all settings off', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings( - requestAlertPermission: false, - requestBadgePermission: false, - requestSoundPermission: false, - defaultPresentAlert: false, - defaultPresentBadge: false, - defaultPresentSound: false); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - expect(log, [ - isMethodCall('initialize', arguments: { - 'requestAlertPermission': false, - 'requestSoundPermission': false, - 'requestBadgePermission': false, - 'defaultPresentAlert': false, - 'defaultPresentSound': false, - 'defaultPresentBadge': false, - }) - ]); - }); - - test('show without macOS-specific details', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - await flutterLocalNotificationsPlugin.show( - 1, 'notification title', 'notification body', null); - expect( - log.last, - isMethodCall('show', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'platformSpecifics': null, - })); - }); - - test('show with macOS-specific details', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - const NotificationDetails notificationDetails = NotificationDetails( - macOS: MacOSNotificationDetails( - subtitle: 'a subtitle', - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - MacOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373'), - ])); - - await flutterLocalNotificationsPlugin.show( - 1, 'notification title', 'notification body', notificationDetails); - + AndroidFlutterLocalNotificationsPlugin>()! + .startForegroundService(1, 'notification title', 'notification body'); expect( log.last, - isMethodCall('show', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'platformSpecifics': { - 'subtitle': 'a subtitle', - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - }, - })); - }); - - group('zonedSchedule', () { - test('no repeat frequency', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin - .initialize(initializationSettings); - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Australia/Sydney')); - final tz.TZDateTime scheduledDate = - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); - const NotificationDetails notificationDetails = NotificationDetails( - macOS: MacOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - MacOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') - ])); - - await flutterLocalNotificationsPlugin.zonedSchedule( - 1, - 'notification title', - 'notification body', - scheduledDate, - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime); - - expect( - log.last, - isMethodCall('zonedSchedule', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'timeZoneName': 'Australia/Sydney', - 'platformSpecifics': { - 'subtitle': null, - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - }, - })); - }); - - test('match time components', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin - .initialize(initializationSettings); - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Australia/Sydney')); - final tz.TZDateTime scheduledDate = - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); - const NotificationDetails notificationDetails = NotificationDetails( - macOS: MacOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - MacOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') - ])); - - await flutterLocalNotificationsPlugin.zonedSchedule( - 1, - 'notification title', - 'notification body', - scheduledDate, - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time); - - expect( - log.last, - isMethodCall('zonedSchedule', arguments: { + isMethodCall('startForegroundService', arguments: { + 'notificationData': { 'id': 1, 'title': 'notification title', 'body': 'notification body', 'payload': '', - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'timeZoneName': 'Australia/Sydney', - 'matchDateTimeComponents': DateTimeComponents.time.index, - 'platformSpecifics': { - 'subtitle': null, - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - }, - })); - }); - - test('weekly repeat frequency', () async { - const MacOSInitializationSettings macOSInitializationSettings = - MacOSInitializationSettings(); - const InitializationSettings initializationSettings = - InitializationSettings(macOS: macOSInitializationSettings); - await flutterLocalNotificationsPlugin - .initialize(initializationSettings); - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Australia/Sydney')); - final tz.TZDateTime scheduledDate = - tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); - const NotificationDetails notificationDetails = NotificationDetails( - macOS: MacOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - sound: 'sound.mp3', - badgeNumber: 1, - attachments: [ - MacOSNotificationAttachment('video.mp4', - identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') - ])); - - await flutterLocalNotificationsPlugin.zonedSchedule( - 1, - 'notification title', - 'notification body', - scheduledDate, - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); - - expect( - log.last, - isMethodCall('zonedSchedule', arguments: { - 'id': 1, - 'title': 'notification title', - 'body': 'notification body', - 'payload': '', - 'scheduledDateTime': _convertDateToISO8601String(scheduledDate), - 'timeZoneName': 'Australia/Sydney', - 'matchDateTimeComponents': - DateTimeComponents.dayOfWeekAndTime.index, - 'platformSpecifics': { - 'subtitle': null, - 'presentAlert': true, - 'presentBadge': true, - 'presentSound': true, - 'sound': 'sound.mp3', - 'badgeNumber': 1, - 'attachments': >[ - { - 'filePath': 'video.mp4', - 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', - } - ], - }, - })); - }); + 'platformSpecifics': null, + }, + 'startType': AndroidServiceStartType.startSticky.value, + 'foregroundServiceTypes': null + })); }); - test('requestPermissions with default settings', () async { - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - MacOSFlutterLocalNotificationsPlugin>() - .requestPermissions(); - expect(log, [ - isMethodCall('requestPermissions', arguments: { - 'sound': null, - 'badge': null, - 'alert': null, - }) - ]); - }); - test('requestPermissions with all settings requested', () async { + test('stopForegroundService', () async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< - MacOSFlutterLocalNotificationsPlugin>() - .requestPermissions(sound: true, badge: true, alert: true); - expect(log, [ - isMethodCall('requestPermissions', arguments: { - 'sound': true, - 'badge': true, - 'alert': true, - }) - ]); - }); - test('cancel', () async { - await flutterLocalNotificationsPlugin.cancel(1); - expect(log, [isMethodCall('cancel', arguments: 1)]); - }); - - test('cancelAll', () async { - await flutterLocalNotificationsPlugin.cancelAll(); - expect(log, [isMethodCall('cancelAll', arguments: null)]); - }); - - test('pendingNotificationRequests', () async { - await flutterLocalNotificationsPlugin.pendingNotificationRequests(); - expect(log, [ - isMethodCall('pendingNotificationRequests', arguments: null) - ]); - }); - - test('getNotificationAppLaunchDetails', () async { - await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); - expect(log, [ - isMethodCall('getNotificationAppLaunchDetails', arguments: null) - ]); + AndroidFlutterLocalNotificationsPlugin>()! + .stopForegroundService(); + expect( + log.last, + isMethodCall( + 'stopForegroundService', + arguments: null, + )); }); }); } - -String _convertDateToISO8601String(tz.TZDateTime dateTime) { - String _twoDigits(int n) { - if (n >= 10) { - return '$n'; - } - return '0$n'; - } - - String _fourDigits(int n) { - final int absN = n.abs(); - final String sign = n < 0 ? '-' : ''; - if (absN >= 1000) { - return '$n'; - } - if (absN >= 100) { - return '${sign}0$absN'; - } - if (absN >= 10) { - return '${sign}00$absN'; - } - return '${sign}000$absN'; - } - - return '${_fourDigits(dateTime.year)}-${_twoDigits(dateTime.month)}-${_twoDigits(dateTime.day)}T${_twoDigits(dateTime.hour)}:${_twoDigits(dateTime.minute)}:${_twoDigits(dateTime.second)}'; // ignore: lines_longer_than_80_chars -} diff --git a/flutter_local_notifications/test/flutter_local_notifications_test.dart b/flutter_local_notifications/test/flutter_local_notifications_test.dart index ce5a1d1bf..064e10be1 100644 --- a/flutter_local_notifications/test/flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/flutter_local_notifications_test.dart @@ -1,9 +1,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -11,12 +11,6 @@ void main() { MockFlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlatform.instance = mock; - test( - 'Throws assertion error when creating an IOSNotificationAttachment with ' - 'no file path', () { - expect(() => IOSNotificationAttachment(null), throwsAssertionError); - }); - test('Creates IOSNotificationAttachment when file path is specified', () { expect( const IOSNotificationAttachment(''), isA()); diff --git a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart new file mode 100644 index 000000000..744c60fcd --- /dev/null +++ b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart @@ -0,0 +1,558 @@ +import 'package:clock/clock.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +import 'utils/date_formatter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + + group('iOS', () { + const MethodChannel channel = + MethodChannel('dexterous.com/flutter/local_notifications'); + final List log = []; + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + // ignore: always_specify_types + channel.setMockMethodCallHandler((methodCall) async { + log.add(methodCall); + if (methodCall.method == 'pendingNotificationRequests') { + return ?>[]; + } else if (methodCall.method == 'getNotificationAppLaunchDetails') { + return null; + } + }); + }); + + tearDown(() { + log.clear(); + }); + + test('initialize with default parameter values', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': true, + 'requestSoundPermission': true, + 'requestBadgePermission': true, + 'defaultPresentAlert': true, + 'defaultPresentSound': true, + 'defaultPresentBadge': true, + 'notificationCategories': [], + }) + ]); + }); + + test('initialize with notification categories', () async { + final IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( + notificationCategories: [ + IOSNotificationCategory( + 'category1', + actions: [ + IOSNotificationAction.plain( + 'action1', + 'Action 1', + options: { + IOSNotificationActionOption.destructive, + }, + ), + ], + options: { + IOSNotificationCategoryOption.allowAnnouncement, + }, + ), + IOSNotificationCategory( + 'category2', + actions: [ + IOSNotificationAction.plain('action2', 'Action 2'), + IOSNotificationAction.plain('action3', 'Action 3'), + ], + ), + IOSNotificationCategory( + 'category3', + actions: [ + IOSNotificationAction.text( + 'action4', + 'Action 4', + buttonTitle: 'Send', + placeholder: 'Placeholder', + ), + ], + ) + ], + ); + final InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': true, + 'requestSoundPermission': true, + 'requestBadgePermission': true, + 'defaultPresentAlert': true, + 'defaultPresentSound': true, + 'defaultPresentBadge': true, + 'notificationCategories': >[ + { + 'identifier': 'category1', + 'actions': >[ + { + 'type': 'plain', + 'identifier': 'action1', + 'title': 'Action 1', + 'options': [2], + } + ], + 'options': [5], + }, + { + 'identifier': 'category2', + 'actions': >[ + { + 'type': 'plain', + 'identifier': 'action2', + 'title': 'Action 2', + 'options': [], + }, + { + 'type': 'plain', + 'identifier': 'action3', + 'title': 'Action 3', + 'options': [], + }, + ], + 'options': [], + }, + { + 'identifier': 'category3', + 'actions': >[ + { + 'type': 'text', + 'identifier': 'action4', + 'title': 'Action 4', + 'options': [], + 'buttonTitle': 'Send', + 'placeholder': 'Placeholder', + }, + ], + 'options': [], + } + ], + }) + ]); + }); + test('initialize with all settings off', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + defaultPresentAlert: false, + defaultPresentBadge: false, + defaultPresentSound: false); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': false, + 'requestSoundPermission': false, + 'requestBadgePermission': false, + 'defaultPresentAlert': false, + 'defaultPresentSound': false, + 'defaultPresentBadge': false, + 'notificationCategories': [], + }) + ]); + }); + + test('show without iOS-specific details', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + await flutterLocalNotificationsPlugin.show( + 1, 'notification title', 'notification body', null); + expect( + log.last, + isMethodCall('show', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'platformSpecifics': null, + })); + }); + + test('show with iOS-specific details', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + const NotificationDetails notificationDetails = NotificationDetails( + iOS: IOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + subtitle: 'a subtitle', + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + IOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373'), + ], + categoryIdentifier: 'category1', + ), + ); + + await flutterLocalNotificationsPlugin.show( + 1, 'notification title', 'notification body', notificationDetails); + + expect( + log.last, + isMethodCall('show', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'platformSpecifics': { + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'subtitle': 'a subtitle', + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + }, + })); + }); + + group('periodicallyShow', () { + final DateTime now = DateTime(2020, 10, 9); + for (final RepeatInterval repeatInterval in RepeatInterval.values) { + test('$repeatInterval', () async { + await withClock(Clock.fixed(now), () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + + const NotificationDetails notificationDetails = NotificationDetails( + iOS: IOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + IOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.periodicallyShow( + 1, + 'notification title', + 'notification body', + repeatInterval, + notificationDetails, + ); + + expect( + log.last, + isMethodCall('periodicallyShow', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'calledAt': now.millisecondsSinceEpoch, + 'repeatInterval': repeatInterval.index, + 'platformSpecifics': { + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'subtitle': null, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + 'categoryIdentifier': 'category1', + }, + })); + }); + }); + } + }); + + group('zonedSchedule', () { + test('no repeat frequency', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Australia/Sydney')); + final tz.TZDateTime scheduledDate = + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); + const NotificationDetails notificationDetails = NotificationDetails( + iOS: IOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + IOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 1, + 'notification title', + 'notification body', + scheduledDate, + notificationDetails, + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime); + + expect( + log.last, + isMethodCall('zonedSchedule', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'uiLocalNotificationDateInterpretation': + UILocalNotificationDateInterpretation.absoluteTime.index, + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'timeZoneName': 'Australia/Sydney', + 'platformSpecifics': { + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'subtitle': null, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + 'categoryIdentifier': null, + }, + })); + }); + + test('match time components', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Australia/Sydney')); + final tz.TZDateTime scheduledDate = + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); + const NotificationDetails notificationDetails = NotificationDetails( + iOS: IOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + IOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 1, + 'notification title', + 'notification body', + scheduledDate, + notificationDetails, + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.time); + + expect( + log.last, + isMethodCall('zonedSchedule', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'uiLocalNotificationDateInterpretation': + UILocalNotificationDateInterpretation.absoluteTime.index, + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'timeZoneName': 'Australia/Sydney', + 'matchDateTimeComponents': DateTimeComponents.time.index, + 'platformSpecifics': { + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'subtitle': null, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + 'categoryIdentifier': null, + }, + })); + }); + + test('match day of week and time components', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Australia/Sydney')); + final tz.TZDateTime scheduledDate = + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); + const NotificationDetails notificationDetails = NotificationDetails( + iOS: IOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + IOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 1, + 'notification title', + 'notification body', + scheduledDate, + notificationDetails, + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); + + expect( + log.last, + isMethodCall('zonedSchedule', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'uiLocalNotificationDateInterpretation': + UILocalNotificationDateInterpretation.absoluteTime.index, + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'timeZoneName': 'Australia/Sydney', + 'matchDateTimeComponents': + DateTimeComponents.dayOfWeekAndTime.index, + 'platformSpecifics': { + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'subtitle': null, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + 'categoryIdentifier': null, + }, + })); + }); + }); + test('requestPermissions with default settings', () async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>()! + .requestPermissions(); + expect(log, [ + isMethodCall('requestPermissions', arguments: { + 'sound': false, + 'badge': false, + 'alert': false, + }) + ]); + }); + test('requestPermissions with all settings requested', () async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>()! + .requestPermissions(sound: true, badge: true, alert: true); + expect(log, [ + isMethodCall('requestPermissions', arguments: { + 'sound': true, + 'badge': true, + 'alert': true, + }) + ]); + }); + test('cancel', () async { + await flutterLocalNotificationsPlugin.cancel(1); + expect(log, [isMethodCall('cancel', arguments: 1)]); + }); + + test('cancelAll', () async { + await flutterLocalNotificationsPlugin.cancelAll(); + expect(log, [isMethodCall('cancelAll', arguments: null)]); + }); + + test('pendingNotificationRequests', () async { + await flutterLocalNotificationsPlugin.pendingNotificationRequests(); + expect(log, [ + isMethodCall('pendingNotificationRequests', arguments: null) + ]); + }); + + test('getNotificationAppLaunchDetails', () async { + await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + expect(log, [ + isMethodCall('getNotificationAppLaunchDetails', arguments: null) + ]); + }); + }); +} diff --git a/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart new file mode 100644 index 000000000..18b100a29 --- /dev/null +++ b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart @@ -0,0 +1,445 @@ +import 'package:clock/clock.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +import 'utils/date_formatter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + group('macOS', () { + const MethodChannel channel = + MethodChannel('dexterous.com/flutter/local_notifications'); + final List log = []; + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + // ignore: always_specify_types + channel.setMockMethodCallHandler((methodCall) async { + log.add(methodCall); + if (methodCall.method == 'pendingNotificationRequests') { + return ?>[]; + } else if (methodCall.method == 'getNotificationAppLaunchDetails') { + return null; + } + }); + }); + + tearDown(() { + log.clear(); + }); + + test('initialize with default parameter values', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': true, + 'requestSoundPermission': true, + 'requestBadgePermission': true, + 'defaultPresentAlert': true, + 'defaultPresentSound': true, + 'defaultPresentBadge': true, + }) + ]); + }); + + test('initialize with all settings off', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + defaultPresentAlert: false, + defaultPresentBadge: false, + defaultPresentSound: false); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': false, + 'requestSoundPermission': false, + 'requestBadgePermission': false, + 'defaultPresentAlert': false, + 'defaultPresentSound': false, + 'defaultPresentBadge': false, + }) + ]); + }); + + test('show without macOS-specific details', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + await flutterLocalNotificationsPlugin.show( + 1, 'notification title', 'notification body', null); + expect( + log.last, + isMethodCall('show', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'platformSpecifics': null, + })); + }); + + test('show with macOS-specific details', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + const NotificationDetails notificationDetails = NotificationDetails( + macOS: MacOSNotificationDetails( + subtitle: 'a subtitle', + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + threadIdentifier: 'thread', + attachments: [ + MacOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373'), + ])); + + await flutterLocalNotificationsPlugin.show( + 1, 'notification title', 'notification body', notificationDetails); + + expect( + log.last, + isMethodCall('show', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'platformSpecifics': { + 'subtitle': 'a subtitle', + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': 'thread', + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + }, + })); + }); + + group('periodicallyShow', () { + final DateTime now = DateTime(2020, 10, 9); + for (final RepeatInterval repeatInterval in RepeatInterval.values) { + test('$repeatInterval', () async { + await withClock(Clock.fixed(now), () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + + const NotificationDetails notificationDetails = NotificationDetails( + macOS: MacOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + MacOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.periodicallyShow( + 1, + 'notification title', + 'notification body', + repeatInterval, + notificationDetails, + ); + + expect( + log.last, + isMethodCall('periodicallyShow', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'calledAt': now.millisecondsSinceEpoch, + 'repeatInterval': repeatInterval.index, + 'platformSpecifics': { + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'subtitle': null, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + }, + })); + }); + }); + } + }); + + group('zonedSchedule', () { + test('no repeat frequency', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Australia/Sydney')); + final tz.TZDateTime scheduledDate = + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); + const NotificationDetails notificationDetails = NotificationDetails( + macOS: MacOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + MacOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 1, + 'notification title', + 'notification body', + scheduledDate, + notificationDetails, + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime); + + expect( + log.last, + isMethodCall('zonedSchedule', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'timeZoneName': 'Australia/Sydney', + 'platformSpecifics': { + 'subtitle': null, + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + }, + })); + }); + + test('match time components', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Australia/Sydney')); + final tz.TZDateTime scheduledDate = + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); + const NotificationDetails notificationDetails = NotificationDetails( + macOS: MacOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + MacOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 1, + 'notification title', + 'notification body', + scheduledDate, + notificationDetails, + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.time); + + expect( + log.last, + isMethodCall('zonedSchedule', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'timeZoneName': 'Australia/Sydney', + 'matchDateTimeComponents': DateTimeComponents.time.index, + 'platformSpecifics': { + 'subtitle': null, + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + }, + })); + }); + + test('weekly repeat frequency', () async { + const MacOSInitializationSettings macOSInitializationSettings = + MacOSInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(macOS: macOSInitializationSettings); + await flutterLocalNotificationsPlugin + .initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Australia/Sydney')); + final tz.TZDateTime scheduledDate = + tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)); + const NotificationDetails notificationDetails = NotificationDetails( + macOS: MacOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + sound: 'sound.mp3', + badgeNumber: 1, + attachments: [ + MacOSNotificationAttachment('video.mp4', + identifier: '2b3f705f-a680-4c9f-8075-a46a70e28373') + ])); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 1, + 'notification title', + 'notification body', + scheduledDate, + notificationDetails, + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); + + expect( + log.last, + isMethodCall('zonedSchedule', arguments: { + 'id': 1, + 'title': 'notification title', + 'body': 'notification body', + 'payload': '', + 'scheduledDateTime': convertDateToISO8601String(scheduledDate), + 'timeZoneName': 'Australia/Sydney', + 'matchDateTimeComponents': + DateTimeComponents.dayOfWeekAndTime.index, + 'platformSpecifics': { + 'subtitle': null, + 'presentAlert': true, + 'presentBadge': true, + 'presentSound': true, + 'sound': 'sound.mp3', + 'badgeNumber': 1, + 'threadIdentifier': null, + 'attachments': >[ + { + 'filePath': 'video.mp4', + 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + } + ], + }, + })); + }); + }); + + test('requestPermissions with default settings', () async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>()! + .requestPermissions(); + expect(log, [ + isMethodCall('requestPermissions', arguments: { + 'sound': null, + 'badge': null, + 'alert': null, + }) + ]); + }); + test('requestPermissions with all settings requested', () async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>()! + .requestPermissions(sound: true, badge: true, alert: true); + expect(log, [ + isMethodCall('requestPermissions', arguments: { + 'sound': true, + 'badge': true, + 'alert': true, + }) + ]); + }); + test('cancel', () async { + await flutterLocalNotificationsPlugin.cancel(1); + expect(log, [isMethodCall('cancel', arguments: 1)]); + }); + + test('cancelAll', () async { + await flutterLocalNotificationsPlugin.cancelAll(); + expect(log, [isMethodCall('cancelAll', arguments: null)]); + }); + + test('pendingNotificationRequests', () async { + await flutterLocalNotificationsPlugin.pendingNotificationRequests(); + expect(log, [ + isMethodCall('pendingNotificationRequests', arguments: null) + ]); + }); + + test('getNotificationAppLaunchDetails', () async { + await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + expect(log, [ + isMethodCall('getNotificationAppLaunchDetails', arguments: null) + ]); + }); + }); +} diff --git a/flutter_local_notifications/test/utils/date_formatter.dart b/flutter_local_notifications/test/utils/date_formatter.dart new file mode 100644 index 000000000..d856276c8 --- /dev/null +++ b/flutter_local_notifications/test/utils/date_formatter.dart @@ -0,0 +1,27 @@ +import 'package:timezone/timezone.dart' as tz; + +String convertDateToISO8601String(tz.TZDateTime dateTime) { + String _twoDigits(int n) { + if (n >= 10) { + return '$n'; + } + return '0$n'; + } + + String _fourDigits(int n) { + final int absN = n.abs(); + final String sign = n < 0 ? '-' : ''; + if (absN >= 1000) { + return '$n'; + } + if (absN >= 100) { + return '${sign}0$absN'; + } + if (absN >= 10) { + return '${sign}00$absN'; + } + return '${sign}000$absN'; + } + + return '${_fourDigits(dateTime.year)}-${_twoDigits(dateTime.month)}-${_twoDigits(dateTime.day)}T${_twoDigits(dateTime.hour)}:${_twoDigits(dateTime.minute)}:${_twoDigits(dateTime.second)}'; // ignore: lines_longer_than_80_chars +} diff --git a/flutter_local_notifications_linux/.gitignore b/flutter_local_notifications_linux/.gitignore new file mode 100644 index 000000000..29fb357a3 --- /dev/null +++ b/flutter_local_notifications_linux/.gitignore @@ -0,0 +1,32 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ diff --git a/flutter_local_notifications_linux/CHANGELOG.md b/flutter_local_notifications_linux/CHANGELOG.md new file mode 100644 index 000000000..095fdb44c --- /dev/null +++ b/flutter_local_notifications_linux/CHANGELOG.md @@ -0,0 +1,21 @@ +## [0.3.0] + +* **Breaking change** the `SelectNotificationCallback` typedef now maps to a function that returns `void` instead of a `Future`. This change was done to better communicate the plugin doesn't actually await any asynchronous computation and is similar to how button pressed callbacks work for Flutter where they are typically use [`VoidCallback`](https://api.flutter.dev/flutter/dart-ui/VoidCallback.html) + +## [0.2.0+1] + +* Fixed links to GNOME developer documentation referenced in API docs + +## [0.2.0] + +* Fixed issue when an app using the plugin is built on the web by using conditional imports +* Changed the logic where notification IDs are saved so that `$XDG_RUNTIME_DIR` environment variable is not set but `$TMPDIR` is set, then they are saved to a file within the `/$TMPDIR/APP_NAME/USER_ID/SESSION_ID` directory. If `$TMPDIR` is not set then, it would save to `/tmp/APP_NAME/USER_ID/SESSION_ID` +* Fixed an issue where errors would occur if the plugin was initialised multiple times + +## [0.1.0+1] + +* Point to types within platform interface + +## [0.1.0] + +* Initial version for Linux diff --git a/flutter_local_notifications_linux/LICENSE b/flutter_local_notifications_linux/LICENSE new file mode 100644 index 000000000..d666ea446 --- /dev/null +++ b/flutter_local_notifications_linux/LICENSE @@ -0,0 +1,27 @@ +Copyright 2018 Michael Bui. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter_local_notifications_linux/README.md b/flutter_local_notifications_linux/README.md new file mode 100644 index 000000000..9fb2828c7 --- /dev/null +++ b/flutter_local_notifications_linux/README.md @@ -0,0 +1,8 @@ +# Flutter Local Notifications Linux plugin + +The Linux implementation of [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications). + +## Usage + +This package is already included as part of the `flutter_local_notifications` package dependency, and will +be included when using `flutter_local_notifications` as normal. diff --git a/flutter_local_notifications_linux/lib/flutter_local_notifications_linux.dart b/flutter_local_notifications_linux/lib/flutter_local_notifications_linux.dart new file mode 100644 index 000000000..e6b1cc4bb --- /dev/null +++ b/flutter_local_notifications_linux/lib/flutter_local_notifications_linux.dart @@ -0,0 +1,18 @@ +/// The Linux implementation of `flutter_local_notifications`. +library flutter_local_notifications_linux; + +// flutter_local_notifications_linux depends on dbus and posix +// which uses FFI internally; export a stub for platforms that don't +// support FFI (e.g., web) to avoid having transitive dependencies +// break web compilation. +export 'src/flutter_local_notifications_stub.dart' + if (dart.library.ffi) 'src/flutter_local_notifications.dart'; +export 'src/model/capabilities.dart'; +export 'src/model/categories.dart'; +export 'src/model/enums.dart'; +export 'src/model/icon.dart'; +export 'src/model/initialization_settings.dart'; +export 'src/model/location.dart'; +export 'src/model/notification_details.dart'; +export 'src/model/sound.dart'; +export 'src/model/timeout.dart'; diff --git a/flutter_local_notifications_linux/lib/src/dbus_wrapper.dart b/flutter_local_notifications_linux/lib/src/dbus_wrapper.dart new file mode 100644 index 000000000..facb27c13 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/dbus_wrapper.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:dbus/dbus.dart'; + +/// Mockable [DBusRemoteObject] wrapper +class DBusWrapper { + late final DBusRemoteObject _object; + late final String _destination; + + /// Build an instance of [DBusRemoteObject] + void build({ + required String destination, + required String path, + }) { + _destination = destination; + _object = DBusRemoteObject( + DBusClient.session(), + destination, + DBusObjectPath(path), + ); + } + + /// Invokes a method on this [DBusRemoteObject]. + /// Throws [DBusMethodResponseException] if the remote side returns an error. + /// + /// If [replySignature] is provided this causes this method to throw a + /// [DBusReplySignatureException] if the result is successful but the returned + /// values do not match the provided signature. + Future callMethod( + String? interface, + String name, + Iterable values, { + DBusSignature? replySignature, + bool noReplyExpected = false, + bool noAutoStart = false, + bool allowInteractiveAuthorization = false, + }) => + _object.callMethod( + interface, + name, + values, + replySignature: replySignature, + noReplyExpected: noReplyExpected, + noAutoStart: noAutoStart, + allowInteractiveAuthorization: allowInteractiveAuthorization, + ); + + /// Creates a stream of signal with the given [name]. + DBusRemoteObjectSignalStream subscribeSignal(String name) => + DBusRemoteObjectSignalStream(_object, _destination, name); +} diff --git a/flutter_local_notifications_linux/lib/src/file_system.dart b/flutter_local_notifications_linux/lib/src/file_system.dart new file mode 100644 index 000000000..feaa32b21 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/file_system.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +/// Mockable file system representation +// ignore: one_member_abstracts +abstract class FileSystem { + /// Returns a [File], that referred to the given [path] + File open(String path); +} + +/// A real implementation of [FileSystem] +class LocalFileSystem implements FileSystem { + @override + File open(String path) => File(path); +} diff --git a/flutter_local_notifications_linux/lib/src/flutter_local_notifications.dart b/flutter_local_notifications_linux/lib/src/flutter_local_notifications.dart new file mode 100644 index 000000000..c49cefe3c --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/flutter_local_notifications.dart @@ -0,0 +1,87 @@ +import 'package:flutter/foundation.dart'; + +import 'flutter_local_notifications_platform_linux.dart'; +import 'model/capabilities.dart'; +import 'model/initialization_settings.dart'; +import 'model/notification_details.dart'; +import 'notifications_manager.dart'; + +/// Linux implementation of the local notifications plugin. +class LinuxFlutterLocalNotificationsPlugin + extends FlutterLocalNotificationsPlatformLinux { + /// Constructs an instance of [LinuxNotificationDetails]. + LinuxFlutterLocalNotificationsPlugin() + : _manager = LinuxNotificationManager(); + + /// Constructs an instance of [LinuxNotificationDetails] + /// with the give [manager]. + @visibleForTesting + LinuxFlutterLocalNotificationsPlugin.private( + LinuxNotificationManager manager, + ) : _manager = manager; + + final LinuxNotificationManager _manager; + + /// Initializes the plugin. + /// + /// Call this method on application before using the plugin further. + /// This should only be done once. When a notification created by this plugin + /// was used to launch the app, calling `initialize` is what will trigger to + /// the `onSelectNotification` callback to be fire. + @override + Future initialize( + LinuxInitializationSettings initializationSettings, { + SelectNotificationCallback? onSelectNotification, + }) async { + await _manager.initialize( + initializationSettings, + onSelectNotification: onSelectNotification, + ); + } + + /// Show a notification with an optional payload that will be passed back to + /// the app when a notification is tapped on. + @override + Future show( + int id, + String? title, + String? body, { + LinuxNotificationDetails? notificationDetails, + String? payload, + }) { + validateId(id); + return _manager.show( + id, + title, + body, + details: notificationDetails, + payload: payload, + ); + } + + @override + Future cancel(int id) { + validateId(id); + return _manager.cancel(id); + } + + @override + Future cancelAll() => _manager.cancelAll(); + + /// Returns the system notification server capabilities. + /// Some functionality may not be implemented by the notification server, + /// conforming clients should check if it is available before using it. + @override + Future getCapabilities() => + _manager.getCapabilities(); + + /// Returns a [Map] with the specified notification id as the key + /// and the id, assigned by the system, as the value. + /// + /// Note: the system ID is unique only within the current user session, + /// so it's undesirable to save it to persistable storage without any + /// invalidation/update. For more information, please see + /// Desktop Notifications Specification https://specifications.freedesktop.org/notification-spec/latest/ar01s02.html + @override + Future> getSystemIdMap() => _manager.getSystemIdMap(); +} diff --git a/flutter_local_notifications_linux/lib/src/flutter_local_notifications_platform_linux.dart b/flutter_local_notifications_linux/lib/src/flutter_local_notifications_platform_linux.dart new file mode 100644 index 000000000..1a49c7c6e --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/flutter_local_notifications_platform_linux.dart @@ -0,0 +1,45 @@ +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; + +import 'model/capabilities.dart'; +import 'model/initialization_settings.dart'; +import 'model/notification_details.dart'; + +export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; + +/// The interface that all implementations of flutter_local_notifications_linux +/// must implement. +abstract class FlutterLocalNotificationsPlatformLinux + extends FlutterLocalNotificationsPlatform { + /// Initializes the plugin. + /// + /// Call this method on application before using the plugin further. + Future initialize( + LinuxInitializationSettings initializationSettings, { + SelectNotificationCallback? onSelectNotification, + }); + + /// Show a notification with an optional payload that will be passed back to + /// the app when a notification is tapped on. + @override + Future show( + int id, + String? title, + String? body, { + LinuxNotificationDetails? notificationDetails, + String? payload, + }); + + /// Returns the system notification server capabilities. + /// Some functionality may not be implemented by the notification server, + /// conforming clients should check if it is available before using it. + Future getCapabilities(); + + /// Returns a [Map] with the specified notification id as the key + /// and the id, assigned by the system, as the value. + /// + /// Note: the system ID is unique only within the current user session, + /// so it's undesirable to save it to persistable storage without any + /// invalidation/update. For more information, please see + /// Desktop Notifications Specification https://specifications.freedesktop.org/notification-spec/latest/ar01s02.html + Future> getSystemIdMap(); +} diff --git a/flutter_local_notifications_linux/lib/src/flutter_local_notifications_stub.dart b/flutter_local_notifications_linux/lib/src/flutter_local_notifications_stub.dart new file mode 100644 index 000000000..fde2ed2d6 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/flutter_local_notifications_stub.dart @@ -0,0 +1,59 @@ +import 'flutter_local_notifications_platform_linux.dart'; +import 'model/capabilities.dart'; +import 'model/initialization_settings.dart'; +import 'model/notification_details.dart'; + +/// A stub implementation to satisfy compilation of multi-platform packages that +/// depend on flutter_local_notifications_linux. +/// This should never actually be created. +/// +/// Notably, because flutter_local_notifications needs to manually register +/// flutter_local_notifications_linux, anything with a transitive dependency on +/// flutter_local_notifications will also depend on +/// flutter_local_notifications_linux, not just at +/// the pubspec level but the code level. +class LinuxFlutterLocalNotificationsPlugin + extends FlutterLocalNotificationsPlatformLinux { + /// Errors on attempted instantiation of the stub. It exists only to satisfy + /// compile-time dependencies, and should never actually be created. + LinuxFlutterLocalNotificationsPlugin() : assert(false); + + /// Errors on attempted calling of the stub. It exists only to satisfy + /// compile-time dependencies, and should never actually be called. + @override + Future initialize( + LinuxInitializationSettings initializationSettings, { + SelectNotificationCallback? onSelectNotification, + }) async { + assert(false); + } + + /// Errors on attempted calling of the stub. It exists only to satisfy + /// compile-time dependencies, and should never actually be called. + @override + Future show( + int id, + String? title, + String? body, { + LinuxNotificationDetails? notificationDetails, + String? payload, + }) async { + assert(false); + } + + /// Errors on attempted calling of the stub. It exists only to satisfy + /// compile-time dependencies, and should never actually be called. + @override + Future getCapabilities() async { + assert(false); + throw UnimplementedError(); + } + + /// Errors on attempted calling of the stub. It exists only to satisfy + /// compile-time dependencies, and should never actually be called. + @override + Future> getSystemIdMap() async { + assert(false); + throw UnimplementedError(); + } +} diff --git a/flutter_local_notifications_linux/lib/src/helpers.dart b/flutter_local_notifications_linux/lib/src/helpers.dart new file mode 100644 index 000000000..ef2c9503d --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/helpers.dart @@ -0,0 +1,61 @@ +import 'package:dbus/dbus.dart'; + +import 'model/enums.dart'; +import 'model/hint.dart'; + +/// [LinuxHintValue] utils. +extension LinuxHintValueExtension on LinuxHintValue { + /// Convert value to [DBusValue]. + DBusValue toDBusValue() { + switch (type) { + case LinuxHintValueType.array: + final List l = value as List; + return DBusArray( + l.first.toDBusValue().signature, + l.map((LinuxHintValue v) => v.toDBusValue()), + ); + case LinuxHintValueType.boolean: + return DBusBoolean(value); + case LinuxHintValueType.byte: + return DBusByte(value); + case LinuxHintValueType.dict: + final Map m = + value as Map; + return DBusDict( + m.keys.first.toDBusValue().signature, + m.values.first.toDBusValue().signature, + m.map( + (LinuxHintValue key, LinuxHintValue value) => + MapEntry( + key.toDBusValue(), + value.toDBusValue(), + ), + ), + ); + case LinuxHintValueType.double: + return DBusDouble(value); + case LinuxHintValueType.int16: + return DBusInt16(value); + case LinuxHintValueType.int32: + return DBusInt32(value); + case LinuxHintValueType.int64: + return DBusInt64(value); + case LinuxHintValueType.string: + return DBusString(value); + case LinuxHintValueType.struct: + return DBusStruct( + (value as List).map( + (LinuxHintValue v) => v.toDBusValue(), + ), + ); + case LinuxHintValueType.uint16: + return DBusUint16(value); + case LinuxHintValueType.uint32: + return DBusUint32(value); + case LinuxHintValueType.uint64: + return DBusUint64(value); + case LinuxHintValueType.variant: + return DBusVariant((value as LinuxHintValue).toDBusValue()); + } + } +} diff --git a/flutter_local_notifications_linux/lib/src/model/capabilities.dart b/flutter_local_notifications_linux/lib/src/model/capabilities.dart new file mode 100644 index 000000000..01c5fcf53 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/capabilities.dart @@ -0,0 +1,135 @@ +import 'package:flutter/foundation.dart'; + +import 'initialization_settings.dart'; +import 'notification_details.dart'; +import 'sound.dart'; + +// TODO(proninyaroslav): add actions + +/// Represents capabilities, implemented by the Linux notification server. +@immutable +class LinuxServerCapabilities { + /// Constructs an instance of [LinuxServerCapabilities] + const LinuxServerCapabilities({ + required this.otherCapabilities, + required this.body, + required this.bodyHyperlinks, + required this.bodyImages, + required this.bodyMarkup, + required this.iconMulti, + required this.iconStatic, + required this.persistence, + required this.sound, + }); + + /// Set of unknown capabilities. + /// Vendor-specific capabilities may be specified as long as they start with + /// `x-vendor`. For example, `x-gnome-foo-cap`. Capability names must not + /// contain spaces. They are limited to alpha-numeric characters and + /// dashes ("-") + final Set otherCapabilities; + + /// Supports body text. Some implementations may only show the title + /// (for instance, onscreen displays, marquee/scrollers). + final bool body; + + /// The server supports hyperlinks in the notifications. + final bool bodyHyperlinks; + + /// The server supports images in the notifications. + final bool bodyImages; + + /// Supports markup in the body text. The markup is XML-based, and consists + /// of a small subset of HTML along with a few additional tags. + /// For more information, see Desktop Notifications Specification https://specifications.freedesktop.org/notification-spec/latest/ar01s04.html + /// If marked up text is sent to a server + /// that does not give this cap, the markup will show through as regular text + /// so must be stripped clientside. + final bool bodyMarkup; + + /// The server will render an animation of all the frames in a given + /// image array. The client may still specify multiple frames even if this + /// cap and/or [iconStatic] is missing, however the server is free to ignore + /// them and use only the primary frame. + final bool iconMulti; + + /// Supports display of exactly 1 frame of any given image array. + /// This value is mutually exclusive with [iconMulti], it is a protocol + /// error for the server to specify both. + final bool iconStatic; + + /// The server supports persistence of notifications. Notifications will be + /// retained until they are acknowledged or removed by the user or + /// recalled by the sender. The presence of this capability allows clients to + /// depend on the server to ensure a notification is seen and eliminate + /// the need for the client to display a reminding function + /// (such as a status icon) of its own. + final bool persistence; + + /// The server supports sounds on notifications. If returned, the server must + /// support the [AssetsLinuxSound], [LinuxNotificationDetails.suppressSound] + /// and [LinuxInitializationSettings.defaultSuppressSound]. + final bool sound; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LinuxServerCapabilities && + setEquals(other.otherCapabilities, otherCapabilities) && + other.body == body && + other.bodyHyperlinks == bodyHyperlinks && + other.bodyImages == bodyImages && + other.bodyMarkup == bodyMarkup && + other.iconMulti == iconMulti && + other.iconStatic == iconStatic && + other.persistence == persistence && + other.sound == sound; + } + + @override + int get hashCode => + otherCapabilities.hashCode ^ + body.hashCode ^ + bodyHyperlinks.hashCode ^ + bodyImages.hashCode ^ + bodyMarkup.hashCode ^ + iconMulti.hashCode ^ + iconStatic.hashCode ^ + persistence.hashCode ^ + sound.hashCode; + + /// Creates a copy of this object, + /// but with the given fields replaced with the new values. + LinuxServerCapabilities copyWith({ + Set? otherCapabilities, + bool? body, + bool? bodyHyperlinks, + bool? bodyImages, + bool? bodyMarkup, + bool? iconMulti, + bool? iconStatic, + bool? persistence, + bool? sound, + }) => + LinuxServerCapabilities( + otherCapabilities: otherCapabilities ?? this.otherCapabilities, + body: body ?? this.body, + bodyHyperlinks: bodyHyperlinks ?? this.bodyHyperlinks, + bodyImages: bodyImages ?? this.bodyImages, + bodyMarkup: bodyMarkup ?? this.bodyMarkup, + iconMulti: iconMulti ?? this.iconMulti, + iconStatic: iconStatic ?? this.iconStatic, + persistence: persistence ?? this.persistence, + sound: sound ?? this.sound, + ); + + @override + String toString() => 'LinuxServerCapabilities(otherCapabilities: ' + '$otherCapabilities, body: $body, bodyHyperlinks: $bodyHyperlinks, ' + 'bodyImages: $bodyImages, bodyMarkup: $bodyMarkup, ' + 'iconMulti: $iconMulti, iconStatic: $iconStatic, ' + 'persistence: $persistence, sound: $sound)'; +} diff --git a/flutter_local_notifications_linux/lib/src/model/categories.dart b/flutter_local_notifications_linux/lib/src/model/categories.dart new file mode 100644 index 000000000..fd6f5423d --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/categories.dart @@ -0,0 +1,119 @@ +import 'package:flutter/foundation.dart'; + +/// Categories of notifications. +@immutable +class LinuxNotificationCategory { + /// Constructs an instance of [LinuxNotificationCategory] + /// with a given [name] of category. + const LinuxNotificationCategory(this.name); + + /// A generic device-related notification + /// that doesn't fit into any other category. + factory LinuxNotificationCategory.device() => + const LinuxNotificationCategory('device'); + + /// A device, such as a USB device, was added to the system. + factory LinuxNotificationCategory.deviceAdded() => + const LinuxNotificationCategory('device.added'); + + /// A device had some kind of error. + factory LinuxNotificationCategory.deviceError() => + const LinuxNotificationCategory('device.error'); + + /// A device, such as a USB device, was removed from the system. + factory LinuxNotificationCategory.deviceRemoved() => + const LinuxNotificationCategory('device.removed'); + + /// A generic e-mail-related notification + /// that doesn't fit into any other category. + factory LinuxNotificationCategory.email() => + const LinuxNotificationCategory('email'); + + /// A new e-mail notification. + factory LinuxNotificationCategory.emailArrived() => + const LinuxNotificationCategory('email.arrived'); + + /// A notification stating that an e-mail has bounced. + factory LinuxNotificationCategory.emailBounced() => + const LinuxNotificationCategory('email.bounced'); + + /// A generic instant message-related notification + /// that doesn't fit into any other + factory LinuxNotificationCategory.im() => + const LinuxNotificationCategory('im'); + + /// An instant message error notification. + factory LinuxNotificationCategory.imError() => + const LinuxNotificationCategory('im.error'); + + /// A received instant message notification. + factory LinuxNotificationCategory.imReceived() => + const LinuxNotificationCategory('im.received'); + + /// A generic network notification that + /// doesn't fit into any other category. + factory LinuxNotificationCategory.network() => + const LinuxNotificationCategory('network'); + + /// A network connection notification, + /// such as successful sign-on to a network service. + /// This should not be confused with + /// [LinuxNotificationCategory.deviceAdded] for new network devices. + factory LinuxNotificationCategory.networkConnected() => + const LinuxNotificationCategory('network.connected'); + + /// A network disconnected notification. + /// This should not be confused with [LinuxNotificationCategory.deviceRemoved] + /// for disconnected network devices. + factory LinuxNotificationCategory.networkDisconnected() => + const LinuxNotificationCategory('network.disconnected'); + + /// A network-related or connection-related error. + factory LinuxNotificationCategory.networkError() => + const LinuxNotificationCategory('network.error'); + + /// A generic presence change notification + /// that doesn't fit into any other category, such as going away or idle. + factory LinuxNotificationCategory.presence() => + const LinuxNotificationCategory('presence'); + + /// An offline presence change notification. + factory LinuxNotificationCategory.presenceOffile() => + const LinuxNotificationCategory('presence.offline'); + + /// An online presence change notification. + factory LinuxNotificationCategory.presenceOnline() => + const LinuxNotificationCategory('presence.online'); + + /// A generic file transfer or download notification + /// that doesn't fit into any other category. + factory LinuxNotificationCategory.transfer() => + const LinuxNotificationCategory('transfer'); + + /// A file transfer or download complete notification. + factory LinuxNotificationCategory.transferComplete() => + const LinuxNotificationCategory('transfer.complete'); + + /// A file transfer or download error. + factory LinuxNotificationCategory.transferError() => + const LinuxNotificationCategory('transfer.error'); + + /// Name of category. + final String name; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LinuxNotificationCategory && + other.name == name; + } + + @override + int get hashCode => name.hashCode; + + @override + String toString() => 'LinuxNotificationCategory(name: $name)'; +} diff --git a/flutter_local_notifications_linux/lib/src/model/enums.dart b/flutter_local_notifications_linux/lib/src/model/enums.dart new file mode 100644 index 000000000..bb7da9d0e --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/enums.dart @@ -0,0 +1,89 @@ +import 'icon.dart'; + +/// The urgency level of the Linux notification. +class LinuxNotificationUrgency { + /// Constructs an instance of [LinuxNotificationUrgency]. + const LinuxNotificationUrgency(this.value); + + /// Low urgency. Used for unimportant notifications. + static const LinuxNotificationUrgency low = LinuxNotificationUrgency(0); + + /// Normal urgency. Used for most standard notifications. + static const LinuxNotificationUrgency normal = LinuxNotificationUrgency(1); + + /// Critical urgency. Used for very important notifications. + static const LinuxNotificationUrgency critical = LinuxNotificationUrgency(2); + + /// All the possible values for the [LinuxNotificationUrgency] enumeration. + static List get values => + [low, normal, critical]; + + /// The integer representation. + final int value; +} + +/// Specifies the Linux notification icon type. +enum LinuxIconType { + /// Icon from the Flutter Assets directory, see [AssetsLinuxIcon] + assets, + + /// Icon from a raw image data bytes, see [ByteDataLinuxIcon]. + byteData, + + /// System theme icon, see [ThemeLinuxIcon]. + theme, +} + +/// Specifies the Linux notification sound type. +enum LinuxSoundType { + /// Sound from the Flutter Assets directory, see [AssetsLinuxSound] + assets, + + /// System theme sound, see [ThemeLinuxSound]. + theme, +} + +/// Represents the notification hint value type. +enum LinuxHintValueType { + /// Ordered list of values of the same type. + array, + + /// Boolean value. + boolean, + + /// Unsigned 8 bit value. + byte, + + /// Associative array of values. + dict, + + /// 64-bit floating point value. + double, + + /// Signed 16-bit integer. + int16, + + /// Signed 32-bit integer. + int32, + + /// Signed 64-bit integer. + int64, + + /// Unicode text string. + string, + + /// Value that contains a fixed set of other values. + struct, + + /// Unsigned 16-bit integer. + uint16, + + /// Unsigned 32-bit integer. + uint32, + + /// Unsigned 64-bit integer. + uint64, + + /// Value that contains any type. + variant, +} diff --git a/flutter_local_notifications_linux/lib/src/model/hint.dart b/flutter_local_notifications_linux/lib/src/model/hint.dart new file mode 100644 index 000000000..60c27d441 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/hint.dart @@ -0,0 +1,209 @@ +import 'package:flutter/foundation.dart'; + +import 'enums.dart'; + +/// Represents a custom Linux notification hint. +/// Hints are a way to provide extra data to a notification server that +/// the server may be able to make use of. +/// For more information, please see Desktop Notifications Specification https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html +@optionalTypeArgs +class LinuxNotificationCustomHint { + /// Constructs an instance of [LinuxNotificationCustomHint]. + const LinuxNotificationCustomHint(this.name, this.value); + + /// Name of this hint. + /// The vendor hint name should be in the form of `x-vendor-name`. + final String name; + + /// Value corresponding to the hint. + final LinuxHintValue value; +} + +/// Represents abstract Linux notification hint value. +@optionalTypeArgs +abstract class LinuxHintValue { + /// Specifies the notification hint value type. + LinuxHintValueType get type; + + /// Value, corresponding to the Dart type system. + T get value; +} + +/// Ordered list of values of the same type. +class LinuxHintArrayValue + implements LinuxHintValue> { + /// Constructs an instance of [LinuxHintArrayValue]. + const LinuxHintArrayValue(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.array; + + @override + final List value; +} + +/// Boolean value. +class LinuxHintBoolValue extends LinuxHintValue { + /// Constructs an instance of [LinuxHintBoolValue]. + LinuxHintBoolValue(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.boolean; + + @override + final bool value; +} + +/// Unsigned 8 bit value. +class LinuxHintByteValue extends LinuxHintValue { + /// Constructs an instance of [LinuxHintByteValue]. + LinuxHintByteValue(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.byte; + + @override + final int value; +} + +/// Associative array of values. +class LinuxHintDictValue + extends LinuxHintValue> { + /// Constructs an instance of [LinuxHintDictValue]. + LinuxHintDictValue(this.value); + + /// Constructs an instance of [LinuxHintDictValue]. + LinuxHintDictValue.stringVariant(Map value) + : value = value.map( + (String key, V value) => MapEntry( + LinuxHintStringValue(key) as K, + value, + ), + ); + + @override + LinuxHintValueType get type => LinuxHintValueType.dict; + + @override + final Map value; +} + +/// 64-bit floating point value. +class LinuxHintDoubleValue extends LinuxHintValue { + /// Constructs an instance of [LinuxHintDoubleValue]. + LinuxHintDoubleValue(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.double; + + @override + final double value; +} + +/// Signed 16-bit integer. +class LinuxHintInt16Value extends LinuxHintValue { + /// Constructs an instance of [LinuxHintInt16Value]. + LinuxHintInt16Value(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.int16; + + @override + final int value; +} + +/// Signed 32-bit integer. +class LinuxHintInt32Value extends LinuxHintValue { + /// Constructs an instance of [LinuxHintInt32Value]. + LinuxHintInt32Value(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.int32; + + @override + final int value; +} + +/// Signed 64-bit integer. +class LinuxHintInt64Value extends LinuxHintValue { + /// Constructs an instance of [LinuxHintInt64Value]. + LinuxHintInt64Value(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.int64; + + @override + final int value; +} + +/// Unicode text string. +class LinuxHintStringValue extends LinuxHintValue { + /// Constructs an instance of [LinuxHintStringValue]. + LinuxHintStringValue(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.string; + + @override + final String value; +} + +/// Value that contains a fixed set of other values. +class LinuxHintStructValue extends LinuxHintValue> { + /// Constructs an instance of [LinuxHintStructValue]. + LinuxHintStructValue(Iterable value) : value = value.toList(); + + @override + LinuxHintValueType get type => LinuxHintValueType.struct; + + @override + final List value; +} + +/// Unsigned 16-bit integer. +class LinuxHintUint16Value extends LinuxHintValue { + /// Constructs an instance of [LinuxHintUint16Value]. + LinuxHintUint16Value(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.uint16; + + @override + final int value; +} + +/// Unsigned 32-bit integer. +class LinuxHintUint32Value extends LinuxHintValue { + /// Constructs an instance of [LinuxHintUint32Value]. + LinuxHintUint32Value(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.uint32; + + @override + final int value; +} + +/// Unsigned 64-bit integer. +class LinuxHintUint64Value extends LinuxHintValue { + /// Constructs an instance of [LinuxHintUint64Value]. + LinuxHintUint64Value(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.uint64; + + @override + final int value; +} + +/// Value that contains any type. +class LinuxHintVariantValue extends LinuxHintValue { + /// Constructs an instance of [LinuxHintVariantValue]. + LinuxHintVariantValue(this.value); + + @override + LinuxHintValueType get type => LinuxHintValueType.variant; + + @override + final LinuxHintValue value; +} diff --git a/flutter_local_notifications_linux/lib/src/model/icon.dart b/flutter_local_notifications_linux/lib/src/model/icon.dart new file mode 100644 index 000000000..e393b3702 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/icon.dart @@ -0,0 +1,94 @@ +import 'dart:typed_data'; + +import 'enums.dart'; + +/// Represents Linux notification icon. +abstract class LinuxNotificationIcon { + /// Implementation-defined icon content. + Object get content; + + /// Defines the type of icon. + LinuxIconType get type; +} + +/// Represents an icon from the Flutter Assets directory. +class AssetsLinuxIcon extends LinuxNotificationIcon { + /// Constructs an instance of [AssetsLinuxIcon]. + AssetsLinuxIcon(this.relativePath); + + @override + Object get content => relativePath; + + @override + LinuxIconType get type => LinuxIconType.assets; + + /// Icon relative path inside the Flutter Assets directory + final String relativePath; +} + +/// Represents an icon from a raw image data bytes, see [LinuxRawIconData]. +class ByteDataLinuxIcon extends LinuxNotificationIcon { + /// Constructs an instance of [ByteDataLinuxIcon]. + ByteDataLinuxIcon(this.iconData); + + @override + Object get content => iconData; + + @override + LinuxIconType get type => LinuxIconType.byteData; + + /// Icon data + final LinuxRawIconData iconData; +} + +/// Represents a system theme icon. +/// See https://www.freedesktop.org/wiki/Specifications/icon-naming-spec/ for more help. +class ThemeLinuxIcon extends LinuxNotificationIcon { + /// Constructs an instance of [ThemeLinuxIcon]. + ThemeLinuxIcon(this.name); + + @override + Object get content => name; + + @override + LinuxIconType get type => LinuxIconType.theme; + + /// Name in a freedesktop.org-compliant icon theme (not a GTK+ stock ID). + final String name; +} + +/// Represents an icon in the raw image data. +class LinuxRawIconData { + /// Constructs an instance of [LinuxRawIconData]. + LinuxRawIconData({ + required this.data, + required this.width, + required this.height, + int? rowStride, + this.bitsPerSample = 8, + this.channels = 3, + this.hasAlpha = false, + }) : rowStride = rowStride ?? ((width * channels * bitsPerSample) / 8).ceil(); + + /// Raw data (decoded from the image format) for the image in bytes. + final Uint8List data; + + /// Width of the image in pixels + final int width; + + /// Height of the image in pixels + final int height; + + /// The number of bytes per row in [data] + final int rowStride; + + /// The number of bits in each color sample + final int bitsPerSample; + + /// The number of channels in the image (e.g. 3 for RGB, 4 for RGBA). + /// If [hasAlpha] is `true`, must be 4. + final int channels; + + /// Determines if the image has an alpha channel + final bool hasAlpha; +} diff --git a/flutter_local_notifications_linux/lib/src/model/initialization_settings.dart b/flutter_local_notifications_linux/lib/src/model/initialization_settings.dart new file mode 100644 index 000000000..cd10aefc4 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/initialization_settings.dart @@ -0,0 +1,30 @@ +import 'icon.dart'; +import 'sound.dart'; + +/// Plugin initialization settings for Linux. +class LinuxInitializationSettings { + /// Constructs an instance of [LinuxInitializationSettings] + const LinuxInitializationSettings({ + required this.defaultActionName, + this.defaultIcon, + this.defaultSound, + this.defaultSuppressSound = false, + }); + + /// Name of the default action (usually triggered by clicking + /// the notification). + /// The name can be anything, though implementations are free not to + /// display it. + final String defaultActionName; + + /// Specifies the default icon for notifications. + final LinuxNotificationIcon? defaultIcon; + + /// Specifies the default sound for notifications. + /// Typical value is `ThemeLinuxSound('message')` + final LinuxNotificationSound? defaultSound; + + /// Causes the server to suppress playing any sounds, if it has that ability. + /// This is usually set when the client itself is going to play its own sound. + final bool defaultSuppressSound; +} diff --git a/flutter_local_notifications_linux/lib/src/model/location.dart b/flutter_local_notifications_linux/lib/src/model/location.dart new file mode 100644 index 000000000..bfbb9216d --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/location.dart @@ -0,0 +1,39 @@ +import 'package:flutter/foundation.dart'; + +/// Represents the location on the screen that the notification should point to. +@immutable +class LinuxNotificationLocation { + /// Constructs an instance of [LinuxNotificationLocation] + const LinuxNotificationLocation(this.x, this.y); + + /// Represents the `X` location on the screen that the notification + /// should point to. + final int x; + + /// Represents the `Y` location on the screen that the notification + /// should point to. + final int y; + + /// Creates a copy of this object, + /// but with the given fields replaced with the new values. + LinuxNotificationLocation copyWith({ + int? x, + int? y, + }) => + LinuxNotificationLocation(x ?? this.x, y ?? this.y); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LinuxNotificationLocation && other.x == x && other.y == y; + } + + @override + int get hashCode => x.hashCode ^ y.hashCode; + + @override + String toString() => 'LinuxNotificationLocation(x: $x, y: $y)'; +} diff --git a/flutter_local_notifications_linux/lib/src/model/notification_details.dart b/flutter_local_notifications_linux/lib/src/model/notification_details.dart new file mode 100644 index 000000000..6057b6a42 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/notification_details.dart @@ -0,0 +1,81 @@ +import 'capabilities.dart'; +import 'categories.dart'; +import 'enums.dart'; +import 'hint.dart'; +import 'icon.dart'; +import 'location.dart'; +import 'sound.dart'; +import 'timeout.dart'; + +/// Configures notification details specific to Linux. +/// The system may not support all features. +class LinuxNotificationDetails { + /// Constructs an instance of [LinuxNotificationDetails]. + const LinuxNotificationDetails({ + this.icon, + this.sound, + this.category, + this.urgency, + this.timeout = const LinuxNotificationTimeout.systemDefault(), + this.resident = false, + this.suppressSound = false, + this.transient = false, + this.location, + this.defaultActionName, + this.customHints, + }); + + /// Specifies the notification icon. + final LinuxNotificationIcon? icon; + + /// Specifies the notification sound. + /// Typical value is `ThemeLinuxSound('message')` + final LinuxNotificationSound? sound; + + /// Specifies the category for notification. + /// This can be used by the notification server to filter or + /// display the data in a certain way. + final LinuxNotificationCategory? category; + + /// Sets the urgency level for notification. + final LinuxNotificationUrgency? urgency; + + /// Sets the timeout for notification. + /// To set the default time, pass [LinuxNotificationTimeout.systemDefault] + /// value. To set the notification to never expire, + /// pass [LinuxNotificationTimeout.expiresNever]. + /// + /// Note that the timeout may be ignored by the server. + final LinuxNotificationTimeout timeout; + + /// When set the server will not automatically remove the notification + /// when an action has been invoked. The notification will remain resident in + /// the server until it is explicitly removed by the user or by the sender. + /// This option is likely only useful when the server has + /// the [LinuxServerCapabilities.persistence] capability. + final bool resident; + + /// Causes the server to suppress playing any sounds, if it has that ability. + /// This is usually set when the client itself is going to play its own sound. + final bool suppressSound; + + /// When set the server will treat the notification as transient and + /// by-pass the server's [LinuxServerCapabilities.persistence] capability, + /// if it should exist. + final bool transient; + + /// Specifies the location on the screen that the notification + /// should point to. + final LinuxNotificationLocation? location; + + /// Name of the default action (usually triggered by clicking + /// the notification). + /// The name can be anything, though implementations are free not to + /// display it. + final String? defaultActionName; + + /// Custom hints list to provide extra data to a notification server that + /// the server may be able to make use of. Before using, make sure that + /// the server supports this capability, see [LinuxServerCapabilities]. + final List? customHints; +} diff --git a/flutter_local_notifications_linux/lib/src/model/sound.dart b/flutter_local_notifications_linux/lib/src/model/sound.dart new file mode 100644 index 000000000..b0581da51 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/sound.dart @@ -0,0 +1,42 @@ +import 'enums.dart'; + +/// Represents Linux notification sound. +abstract class LinuxNotificationSound { + /// Implementation-defined sound content. + Object get content; + + /// Defines the type of sound. + LinuxSoundType get type; +} + +/// Represents a sound from the Flutter Assets directory. +class AssetsLinuxSound extends LinuxNotificationSound { + /// Constructs an instance of [AssetsLinuxSound]. + AssetsLinuxSound(this.relativePath); + + @override + Object get content => relativePath; + + @override + LinuxSoundType get type => LinuxSoundType.assets; + + /// Sound relative path inside the Flutter Assets directory + final String relativePath; +} + +/// Represents a system theme sound. +/// See https://www.freedesktop.org/wiki/Specifications/sound-theme-spec/ for more help. +class ThemeLinuxSound extends LinuxNotificationSound { + /// Constructs an instance of [ThemeLinuxSound]. + ThemeLinuxSound(this.name); + + @override + Object get content => name; + + @override + LinuxSoundType get type => LinuxSoundType.theme; + + /// A themeable named sound from the + /// freedesktop.org sound naming specification http://0pointer.de/public/sound-naming-spec.html + final String name; +} diff --git a/flutter_local_notifications_linux/lib/src/model/timeout.dart b/flutter_local_notifications_linux/lib/src/model/timeout.dart new file mode 100644 index 000000000..ff2392909 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/model/timeout.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart'; + +/// The timeout of the Linux notification. +@immutable +class LinuxNotificationTimeout { + /// Constructs an instance of [LinuxNotificationTimeout] + /// with a given [value] in milliseconds. + const LinuxNotificationTimeout(this.value); + + /// Constructs an instance of [LinuxNotificationTimeout] + /// with a given [Duration] value. + LinuxNotificationTimeout.fromDuration(Duration duration) + : value = duration.inMilliseconds; + + /// Constructs an instance of [LinuxNotificationTimeout] + /// with a [value] equal to `-1`. + /// The system default timeout value will be used. + const LinuxNotificationTimeout.systemDefault() : value = -1; + + /// Constructs an instance of [LinuxNotificationTimeout] + /// with a [value] equal to `0`. The notification will be never expires. + const LinuxNotificationTimeout.expiresNever() : value = 0; + + /// The integer representation in milliseconds. + final int value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LinuxNotificationTimeout && other.value == value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() => 'LinuxNotificationTimeout(value: $value)'; +} diff --git a/flutter_local_notifications_linux/lib/src/notification_info.dart b/flutter_local_notifications_linux/lib/src/notification_info.dart new file mode 100644 index 000000000..b3e5c54d9 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/notification_info.dart @@ -0,0 +1,66 @@ +import 'package:flutter/foundation.dart'; + +/// Represents a Linux notification information +@immutable +class LinuxNotificationInfo { + /// Constructs an instance of [LinuxPlatformInfoData]. + const LinuxNotificationInfo({ + required this.id, + required this.systemId, + this.payload, + }); + + /// Constructs an instance of [LinuxPlatformInfoData] from [json]. + factory LinuxNotificationInfo.fromJson(Map json) => + LinuxNotificationInfo( + id: json['id'] as int, + systemId: json['systemId'] as int, + payload: json['payload'] as String?, + ); + + /// Notification id + final int id; + + /// Notification id, which is returned by the system, + /// see Desktop Notifications Specification https://specifications.freedesktop.org/notification-spec/latest/ + final int systemId; + + /// Notification payload, that will be passed back to the app + /// when a notification is tapped on. + final String? payload; + + /// Returns the object as a key-value map + Map toJson() => { + 'id': id, + 'systemId': systemId, + 'payload': payload, + }; + + /// Creates a copy of this object, + /// but with the given fields replaced with the new values. + LinuxNotificationInfo copyWith({ + int? id, + int? systemId, + String? payload, + }) => + LinuxNotificationInfo( + id: id ?? this.id, + systemId: systemId ?? this.systemId, + payload: payload ?? this.payload, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LinuxNotificationInfo && + other.id == id && + other.systemId == systemId && + other.payload == payload; + } + + @override + int get hashCode => id.hashCode ^ systemId.hashCode ^ payload.hashCode; +} diff --git a/flutter_local_notifications_linux/lib/src/notifications_manager.dart b/flutter_local_notifications_linux/lib/src/notifications_manager.dart new file mode 100644 index 000000000..705c4ccce --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/notifications_manager.dart @@ -0,0 +1,346 @@ +import 'dart:async'; + +import 'package:dbus/dbus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:path/path.dart' as path; + +import 'dbus_wrapper.dart'; +import 'helpers.dart'; +import 'model/capabilities.dart'; +import 'model/enums.dart'; +import 'model/hint.dart'; +import 'model/icon.dart'; +import 'model/initialization_settings.dart'; +import 'model/location.dart'; +import 'model/notification_details.dart'; +import 'model/sound.dart'; +import 'model/timeout.dart'; +import 'notification_info.dart'; +import 'platform_info.dart'; +import 'storage.dart'; + +/// Linux notification manager and client +class LinuxNotificationManager { + /// Constructs an instance of of [LinuxNotificationManager] + LinuxNotificationManager() + : _dbus = DBusWrapper(), + _platformInfo = LinuxPlatformInfo(), + _storage = NotificationStorage(); + + /// Constructs an instance of of [LinuxNotificationManager] + /// with the given class dependencies. + @visibleForTesting + LinuxNotificationManager.private({ + DBusWrapper? dbus, + LinuxPlatformInfo? platformInfo, + NotificationStorage? storage, + }) : _dbus = dbus ?? DBusWrapper(), + _platformInfo = platformInfo ?? LinuxPlatformInfo(), + _storage = storage ?? NotificationStorage(); + + final DBusWrapper _dbus; + final LinuxPlatformInfo _platformInfo; + final NotificationStorage _storage; + + late final LinuxInitializationSettings _initializationSettings; + late final SelectNotificationCallback? _onSelectNotification; + late final LinuxPlatformInfoData _platformData; + + bool _initialized = false; + + /// Initializes the manager. + /// Call this method on application before using the manager further. + Future initialize( + LinuxInitializationSettings initializationSettings, { + SelectNotificationCallback? onSelectNotification, + }) async { + if (_initialized) { + return; + } + _initialized = true; + _initializationSettings = initializationSettings; + _onSelectNotification = onSelectNotification; + _dbus.build( + destination: _DBusInterfaceSpec.destination, + path: _DBusInterfaceSpec.path, + ); + _platformData = await _platformInfo.getAll(); + + await _storage.forceReloadCache(); + _subscribeSignals(); + } + + /// Show notification + Future show( + int id, + String? title, + String? body, { + LinuxNotificationDetails? details, + String? payload, + }) async { + final LinuxNotificationInfo? prevNotify = await _storage.getById(id); + final LinuxNotificationIcon? defaultIcon = + _initializationSettings.defaultIcon; + + final DBusMethodSuccessResponse result = await _dbus.callMethod( + _DBusInterfaceSpec.destination, + _DBusMethodsSpec.notify, + [ + // app_name + DBusString(_platformData.appName ?? ''), + // replaces_id + DBusUint32(prevNotify?.systemId ?? 0), + // app_icon + DBusString(_getAppIcon(details?.icon ?? defaultIcon) ?? ''), + // summary + DBusString(title ?? ''), + // body + DBusString(body ?? ''), + // actions + DBusArray.string(_buildActions(details, _initializationSettings)), + // hints + DBusDict.stringVariant(_buildHints(details, _initializationSettings)), + // expire_timeout + DBusInt32( + details?.timeout.value ?? + const LinuxNotificationTimeout.systemDefault().value, + ), + ], + replySignature: DBusSignature('u'), + ); + + final int systemId = (result.returnValues[0] as DBusUint32).value; + final LinuxNotificationInfo notify = prevNotify?.copyWith( + systemId: systemId, + payload: payload, + ) ?? + LinuxNotificationInfo( + id: id, + systemId: systemId, + payload: payload, + ); + await _storage.insert(notify); + } + + Map _buildHints( + LinuxNotificationDetails? details, + LinuxInitializationSettings initSettings, + ) { + final Map hints = {}; + final LinuxNotificationIcon? icon = + details?.icon ?? initSettings.defaultIcon; + if (icon?.type == LinuxIconType.byteData) { + final LinuxRawIconData data = icon!.content as LinuxRawIconData; + hints['image-data'] = DBusStruct( + [ + DBusInt32(data.width), + DBusInt32(data.height), + DBusInt32(data.rowStride), + DBusBoolean(data.hasAlpha), + DBusInt32(data.bitsPerSample), + DBusInt32(data.channels), + DBusArray.byte(data.data), + ], + ); + } + final LinuxNotificationSound? sound = + details?.sound ?? initSettings.defaultSound; + if (sound != null) { + switch (sound.type) { + case LinuxSoundType.assets: + hints['sound-file'] = DBusString( + path.join( + _platformData.assetsPath!, + sound.content as String, + ), + ); + break; + case LinuxSoundType.theme: + hints['sound-name'] = DBusString(sound.content as String); + break; + } + } + if (details?.category != null) { + hints['category'] = DBusString(details!.category!.name); + } + if (details?.urgency != null) { + hints['urgency'] = DBusByte(details!.urgency!.value); + } + if (details?.resident ?? false) { + hints['resident'] = const DBusBoolean(true); + } + final bool? suppressSound = + details?.suppressSound ?? initSettings.defaultSuppressSound; + if (suppressSound ?? false) { + hints['suppress-sound'] = const DBusBoolean(true); + } + if (details?.transient ?? false) { + hints['transient'] = const DBusBoolean(true); + } + if (details?.location != null) { + final LinuxNotificationLocation location = details!.location!; + hints['x'] = DBusByte(location.x); + hints['y'] = DBusByte(location.y); + } + if (details?.customHints != null) { + hints.addAll(_buildCustomHints(details!.customHints!)); + } + + return hints; + } + + Map _buildCustomHints( + List hints, + ) => + Map.fromEntries( + hints.map( + (LinuxNotificationCustomHint hint) => MapEntry( + hint.name, + hint.value.toDBusValue(), + ), + ), + ); + + // TODO(proninyaroslav): add actions + List _buildActions( + LinuxNotificationDetails? details, + LinuxInitializationSettings initSettings, + ) => + // Add default action, which is triggered when the notification is clicked + [ + _kDefaultActionName, + details?.defaultActionName ?? initSettings.defaultActionName, + ]; + + /// Cancel notification with the given [id]. + Future cancel(int id) async { + final LinuxNotificationInfo? notify = await _storage.getById(id); + await _storage.removeById(id); + if (notify != null) { + await _dbusCancel(notify.systemId); + } + } + + /// Cancel all notifications. + Future cancelAll() async { + final List notifyList = await _storage.getAll(); + final List idList = []; + for (final LinuxNotificationInfo notify in notifyList) { + idList.add(notify.id); + await _dbusCancel(notify.systemId); + } + await _storage.removeByIdList(idList); + } + + /// Returns the system notification server capabilities. + Future getCapabilities() async { + final DBusMethodSuccessResponse result = await _dbus.callMethod( + _DBusInterfaceSpec.destination, + _DBusMethodsSpec.getCapabilities, + [], + replySignature: DBusSignature('as'), + ); + final Set capsSet = (result.returnValues[0] as DBusArray) + .children + .map((DBusValue c) => (c as DBusString).value) + .toSet(); + + final LinuxServerCapabilities capabilities = LinuxServerCapabilities( + otherCapabilities: const {}, + body: capsSet.remove('body'), + bodyHyperlinks: capsSet.remove('body-hyperlinks'), + bodyImages: capsSet.remove('body-images'), + bodyMarkup: capsSet.remove('body-markup'), + iconMulti: capsSet.remove('icon-multi'), + iconStatic: capsSet.remove('icon-static'), + persistence: capsSet.remove('persistence'), + sound: capsSet.remove('sound'), + ); + return capabilities.copyWith(otherCapabilities: capsSet); + } + + /// Returns a [Map] with the specified notification id as the key + /// and the id, assigned by the system, as the value. + Future> getSystemIdMap() async => + Map.fromEntries(await _storage.getAll().then( + (List list) => list.map( + (LinuxNotificationInfo notify) => MapEntry( + notify.id, + notify.systemId, + ), + ), + )); + + Future _dbusCancel(int systemId) => _dbus.callMethod( + _DBusInterfaceSpec.destination, + _DBusMethodsSpec.closeNotification, + [DBusUint32(systemId)], + replySignature: DBusSignature(''), + ); + + String? _getAppIcon(LinuxNotificationIcon? icon) { + if (icon == null) { + return null; + } + switch (icon.type) { + case LinuxIconType.assets: + if (_platformData.assetsPath == null) { + return null; + } else { + final String relativePath = icon.content as String; + return path.join(_platformData.assetsPath!, relativePath); + } + case LinuxIconType.byteData: + return null; + case LinuxIconType.theme: + return icon.content as String; + } + } + + /// Subscribe to the signals for actions and closing notifications. + void _subscribeSignals() { + _dbus.subscribeSignal(_DBusMethodsSpec.actionInvoked).listen( + (DBusSignal s) async { + if (s.signature != DBusSignature('us')) { + return; + } + + final int systemId = (s.values[0] as DBusUint32).value; + final String actionKey = (s.values[1] as DBusString).value; + // TODO(proninyaroslav): add actions + if (actionKey == _kDefaultActionName) { + final LinuxNotificationInfo? notify = + await _storage.getBySystemId(systemId); + _onSelectNotification?.call(notify?.payload); + } + }, + ); + + _dbus.subscribeSignal(_DBusMethodsSpec.notificationClosed).listen( + (DBusSignal s) async { + if (s.signature != DBusSignature('uu')) { + return; + } + + final int systemId = (s.values[0] as DBusUint32).value; + await _storage.removeBySystemId(systemId); + }, + ); + } +} + +const String _kDefaultActionName = 'default'; + +class _DBusInterfaceSpec { + static const String destination = 'org.freedesktop.Notifications'; + static const String path = '/org/freedesktop/Notifications'; +} + +class _DBusMethodsSpec { + static const String notify = 'Notify'; + static const String closeNotification = 'CloseNotification'; + static const String actionInvoked = 'ActionInvoked'; + static const String notificationClosed = 'NotificationClosed'; + static const String getCapabilities = 'GetCapabilities'; +} diff --git a/flutter_local_notifications_linux/lib/src/platform_info.dart b/flutter_local_notifications_linux/lib/src/platform_info.dart new file mode 100644 index 000000000..08d6ecfb8 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/platform_info.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:xdg_directories/xdg_directories.dart' as xdg; + +import 'posix.dart'; + +/// Provides Linux platform-specific info +class LinuxPlatformInfo { + final Posix _posix = Posix(); + + /// Returns all platform-specific info + Future getAll() async { + try { + final String exePath = + await File('/proc/self/exe').resolveSymbolicLinks(); + final String processName = path.basenameWithoutExtension(exePath); + final String appPath = path.dirname(exePath); + final String assetPath = path.join(appPath, 'data', 'flutter_assets'); + final String versionPath = path.join(assetPath, 'version.json'); + final Map json = jsonDecode( + await File(versionPath).readAsString(), + ); + late final Directory runtimeDir; + if (xdg.runtimeDir == null) { + final int pid = _posix.getpid(); + final int userId = _posix.getuid(); + final int sessionId = _posix.getsid(pid); + final Map env = Platform.environment; + final String? tmpdir = env['TMPDIR']; + runtimeDir = Directory( + path.join( + tmpdir == null || tmpdir.isEmpty ? '/tmp' : tmpdir, + processName, + '$userId', + '$sessionId', + ), + ); + } else { + runtimeDir = Directory(path.join(xdg.runtimeDir!.path, processName)); + } + if (!runtimeDir.existsSync()) { + await runtimeDir.create(recursive: true); + } + + return LinuxPlatformInfoData( + appName: json['app_name'] ?? '', + assetsPath: assetPath, + runtimePath: runtimeDir.path, + ); + // ignore: avoid_catches_without_on_clauses + } catch (e) { + return const LinuxPlatformInfoData(); + } + } +} + +/// Represents Linux platform-specific info +class LinuxPlatformInfoData { + /// Constructs an instance of [LinuxPlatformInfoData]. + const LinuxPlatformInfoData({ + this.appName, + this.assetsPath, + this.runtimePath, + }); + + /// Application name + final String? appName; + + /// Path to the Flutter Assets directory + final String? assetsPath; + + /// The base directory relative to which user-specific runtime files and + /// other file objects should be placed + /// (Corresponds to `$XDG_RUNTIME_DIR` environment variable). + /// Please see XDG Base Directory Specification https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + /// If `$XDG_RUNTIME_DIR` is not set, the following directory structure is used: `/[$TMPDIR|tmp]/APP_NAME/USER_ID/SESSION_ID` + final String? runtimePath; +} diff --git a/flutter_local_notifications_linux/lib/src/posix.dart b/flutter_local_notifications_linux/lib/src/posix.dart new file mode 100644 index 000000000..f4cbddeff --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/posix.dart @@ -0,0 +1,27 @@ +import 'dart:ffi' as ffi; + +/// Represents Linux POSIX calls. +class Posix { + /// Constructs an instance of [Posix]. + Posix() { + final ffi.DynamicLibrary _dylib = ffi.DynamicLibrary.open('libc.so.6'); + getpid = _dylib + .lookup>('getpid') + .asFunction(); + getsid = _dylib + .lookup>('getsid') + .asFunction(); + getuid = _dylib + .lookup>('getuid') + .asFunction(); + } + + /// Get the process ID of the calling process. + late final int Function() getpid; + + /// Return the session ID of the given process. + late final int Function(int pid) getsid; + + /// Get the real user ID of the calling process. + late final int Function() getuid; +} diff --git a/flutter_local_notifications_linux/lib/src/storage.dart b/flutter_local_notifications_linux/lib/src/storage.dart new file mode 100644 index 000000000..3bba1ed72 --- /dev/null +++ b/flutter_local_notifications_linux/lib/src/storage.dart @@ -0,0 +1,178 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'file_system.dart'; +import 'notification_info.dart'; +import 'platform_info.dart'; + +const String _kFileName = 'notification_plugin_cache.json'; + +/// Represents a persisten storage for the notifications info, +/// see [LinuxNotificationInfo]. +/// The storage data exists within the user session. +class NotificationStorage { + /// Constructs an instance of of [NotificationStorageImpl]. + NotificationStorage({ + LinuxPlatformInfo? platformInfo, + FileSystem? fs, + }) : _platformInfo = platformInfo ?? LinuxPlatformInfo(), + _fs = fs ?? LocalFileSystem(); + + final LinuxPlatformInfo _platformInfo; + final FileSystem _fs; + + _Cache? _cachedInfo; + + /// Get all notifications. + Future> getAll() async { + final _Cache cache = await _readInfoMap(); + return cache.toImmutableMap().values.toList(); + } + + /// Get notification by [LinuxNotificationInfo.id]. + Future getBySystemId(int systemId) async { + final _Cache cache = await _readInfoMap(); + return cache.getBySystemId(systemId); + } + + /// Get notification by [LinuxNotificationInfo.systemId]. + Future getById(int id) async { + final _Cache cache = await _readInfoMap(); + return cache.getById(id); + } + + /// Insert notification to the storage. + /// Returns `true` if the operation succeeded. + Future insert(LinuxNotificationInfo notification) async { + final _Cache cache = await _readInfoMap(); + cache.insert(notification); + return _writeInfoList(cache.values.toList()); + } + + /// Remove notification from the storage by [LinuxNotificationInfo.id]. + /// Returns `true` if the operation succeeded. + Future removeById(int id) async { + final _Cache cache = await _readInfoMap(); + cache.removeById(id); + return _writeInfoList(cache.values.toList()); + } + + /// Remove notification from the storage by [LinuxNotificationInfo.systemId]. + /// Returns `true` if the operation succeeded. + Future removeBySystemId(int systemId) async { + final _Cache cache = await _readInfoMap(); + final LinuxNotificationInfo? info = cache.getBySystemId(systemId); + if (info != null) { + cache.removeById(info.id); + } + return _writeInfoList(cache.values.toList()); + } + + /// Remove notification from the storage by [idList]. + /// Returns `true` if the operation succeeded. + Future removeByIdList(List idList) async { + final _Cache cache = await _readInfoMap(); + // ignore: prefer_foreach + for (final int id in idList) { + cache.removeById(id); + } + return _writeInfoList(cache.values.toList()); + } + + /// Force read info from the disk to the cache. + Future forceReloadCache() async { + _cachedInfo = await _readFromCache(); + } + + Future _getStorageFile() async { + final LinuxPlatformInfoData data = await _platformInfo.getAll(); + final String? dir = data.runtimePath; + if (dir == null) { + return null; + } + return _fs.open(path.join(dir, _kFileName)); + } + + /// Gets a [LinuxNotificationInfo] from the stored file. + /// Once read, the data are maintained in memory. + Future<_Cache> _readInfoMap() async { + if (_cachedInfo != null) { + return _cachedInfo!; + } + return _cachedInfo = await _readFromCache(); + } + + Future<_Cache> _readFromCache() async { + final _Cache cache = _Cache(); + final File? storageFile = await _getStorageFile(); + if (storageFile != null && storageFile.existsSync()) { + final String jsonStr = storageFile.readAsStringSync(); + if (jsonStr.isNotEmpty) { + final dynamic json = jsonDecode(jsonStr); + if (json is List) { + for (final dynamic j in json) { + final LinuxNotificationInfo info = + LinuxNotificationInfo.fromJson(j); + cache.insert(info); + } + } else { + cache.insert(LinuxNotificationInfo.fromJson(json)); + } + } + } + return cache; + } + + /// Writes info list to disk. Returns [true] if the operation succeeded. + Future _writeInfoList(List infoList) async { + try { + final File? storageFile = await _getStorageFile(); + if (storageFile == null) { + return false; + } + if (!storageFile.existsSync()) { + storageFile.createSync(recursive: true); + } + final String jsonStr = jsonEncode(infoList); + storageFile.writeAsStringSync(jsonStr); + } on IOException catch (e) { + // ignore: avoid_print + print('Error saving preferences to disk: $e'); + return false; + } + return true; + } +} + +class _Cache { + _Cache() + : _infoMap = {}, + _systemIdMap = {}; + + final Map _infoMap; + + /// System ID to ID map. + final Map _systemIdMap; + + LinuxNotificationInfo? getById(int? id) => _infoMap[id]; + + LinuxNotificationInfo? getBySystemId(int? id) => _infoMap[_systemIdMap[id]]; + + void insert(LinuxNotificationInfo info) { + _infoMap[info.id] = info; + _systemIdMap[info.systemId] = info.id; + } + + void removeById(int id) { + final LinuxNotificationInfo? info = _infoMap.remove(id); + _systemIdMap.remove(info?.systemId); + } + + Iterable get values => _infoMap.values; + + Map toImmutableMap() => + UnmodifiableMapView(_infoMap); +} diff --git a/flutter_local_notifications_linux/pubspec.yaml b/flutter_local_notifications_linux/pubspec.yaml new file mode 100644 index 000000000..8b5ffd7d5 --- /dev/null +++ b/flutter_local_notifications_linux/pubspec.yaml @@ -0,0 +1,21 @@ +name: flutter_local_notifications_linux +description: Linux implementation of the flutter_local_notifications plugin +version: 0.3.0 +homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications + +dependencies: + flutter: + sdk: flutter + flutter_local_notifications_platform_interface: ^5.0.0 + dbus: ^0.5.0 + path: ^1.8.0 + xdg_directories: ^0.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.1.4 + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.2.0' diff --git a/flutter_local_notifications_linux/test/mock/fake_stream_subscription.dart b/flutter_local_notifications_linux/test/mock/fake_stream_subscription.dart new file mode 100644 index 000000000..5fe2621e9 --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/fake_stream_subscription.dart @@ -0,0 +1,5 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; + +class FakeStreamSubscription extends Fake implements StreamSubscription {} diff --git a/flutter_local_notifications_linux/test/mock/mock.dart b/flutter_local_notifications_linux/test/mock/mock.dart new file mode 100644 index 000000000..d563a75ff --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock.dart @@ -0,0 +1,7 @@ +export 'fake_stream_subscription.dart'; +export 'mock_dbus_wrapper.dart'; +export 'mock_file.dart'; +export 'mock_file_system.dart'; +export 'mock_platform_info.dart'; +export 'mock_select_notification_callback.dart'; +export 'mock_storage.dart'; diff --git a/flutter_local_notifications_linux/test/mock/mock_dbus_wrapper.dart b/flutter_local_notifications_linux/test/mock/mock_dbus_wrapper.dart new file mode 100644 index 000000000..e2ae2b96b --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock_dbus_wrapper.dart @@ -0,0 +1,10 @@ +import 'package:dbus/dbus.dart'; +import 'package:flutter_local_notifications_linux/src/dbus_wrapper.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockDBusWrapper extends Mock implements DBusWrapper {} + +class MockDBusRemoteObject extends Mock implements DBusRemoteObject {} + +class MockDBusRemoteObjectSignalStream extends Mock + implements DBusRemoteObjectSignalStream {} diff --git a/flutter_local_notifications_linux/test/mock/mock_file.dart b/flutter_local_notifications_linux/test/mock/mock_file.dart new file mode 100644 index 000000000..b1d47c46e --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock_file.dart @@ -0,0 +1,5 @@ +import 'dart:io'; + +import 'package:mocktail/mocktail.dart'; + +class MockFile extends Mock implements File {} diff --git a/flutter_local_notifications_linux/test/mock/mock_file_system.dart b/flutter_local_notifications_linux/test/mock/mock_file_system.dart new file mode 100644 index 000000000..a9b574408 --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock_file_system.dart @@ -0,0 +1,4 @@ +import 'package:flutter_local_notifications_linux/src/file_system.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockFileSystem extends Mock implements FileSystem {} diff --git a/flutter_local_notifications_linux/test/mock/mock_platform_info.dart b/flutter_local_notifications_linux/test/mock/mock_platform_info.dart new file mode 100644 index 000000000..6496aa6fc --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock_platform_info.dart @@ -0,0 +1,4 @@ +import 'package:flutter_local_notifications_linux/src/platform_info.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockLinuxPlatformInfo extends Mock implements LinuxPlatformInfo {} diff --git a/flutter_local_notifications_linux/test/mock/mock_select_notification_callback.dart b/flutter_local_notifications_linux/test/mock/mock_select_notification_callback.dart new file mode 100644 index 000000000..cdbde22b7 --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock_select_notification_callback.dart @@ -0,0 +1,9 @@ +import 'package:mocktail/mocktail.dart'; + +// ignore: one_member_abstracts +abstract class _SelectNotificationCallback { + Future call(String? payload); +} + +class MockSelectNotificationCallback extends Mock + implements _SelectNotificationCallback {} diff --git a/flutter_local_notifications_linux/test/mock/mock_storage.dart b/flutter_local_notifications_linux/test/mock/mock_storage.dart new file mode 100644 index 000000000..09cc53e9f --- /dev/null +++ b/flutter_local_notifications_linux/test/mock/mock_storage.dart @@ -0,0 +1,4 @@ +import 'package:flutter_local_notifications_linux/src/storage.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockNotificationStorage extends Mock implements NotificationStorage {} diff --git a/flutter_local_notifications_linux/test/notifications_manager_test.dart b/flutter_local_notifications_linux/test/notifications_manager_test.dart new file mode 100644 index 000000000..00f03f7a3 --- /dev/null +++ b/flutter_local_notifications_linux/test/notifications_manager_test.dart @@ -0,0 +1,1231 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:dbus/dbus.dart'; +import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; +import 'package:flutter_local_notifications_linux/src/dbus_wrapper.dart'; +import 'package:flutter_local_notifications_linux/src/model/hint.dart'; +import 'package:flutter_local_notifications_linux/src/notification_info.dart'; +import 'package:flutter_local_notifications_linux/src/notifications_manager.dart'; +import 'package:flutter_local_notifications_linux/src/platform_info.dart'; +import 'package:flutter_local_notifications_linux/src/storage.dart'; +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; + +import 'mock/mock.dart'; + +void main() { + group('Notifications manager |', () { + late LinuxNotificationManager manager; + late final DBusWrapper mockDbus; + late final DBusRemoteObjectSignalStream mockActionInvokedSignal; + late final DBusRemoteObjectSignalStream mockNotifyClosedSignal; + late final LinuxPlatformInfo mockPlatformInfo; + late final NotificationStorage mockStorage; + late final SelectNotificationCallback mockSelectNotificationCallback; + + const LinuxPlatformInfoData platformInfo = LinuxPlatformInfoData( + appName: 'Test', + assetsPath: 'assets', + runtimePath: 'run', + ); + + setUpAll(() { + mockDbus = MockDBusWrapper(); + mockActionInvokedSignal = MockDBusRemoteObjectSignalStream(); + mockNotifyClosedSignal = MockDBusRemoteObjectSignalStream(); + mockPlatformInfo = MockLinuxPlatformInfo(); + mockStorage = MockNotificationStorage(); + mockSelectNotificationCallback = MockSelectNotificationCallback(); + + when( + () => mockPlatformInfo.getAll(), + ).thenAnswer((_) async => platformInfo); + when( + () => mockStorage.forceReloadCache(), + ).thenAnswer((_) async => {}); + when( + () => mockDbus.build( + destination: 'org.freedesktop.Notifications', + path: '/org/freedesktop/Notifications', + ), + ).thenAnswer((_) => {}); + when( + () => mockDbus.subscribeSignal('ActionInvoked'), + ).thenAnswer((_) => mockActionInvokedSignal); + when( + () => mockDbus.subscribeSignal('NotificationClosed'), + ).thenAnswer((_) => mockNotifyClosedSignal); + when( + () => mockSelectNotificationCallback.call(any()), + ).thenAnswer((_) async => {}); + }); + + setUp(() { + manager = LinuxNotificationManager.private( + dbus: mockDbus, + platformInfo: mockPlatformInfo, + storage: mockStorage, + ); + + when( + () => mockActionInvokedSignal.listen(any()), + ).thenReturn(FakeStreamSubscription()); + when( + () => mockNotifyClosedSignal.listen(any()), + ).thenReturn(FakeStreamSubscription()); + }); + + void mockCloseMethod() => when( + () => mockDbus.callMethod( + 'org.freedesktop.Notifications', + 'CloseNotification', + any(), + replySignature: DBusSignature(''), + ), + ).thenAnswer( + (_) async => DBusMethodSuccessResponse(), + ); + + VerificationResult verifyCloseMethod(int systemId) => verify( + () => mockDbus.callMethod( + 'org.freedesktop.Notifications', + 'CloseNotification', + [DBusUint32(systemId)], + replySignature: DBusSignature(''), + ), + ); + + test('Initialize', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: 'test', + ); + + await manager.initialize(initSettings); + + verify(() => mockPlatformInfo.getAll()).called(1); + verify(() => mockStorage.forceReloadCache()).called(1); + verify( + () => mockDbus.build( + destination: 'org.freedesktop.Notifications', + path: '/org/freedesktop/Notifications', + ), + ).called(1); + verify(() => mockActionInvokedSignal.listen(any())).called(1); + verify(() => mockNotifyClosedSignal.listen(any())).called(1); + }); + + const String kDefaultActionName = 'Open notification'; + + group('Show |', () { + List buildNotifyMethodValues({ + int? replacesId, + String? appIcon, + String? title, + String? body, + List? actions, + Map? hints, + int? expireTimeout, + }) => + [ + // app_name + DBusString(platformInfo.appName!), + // replaces_id + DBusUint32(replacesId ?? 0), + // app_icon + DBusString(appIcon ?? ''), + // summary + DBusString(title ?? ''), + // body + DBusString(body ?? ''), + // actions + DBusArray.string( + ['default', kDefaultActionName, ...?actions]), + // hints + DBusDict.stringVariant(hints ?? {}), + // expire_timeout + DBusInt32( + expireTimeout ?? + const LinuxNotificationTimeout.systemDefault().value, + ), + ]; + + void mockNotifyMethod(int systemId) => when( + () => mockDbus.callMethod( + 'org.freedesktop.Notifications', + 'Notify', + any(), + replySignature: DBusSignature('u'), + ), + ).thenAnswer( + (_) async => DBusMethodSuccessResponse( + [DBusUint32(systemId)], + ), + ); + + VerificationResult verifyNotifyMethod(List values) => + verify(() => mockDbus.callMethod( + 'org.freedesktop.Notifications', + 'Notify', + values, + replySignature: DBusSignature('u'), + )); + + test('Simple notification', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + title: 'Title', + body: 'Body', + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, 'Title', 'Body'); + + verifyNotifyMethod(values).called(1); + verify( + () => mockStorage.insert(notify), + ).called(1); + }); + + test('Simple notification without title and body', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues(); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null); + + verifyNotifyMethod(values).called(1); + verify( + () => mockStorage.insert(notify), + ).called(1); + }); + + test('Replace previous notification', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo prevNotify = LinuxNotificationInfo( + id: 0, + systemId: 1, + payload: 'payload', + ); + const LinuxNotificationInfo newNotify = LinuxNotificationInfo( + id: 0, + systemId: 2, + payload: 'payload', + ); + + final List values = buildNotifyMethodValues( + replacesId: prevNotify.systemId, + title: 'Title', + body: 'Body', + ); + + mockNotifyMethod(newNotify.systemId); + when( + () => mockStorage.getById(newNotify.id), + ).thenAnswer((_) async => prevNotify); + when( + () => mockStorage.insert(newNotify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(newNotify.id, 'Title', 'Body'); + + verifyNotifyMethod(values).called(1); + verify( + () => mockStorage.insert(newNotify), + ).called(1); + }); + + test('Assets details icon', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultIcon: AssetsLinuxIcon('icon.png'), + ); + + final LinuxNotificationDetails details = LinuxNotificationDetails( + icon: AssetsLinuxIcon('details_icon.png'), + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + appIcon: path.join(platformInfo.assetsPath!, 'details_icon.png'), + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Byte details icon', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultIcon: AssetsLinuxIcon('icon.png'), + ); + + final ByteDataLinuxIcon icon = ByteDataLinuxIcon( + LinuxRawIconData( + data: Uint8List(64), + width: 8, + height: 8, + ), + ); + final LinuxNotificationDetails details = LinuxNotificationDetails( + icon: icon, + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'image-data': DBusStruct( + [ + DBusInt32(icon.iconData.width), + DBusInt32(icon.iconData.height), + DBusInt32(icon.iconData.rowStride), + DBusBoolean(icon.iconData.hasAlpha), + DBusInt32(icon.iconData.bitsPerSample), + DBusInt32(icon.iconData.channels), + DBusArray.byte(icon.iconData.data), + ], + ), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Theme details icon', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultIcon: AssetsLinuxIcon('icon.png'), + ); + + final LinuxNotificationDetails details = LinuxNotificationDetails( + icon: ThemeLinuxIcon('test'), + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + appIcon: details.icon!.content as String, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Default icon', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultIcon: AssetsLinuxIcon('icon.png'), + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + appIcon: path.join(platformInfo.assetsPath!, 'icon.png'), + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null); + + verifyNotifyMethod(values).called(1); + }); + + test('Timeout', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + const LinuxNotificationDetails details = LinuxNotificationDetails( + timeout: LinuxNotificationTimeout(100), + ); + + final List values = buildNotifyMethodValues( + expireTimeout: details.timeout.value, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Assets sound in details', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultSound: AssetsLinuxSound('default_sound.mp3'), + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final LinuxNotificationDetails details = LinuxNotificationDetails( + sound: AssetsLinuxSound('sound.mp3'), + ); + + final List values = buildNotifyMethodValues( + hints: { + 'sound-file': DBusString( + path.join( + platformInfo.assetsPath!, + details.sound!.content as String, + ), + ), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Theme sound in details', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultSound: AssetsLinuxSound('default_sound.mp3'), + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final LinuxNotificationDetails details = LinuxNotificationDetails( + sound: ThemeLinuxSound('test'), + ); + + final List values = buildNotifyMethodValues( + hints: { + 'sound-name': DBusString(details.sound!.content as String), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Default sound', () async { + final LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultSound: AssetsLinuxSound('sound.mp3'), + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'sound-file': DBusString( + path.join( + platformInfo.assetsPath!, + initSettings.defaultSound!.content as String, + ), + ), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null); + + verifyNotifyMethod(values).called(1); + }); + + test('Category', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final LinuxNotificationDetails details = LinuxNotificationDetails( + category: LinuxNotificationCategory.email(), + ); + + final List values = buildNotifyMethodValues( + hints: { + 'category': DBusString(details.category!.name), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Urgency', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + const LinuxNotificationDetails details = LinuxNotificationDetails( + urgency: LinuxNotificationUrgency.normal, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'urgency': DBusByte(details.urgency!.value), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Resident notification', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + const LinuxNotificationDetails details = LinuxNotificationDetails( + resident: true, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'resident': DBusBoolean(details.resident), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Suppress sound in details', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + const LinuxNotificationDetails details = LinuxNotificationDetails( + suppressSound: true, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'suppress-sound': DBusBoolean(details.suppressSound), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Default suppress sound', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultSuppressSound: true, + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'suppress-sound': DBusBoolean(initSettings.defaultSuppressSound), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null); + + verifyNotifyMethod(values).called(1); + }); + + test('Transient notification', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + const LinuxNotificationDetails details = LinuxNotificationDetails( + transient: true, + ); + + final List values = buildNotifyMethodValues( + hints: { + 'transient': DBusBoolean(details.transient), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Notification location', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + const LinuxNotificationDetails details = LinuxNotificationDetails( + location: LinuxNotificationLocation(50, 100), + ); + + final List values = buildNotifyMethodValues( + hints: { + 'x': DBusByte(details.location!.x), + 'y': DBusByte(details.location!.y), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + + test('Custom hints', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + final LinuxNotificationDetails details = LinuxNotificationDetails( + customHints: [ + LinuxNotificationCustomHint( + 'array-hint', + LinuxHintArrayValue( + [ + LinuxHintStringValue('1'), + LinuxHintStringValue('2'), + ], + ), + ), + LinuxNotificationCustomHint( + 'bool-hint', + LinuxHintBoolValue(true), + ), + LinuxNotificationCustomHint( + 'byte-hint', + LinuxHintByteValue(1), + ), + LinuxNotificationCustomHint( + 'dict-hint', + LinuxHintDictValue( + { + LinuxHintByteValue(1): LinuxHintStringValue('1'), + LinuxHintByteValue(2): LinuxHintStringValue('2'), + }, + ), + ), + LinuxNotificationCustomHint( + 'double-hint', + LinuxHintDoubleValue(1.1), + ), + LinuxNotificationCustomHint( + 'int16-hint', + LinuxHintInt16Value(1), + ), + LinuxNotificationCustomHint( + 'int32-hint', + LinuxHintInt32Value(1), + ), + LinuxNotificationCustomHint( + 'int64-hint', + LinuxHintInt64Value(1), + ), + LinuxNotificationCustomHint( + 'string-hint', + LinuxHintStringValue('test'), + ), + LinuxNotificationCustomHint( + 'struct-hint', + LinuxHintStructValue( + [ + LinuxHintStringValue('test'), + LinuxHintBoolValue(true), + ], + ), + ), + LinuxNotificationCustomHint( + 'uint16-hint', + LinuxHintUint16Value(1), + ), + LinuxNotificationCustomHint( + 'uint32-hint', + LinuxHintUint32Value(1), + ), + LinuxNotificationCustomHint( + 'uint64-hint', + LinuxHintUint64Value(1), + ), + LinuxNotificationCustomHint( + 'variant-hint', + LinuxHintVariantValue(LinuxHintByteValue(1)), + ), + ], + ); + + final List values = buildNotifyMethodValues( + hints: { + 'array-hint': DBusArray( + DBusSignature('s'), + [ + const DBusString('1'), + const DBusString('2'), + ], + ), + 'bool-hint': const DBusBoolean(true), + 'byte-hint': DBusByte(1), + 'dict-hint': DBusDict( + DBusSignature('y'), + DBusSignature('s'), + { + DBusByte(1): const DBusString('1'), + DBusByte(2): const DBusString('2'), + }, + ), + 'double-hint': const DBusDouble(1.1), + 'int16-hint': DBusInt16(1), + 'int32-hint': DBusInt32(1), + 'int64-hint': DBusInt64(1), + 'string-hint': const DBusString('test'), + 'struct-hint': DBusStruct( + [ + const DBusString('test'), + const DBusBoolean(true), + ], + ), + 'uint16-hint': DBusUint16(1), + 'uint32-hint': DBusUint32(1), + 'uint64-hint': const DBusUint64(1), + 'variant-hint': DBusVariant(DBusByte(1)), + }, + ); + + mockNotifyMethod(notify.systemId); + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async {}); + when( + () => mockStorage.insert(notify), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.show(notify.id, null, null, details: details); + + verifyNotifyMethod(values).called(1); + }); + }); + + test('Cancel', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + defaultSuppressSound: true, + ); + + const LinuxNotificationInfo notify = LinuxNotificationInfo( + id: 0, + systemId: 1, + ); + + mockCloseMethod(); + + when( + () => mockStorage.getById(notify.id), + ).thenAnswer((_) async => notify); + when( + () => mockStorage.removeById(notify.id), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.cancel(notify.id); + + verifyCloseMethod(notify.systemId).called(1); + verify( + () => mockStorage.removeById(notify.id), + ).called(1); + }); + + test('Cancel all', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + ); + + const List notifications = [ + LinuxNotificationInfo( + id: 0, + systemId: 1, + ), + LinuxNotificationInfo( + id: 1, + systemId: 2, + ), + ]; + + mockCloseMethod(); + + when( + () => mockStorage.getAll(), + ).thenAnswer((_) async => notifications); + when( + () => mockStorage.removeByIdList( + notifications.map((LinuxNotificationInfo n) => n.id).toList(), + ), + ).thenAnswer((_) async => true); + + await manager.initialize(initSettings); + await manager.cancelAll(); + + for (final LinuxNotificationInfo notify in notifications) { + verifyCloseMethod(notify.systemId).called(1); + } + verify( + () => mockStorage.removeByIdList( + notifications.map((LinuxNotificationInfo n) => n.id).toList(), + ), + ).called(1); + }); + + test('Notification closed by system', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + ); + + const List notifications = [ + LinuxNotificationInfo( + id: 0, + systemId: 1, + ), + LinuxNotificationInfo( + id: 1, + systemId: 2, + ), + ]; + + final List> completers = >[]; + for (final LinuxNotificationInfo notify in notifications) { + when( + () => mockStorage.removeBySystemId(notify.systemId), + ).thenAnswer((_) async => true); + } + + when( + () => mockNotifyClosedSignal.listen(any()), + ).thenAnswer((Invocation invocation) { + final Future Function(DBusSignal) callback = + invocation.positionalArguments.single; + for (final LinuxNotificationInfo notify in notifications) { + callback( + DBusSignal( + '', + DBusObjectPath('/org/freedesktop/Notifications'), + 'org.freedesktop.Notifications', + 'NotificationClosed', + [ + DBusUint32(notify.systemId), + DBusUint32(1), + ], + ), + ).then((_) { + for (final Completer completer in completers) { + if (!completer.isCompleted) { + completer.complete(); + } + } + }); + } + return FakeStreamSubscription(); + }); + + await manager.initialize(initSettings); + await Future.forEach( + completers, + (Completer completer) => completer.future, + ); + + for (final LinuxNotificationInfo notify in notifications) { + verify( + () => mockStorage.removeBySystemId(notify.systemId), + ).called(1); + } + }); + + test('Open notification', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + ); + + const List notifications = [ + LinuxNotificationInfo( + id: 0, + systemId: 1, + payload: 'payload1', + ), + LinuxNotificationInfo( + id: 1, + systemId: 2, + payload: 'payload2', + ), + ]; + + final List> completers = >[]; + for (final LinuxNotificationInfo notify in notifications) { + when( + () => mockStorage.getBySystemId(notify.systemId), + ).thenAnswer((_) async => notify); + completers.add(Completer()); + } + when( + () => mockActionInvokedSignal.listen(any()), + ).thenAnswer((Invocation invocation) { + final Future Function(DBusSignal) callback = + invocation.positionalArguments.single; + for (final LinuxNotificationInfo notify in notifications) { + callback( + DBusSignal( + '', + DBusObjectPath('/org/freedesktop/Notifications'), + 'org.freedesktop.Notifications', + 'ActionInvoked', + [ + DBusUint32(notify.systemId), + const DBusString('default'), + ], + ), + ).then((_) { + for (final Completer completer in completers) { + if (!completer.isCompleted) { + completer.complete(); + } + } + }); + } + return FakeStreamSubscription(); + }); + + await manager.initialize( + initSettings, + onSelectNotification: mockSelectNotificationCallback, + ); + await Future.forEach( + completers, + (Completer completer) => completer.future, + ); + + for (final LinuxNotificationInfo notify in notifications) { + verify( + () => mockStorage.getBySystemId(notify.systemId), + ).called(1); + verify( + () => mockSelectNotificationCallback.call(notify.payload), + ).called(1); + } + }); + + test('Notification server capabilities', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings(defaultActionName: kDefaultActionName); + + when( + () => mockDbus.callMethod( + 'org.freedesktop.Notifications', + 'GetCapabilities', + [], + replySignature: DBusSignature('as'), + ), + ).thenAnswer( + (_) async => DBusMethodSuccessResponse( + [ + DBusArray( + DBusSignature('s'), + [ + const DBusString('body'), + const DBusString('body-hyperlinks'), + const DBusString('body-images'), + const DBusString('body-markup'), + const DBusString('icon-multi'), + const DBusString('icon-static'), + const DBusString('persistence'), + const DBusString('sound'), + const DBusString('test-cap'), + ], + ), + ], + ), + ); + + await manager.initialize(initSettings); + expect( + await manager.getCapabilities(), + const LinuxServerCapabilities( + otherCapabilities: {'test-cap'}, + body: true, + bodyHyperlinks: true, + bodyImages: true, + bodyMarkup: true, + iconMulti: true, + iconStatic: true, + persistence: true, + sound: true, + ), + ); + }); + + test('Get system ID map', () async { + const LinuxInitializationSettings initSettings = + LinuxInitializationSettings( + defaultActionName: kDefaultActionName, + ); + + const List notifications = [ + LinuxNotificationInfo( + id: 0, + systemId: 1, + ), + LinuxNotificationInfo( + id: 1, + systemId: 2, + ), + ]; + + when( + () => mockStorage.getAll(), + ).thenAnswer((_) async => notifications); + + await manager.initialize(initSettings); + expect( + await manager.getSystemIdMap(), + Map.fromEntries( + notifications.map( + (LinuxNotificationInfo notify) => MapEntry( + notify.id, + notify.systemId, + ), + ), + ), + ); + }); + }); +} diff --git a/flutter_local_notifications_linux/test/posix_test.dart b/flutter_local_notifications_linux/test/posix_test.dart new file mode 100644 index 000000000..e974ee84c --- /dev/null +++ b/flutter_local_notifications_linux/test/posix_test.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:flutter_local_notifications_linux/src/posix.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('POSIX |', () { + late Posix posix; + + setUpAll(() { + posix = Posix(); + }); + + test('getpid', () { + expect(posix.getpid(), equals(pid)); + }); + }); +} diff --git a/flutter_local_notifications_linux/test/storage_test.dart b/flutter_local_notifications_linux/test/storage_test.dart new file mode 100644 index 000000000..be4e1a808 --- /dev/null +++ b/flutter_local_notifications_linux/test/storage_test.dart @@ -0,0 +1,263 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_local_notifications_linux/src/file_system.dart'; +import 'package:flutter_local_notifications_linux/src/notification_info.dart'; +import 'package:flutter_local_notifications_linux/src/platform_info.dart'; +import 'package:flutter_local_notifications_linux/src/storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; + +import 'mock/mock.dart'; + +void main() { + group('Notification storage |', () { + late NotificationStorage storage; + late final LinuxPlatformInfo mockPlatformInfo; + late final FileSystem mockFs; + late final File mockStorageFile; + + const LinuxPlatformInfoData platformInfo = LinuxPlatformInfoData( + appName: 'Test', + assetsPath: 'assets', + runtimePath: 'run', + ); + + final String fileStoragePath = path.join( + platformInfo.runtimePath!, + 'notification_plugin_cache.json', + ); + + setUpAll(() { + mockPlatformInfo = MockLinuxPlatformInfo(); + mockFs = MockFileSystem(); + mockStorageFile = MockFile(); + + when( + () => mockPlatformInfo.getAll(), + ).thenAnswer((_) async => platformInfo); + when(() => mockFs.open(fileStoragePath)).thenReturn(mockStorageFile); + }); + + setUp(() { + storage = NotificationStorage( + platformInfo: mockPlatformInfo, + fs: mockFs, + ); + }); + + test('Insert', () async { + const List notifications = [ + LinuxNotificationInfo(id: 1, systemId: 1), + LinuxNotificationInfo( + id: 2, + systemId: 2, + payload: 'test', + ), + ]; + + when(() => mockStorageFile.existsSync()).thenReturn(false); + when( + () => mockStorageFile.createSync(recursive: true), + ).thenAnswer((_) => {}); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + when(() => mockStorageFile.readAsStringSync()).thenReturn(''); + + expect(await storage.insert(notifications[0]), isTrue); + expect(await storage.insert(notifications[1]), isTrue); + + verify( + () => mockStorageFile.createSync(recursive: true), + ).called(2); + verify( + () => mockStorageFile.writeAsStringSync( + jsonEncode([notifications[0]]), + ), + ).called(1); + verify( + () => mockStorageFile.writeAsStringSync(jsonEncode(notifications)), + ).called(1); + }); + + test('Remove', () async { + const List notifications = [ + LinuxNotificationInfo(id: 1, systemId: 1), + LinuxNotificationInfo( + id: 2, + systemId: 2, + payload: 'test', + ), + ]; + + when(() => mockStorageFile.existsSync()).thenReturn(true); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + when(() => mockStorageFile.readAsStringSync()).thenReturn(''); + + await storage.insert(notifications[0]); + await storage.insert(notifications[1]); + + expect(await storage.removeById(notifications[0].id), isTrue); + expect(await storage.removeById(notifications[1].id), isTrue); + + verify( + () => mockStorageFile.writeAsStringSync( + jsonEncode([notifications[1]]), + ), + ).called(1); + verify( + () => mockStorageFile.writeAsStringSync( + jsonEncode([]), + ), + ).called(1); + }); + + test('Get all', () async { + const List notifications = [ + LinuxNotificationInfo(id: 1, systemId: 1), + LinuxNotificationInfo( + id: 2, + systemId: 2, + payload: 'test', + ), + ]; + + when(() => mockStorageFile.existsSync()).thenReturn(true); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + + when(() => mockStorageFile.readAsStringSync()).thenReturn(''); + expect(await storage.getAll(), []); + + when( + () => mockStorageFile.readAsStringSync(), + ).thenReturn(jsonEncode([])); + expect(await storage.getAll(), []); + + when( + () => mockStorageFile.readAsStringSync(), + ).thenReturn(jsonEncode(notifications)); + await storage.insert(notifications[0]); + await storage.insert(notifications[1]); + + expect( + await storage.getAll(), + notifications, + ); + }); + + test('Get by ID', () async { + const LinuxNotificationInfo notification = LinuxNotificationInfo( + id: 1, + systemId: 1, + ); + + when(() => mockStorageFile.existsSync()).thenReturn(true); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + + when(() => mockStorageFile.readAsStringSync()).thenReturn(''); + expect(await storage.getAll(), []); + + when( + () => mockStorageFile.readAsStringSync(), + ).thenReturn(jsonEncode([])); + expect(await storage.getAll(), []); + + when( + () => mockStorageFile.readAsStringSync(), + ).thenReturn(jsonEncode(notification)); + await storage.insert(notification); + + expect(await storage.getById(2), isNull); + expect(await storage.getById(notification.id), notification); + }); + + test('Get by system ID', () async { + const LinuxNotificationInfo notification = LinuxNotificationInfo( + id: 1, + systemId: 2, + ); + + when(() => mockStorageFile.existsSync()).thenReturn(true); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + + when( + () => mockStorageFile.readAsStringSync(), + ).thenReturn(jsonEncode(notification)); + await storage.insert(notification); + + expect(await storage.getBySystemId(notification.systemId), notification); + }); + + test('Get all, file does not exist', () async { + when(() => mockStorageFile.existsSync()).thenReturn(false); + expect(await storage.getAll(), []); + }); + + test('Remove by ID list', () async { + const List notifications = [ + LinuxNotificationInfo(id: 1, systemId: 1), + LinuxNotificationInfo( + id: 2, + systemId: 2, + payload: 'test', + ), + ]; + + when(() => mockStorageFile.existsSync()).thenReturn(true); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + when(() => mockStorageFile.readAsStringSync()).thenReturn(''); + + await storage.insert(notifications[0]); + await storage.insert(notifications[1]); + + expect( + await storage.removeByIdList( + notifications.map((LinuxNotificationInfo n) => n.id).toList(), + ), + isTrue, + ); + expect(await storage.getAll(), []); + + verify( + () => mockStorageFile.writeAsStringSync( + jsonEncode([]), + ), + ).called(1); + }); + + test('Remove by system ID', () async { + const LinuxNotificationInfo notification = LinuxNotificationInfo( + id: 1, + systemId: 2, + ); + + when(() => mockStorageFile.existsSync()).thenReturn(true); + when( + () => mockStorageFile.writeAsStringSync(any()), + ).thenAnswer((_) => {}); + when(() => mockStorageFile.readAsStringSync()).thenReturn(''); + + await storage.insert(notification); + + expect(await storage.removeBySystemId(notification.systemId), isTrue); + + verify( + () => mockStorageFile.writeAsStringSync( + jsonEncode([]), + ), + ).called(1); + }); + }); +} diff --git a/flutter_local_notifications_platform_interface/CHANGELOG.md b/flutter_local_notifications_platform_interface/CHANGELOG.md index 0ec565b6c..d6d6936c6 100644 --- a/flutter_local_notifications_platform_interface/CHANGELOG.md +++ b/flutter_local_notifications_platform_interface/CHANGELOG.md @@ -1,3 +1,21 @@ +## [5.0.0] + +* **Breaking change** the `SelectNotificationCallback` typedef now maps to a function that returns `void` instead of a `Future`. This change was done to better communicate the plugin doesn't actually await any asynchronous computation and is similar to how button pressed callbacks work for Flutter where they are typically use [`VoidCallback`](https://api.flutter.dev/flutter/dart-ui/VoidCallback.html) + +## [4.0.1] + +* Moved the `SelectNotificationCallback` typedef and `validateId` method previously defined in the plugin to the platform interface. This is so they could be reused by platform implementations + +## [4.0.0] + +* Updated Flutter SDK constraint +* Updated Dart SDK constraint +* Bumped mockito dependency + +## [3.0.0] + +* Migrated to null safety + ## [2.0.0+1] * Added more API docs diff --git a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart index 4e76483ef..6935062ee 100644 --- a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart +++ b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart @@ -3,7 +3,9 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'src/notification_app_launch_details.dart'; import 'src/types.dart'; +export 'src/helpers.dart'; export 'src/notification_app_launch_details.dart'; +export 'src/typedefs.dart'; export 'src/types.dart'; /// The interface that all implementations of flutter_local_notifications must @@ -12,7 +14,7 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface { /// Constructs an instance of [FlutterLocalNotificationsPlatform]. FlutterLocalNotificationsPlatform() : super(token: _token); - static FlutterLocalNotificationsPlatform _instance; + static late FlutterLocalNotificationsPlatform _instance; static final Object _token = Object(); @@ -28,14 +30,16 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface { } /// Returns info on if a notification had been used to launch the application. - Future getNotificationAppLaunchDetails() async { + Future + getNotificationAppLaunchDetails() async { throw UnimplementedError( 'getNotificationAppLaunchDetails() has not been implemented'); } /// Show a notification with an optional payload that will be passed back to /// the app when a notification is tapped on. - Future show(int id, String title, String body, {String payload}) async { + Future show(int id, String? title, String? body, + {String? payload}) async { throw UnimplementedError('show() has not been implemented'); } @@ -44,7 +48,7 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface { /// notification will be an hour after the method has been called and then /// every hour after that. Future periodicallyShow( - int id, String title, String body, RepeatInterval repeatInterval) { + int id, String? title, String? body, RepeatInterval repeatInterval) { throw UnimplementedError('periodicallyShow() has not been implemented'); } diff --git a/flutter_local_notifications_platform_interface/lib/src/helpers.dart b/flutter_local_notifications_platform_interface/lib/src/helpers.dart new file mode 100644 index 000000000..d30df5864 --- /dev/null +++ b/flutter_local_notifications_platform_interface/lib/src/helpers.dart @@ -0,0 +1,9 @@ +/// Helper method for validating notification IDs. +/// Ensures IDs are valid 32-bit integers. +void validateId(int id) { + ArgumentError.checkNotNull(id, 'id'); + if (id > 0x7FFFFFFF || id < -0x80000000) { + throw ArgumentError.value(id, 'id', + 'must fit within the size of a 32-bit integer i.e. in the range [-2^31, 2^31 - 1]'); // ignore: lines_longer_than_80_chars + } +} diff --git a/flutter_local_notifications_platform_interface/lib/src/notification_app_launch_details.dart b/flutter_local_notifications_platform_interface/lib/src/notification_app_launch_details.dart index bcdf482f6..0079e2fa1 100644 --- a/flutter_local_notifications_platform_interface/lib/src/notification_app_launch_details.dart +++ b/flutter_local_notifications_platform_interface/lib/src/notification_app_launch_details.dart @@ -8,5 +8,5 @@ class NotificationAppLaunchDetails { final bool didNotificationLaunchApp; /// The payload of the notification that launched the app - final String payload; + final String? payload; } diff --git a/flutter_local_notifications_platform_interface/lib/src/typedefs.dart b/flutter_local_notifications_platform_interface/lib/src/typedefs.dart new file mode 100644 index 000000000..f35447a13 --- /dev/null +++ b/flutter_local_notifications_platform_interface/lib/src/typedefs.dart @@ -0,0 +1,7 @@ +/// Signature of callback passed to [initialize] that is triggered when user +/// taps on a notification. +typedef SelectNotificationCallback = void Function(String? payload); + +/// Callback function when a notification is received. +typedef NotificationActionCallback = void Function( + String id, String? input, String? payload); diff --git a/flutter_local_notifications_platform_interface/lib/src/types.dart b/flutter_local_notifications_platform_interface/lib/src/types.dart index 73cf10219..a4018dc7f 100644 --- a/flutter_local_notifications_platform_interface/lib/src/types.dart +++ b/flutter_local_notifications_platform_interface/lib/src/types.dart @@ -23,11 +23,11 @@ class PendingNotificationRequest { final int id; /// The notification's title. - final String title; + final String? title; /// The notification's body. - final String body; + final String? body; /// The notification's payload. - final String payload; + final String? payload; } diff --git a/flutter_local_notifications_platform_interface/pubspec.yaml b/flutter_local_notifications_platform_interface/pubspec.yaml index dccad6f06..69b6d89ca 100644 --- a/flutter_local_notifications_platform_interface/pubspec.yaml +++ b/flutter_local_notifications_platform_interface/pubspec.yaml @@ -1,18 +1,18 @@ name: flutter_local_notifications_platform_interface description: A common platform interface for the flutter_local_notifications plugin. -version: 2.0.0+1 +version: 5.0.0 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0" + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.2.0' dependencies: flutter: sdk: flutter - plugin_platform_interface: ^1.0.2 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 \ No newline at end of file + mockito: ^5.0.8 diff --git a/flutter_local_notifications_platform_interface/test/flutter_local_notifications_platform_interface_test.dart b/flutter_local_notifications_platform_interface/test/flutter_local_notifications_platform_interface_test.dart index 80a55ea46..8dd31ce63 100644 --- a/flutter_local_notifications_platform_interface/test/flutter_local_notifications_platform_interface_test.dart +++ b/flutter_local_notifications_platform_interface/test/flutter_local_notifications_platform_interface_test.dart @@ -21,7 +21,7 @@ void main() { expect(() { FlutterLocalNotificationsPlatform.instance = ImplementsFlutterLocalNotificationsPlatform(); - }, throwsA(isInstanceOf())); + }, throwsAssertionError); }); test('Can be mocked with `implements`', () { diff --git a/images/gnome_linux_notification.png b/images/gnome_linux_notification.png new file mode 100644 index 000000000..046812d8c Binary files /dev/null and b/images/gnome_linux_notification.png differ diff --git a/images/kde_linux_notification.png b/images/kde_linux_notification.png new file mode 100644 index 000000000..037850bd1 Binary files /dev/null and b/images/kde_linux_notification.png differ