diff --git a/analysis_options.yaml b/analysis_options.yaml index 343acd642..955eee9f9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,7 +10,6 @@ linter: - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses - avoid_catching_errors - - avoid_classes_with_only_static_members - avoid_double_and_int_checks - avoid_empty_else - avoid_equals_and_hash_code_on_mutable_classes diff --git a/flutter_local_notifications/CHANGELOG.md b/flutter_local_notifications/CHANGELOG.md index 160e1c52d..7e36a6c23 100644 --- a/flutter_local_notifications/CHANGELOG.md +++ b/flutter_local_notifications/CHANGELOG.md @@ -1,3 +1,11 @@ +## [19.0.0-dev.4] + +* [Windows] **Breaking change** Reworked the APIs around custom images and audio. Check the updated example for more details, but in short: + * Instead of `WindowsNotificationAudio.fromFile()`, use `WindowsNotificationAudio.asset()` + * Instead of `WindowsImage.file()`, use `WindowsImage()`. See the docs for what URIs are supported +* [Windows] Added `MsixUtils.hasPackageIdentity()` and `MsixUtils.assetUri()`. You shouldn't need to use `.assetUri()` directly, but it may be helpful to check `.hasPackageIdentity()` to know what features your application can support. +* [Windows] Added `FlutterLocalNotificationsWindows.isValidXml()` for testing raw XML. + ## [19.0.0-dev.3] * [iOS][macOS] **Breaking changes** the `DarwinNotificationActionOption` and `DarwinNotificationCategoryOption` are now enhanced enums with values accessible through the `value` property that are exactly the same as their native representations. Previously a bitwise left shift operation was applied to the index value @@ -13,7 +21,7 @@ * **Breaking change** bumped minimum Flutter SDK requirement to 3.19.0 and Dart SDK requirement to 3.3.0 * **Breaking change** [iOS] removed `uiLocalNotificationDateInterpretation` parameter from `zonedSchedule()` method. This was done as it actually no relevant as of the 18.0.0 that dropped support for iOS versions older than 10, which in turn meant that the deprecated `UILocalNotification` APIs from Apple were no longer used. The corresponding `UILocalNotificationDateInterpretation` enum has already been removed as well -* [Windows] Added support for Windows. Thanks to the PR from [Levi Lesches](https://github.com/Levi-Lesches/) that continued from the contributions from +* [Windows] Added support for Windows. Thanks to the PR from [Levi Lesches](https://github.com/Levi-Lesches/) that continued from the contributions from * Bumped `timezone` dependency so that minimum version is now 0.10.0 ## [18.0.1] @@ -167,7 +175,7 @@ # [15.1.0] * [iOS][macOS] added the ability to request provisional permissions. On iOS, this is only applicable to iOS 12 or newer. On macOS, this property is only applicable to macOS 10.14 or newer. Thanks to the PR from [Tokenyet](https://github.com/MaikuB/flutter_local_notifications/pull/2022) - + # [15.0.1] * [Android] fixed issue [2033](https://github.com/MaikuB/flutter_local_notifications/issues/2033) where notifications on scheduled using older version of the plugin would fail to have the next subsequent ones scheduled. This issue started occuring in 14.0 where support for inexact notifications was added using the `ScheduleMode` enum that was added and resulted in the deprecation of `androidAllowWhileIdle`. A mechanism was added to help "migrate" old notifications that had `androidAllowWhileIdle` specified but didn't account for how there are recurring notifications that were scheduled using older versions of the plugin prior to `androidAllowWhile` being added. This was also released part of the 14.1.2 hotfix release @@ -176,7 +184,7 @@ * **Breaking change** removed deprecated `schedule()`, `showDailyAtTime()` and `showWeeklyAtDayAndTime()` methods. Notifications that were scheduled prior to this release should still work * **Breaking change** removed `Time` class -* [Linux] **Breaking change** calling `zonedSchedule()` on Linux will now throw an `UnimplementedError` to align with how their is a Linux implementation but the method hasn't been implemented +* [Linux] **Breaking change** calling `zonedSchedule()` on Linux will now throw an `UnimplementedError` to align with how their is a Linux implementation but the method hasn't been implemented * [iOS][macOS] **Breaking change** added supported for banner and list presentation options for iOS and macOS that is applicable for iOS 14.0 or newer and macOS 11 or newer. This is a breaking change as the values default to true and the alert presentation option is no longer applicable on these OS versions as Apple has deprecated it to be replaced by the banner and list presentations. Please ensure that if you target these OS versions that you configure the options appropriately for your application. * [Android] updated tags used when writing error logs. For corrupt scheduled notifications and error is logged the tag is now `ScheduledNotifReceiver` instead of `ScheduledNotifReceiver`. When logging that exact alarm permissions have been revoked the the tag is now `FLTLocalNotifPlugin` instead of `notification` * Updated API documentation related to the iOS/macOS notification presentation options to include links to Apple's documentations to show what they correspond to @@ -272,10 +280,10 @@ ... } ``` - + # [12.0.4] -* Fixed issue [1796](https://github.com/MaikuB/flutter_local_notifications/issues/1796) where a `java.lang.ClassCastException` may be thrown on some Android devices when the `onDidReceiveBackgroundNotificationResponse` has been specified when calling `initialize()` +* Fixed issue [1796](https://github.com/MaikuB/flutter_local_notifications/issues/1796) where a `java.lang.ClassCastException` may be thrown on some Android devices when the `onDidReceiveBackgroundNotificationResponse` has been specified when calling `initialize()` # [12.0.3+1] @@ -299,7 +307,7 @@ # [12.0.1] -* [Android][iOS] fixed issue [1721](https://github.com/MaikuB/flutter_local_notifications/issues/1721) where a crash occurs upon tapping on a notification action fbut the `onDidReceiveBackgroundNotificationResponse` optional callback hasn't been specified. +* [Android][iOS] fixed issue [1721](https://github.com/MaikuB/flutter_local_notifications/issues/1721) where a crash occurs upon tapping on a notification action fbut the `onDidReceiveBackgroundNotificationResponse` optional callback hasn't been specified. * [iOS] suppressed deprecation warnings where plugin was Apple's old notification APIs to support older iOS devices # [12.0.0] @@ -333,7 +341,7 @@ * `GET_ACTIVE_NOTIFICATION_MESSAGING_STYLE_ERROR_CODE` -> `getActiveNotificationMessagingStyle` * `PERMISSION_REQUEST_IN_PROGRESS` -> `permissionRequestInProgress` * [Android] **Breaking change** the `category` of the `AndroidNotificationDetails` now requires an instance of the newly added `AndroidNotificationCategory` class instead of a string. This was to improve the discoverability of the APIs and improve the semantics as the category can specified in a similar fashion to using an enum value -* **Breaking change** callbacks have now been reworked. There are now the following callbacks and both will pass an instance of the `NotificationResponse` class +* **Breaking change** callbacks have now been reworked. There are now the following callbacks and both will pass an instance of the `NotificationResponse` class * `onDidReceiveNotificationResponse`: invoked only when the app is running. This works for when a user has selected a notification or notification action. This replaces the `onSelectNotification` callback that existed before. For notification actions, the action needs to be configured to indicate the the app or user interface should be shown on invoking the action for this callback to be invoked i.e. by specifying the `DarwinNotificationActionOption.foreground` option on iOS and the `showsUserInterface` property on Android. On macOS and Linux, as there's no support for background isolates it will always invoke this callback * `onDidReceiveBackgroundNotificationResponse`: invoked on a background isolate for when a user has selected a notification action. This replaces the `onSelectNotificationAction` callback * **Breaking change** the `NotificationAppLaunchDetails` has been updated to contain an instance `NotificationResponse` class with the `payload` belonging to the `NotificationResponse` class. This is to allow knowing more details about what caused the app to launch e.g. if a notification action was used to do so @@ -435,7 +443,7 @@ # [9.2.0] * [Android] Added `areNotificationsEnabled()` method to `AndroidFlutterLocalNotificationsPlugin`. This allows querying if notifications are enabled for the app calling the method. Thanks to the PR from [Konstantin Pelz](https://github.com/komape) -* [Linux] Fix `initialize()` returning null all the time instead of returning an appropriate boolean value to indicate if plugin has been initialised +* [Linux] Fix `initialize()` returning null all the time instead of returning an appropriate boolean value to indicate if plugin has been initialised # [9.1.5] @@ -836,7 +844,7 @@ Please note that there are a number of breaking changes in this release to impro * `BitmapSource.Drawable` -> `DrawableResourceAndroidBitmap` * `BitmapSource.FilePath` -> `FilePathAndroidBitmap` - Each of these subclasses has a constructor that an argument referring to the bitmap itself. For example, if you previously had the following code + Each of these subclasses has a constructor that an argument referring to the bitmap itself. For example, if you previously had the following code ```dart var androidPlatformChannelSpecifics = AndroidNotificationDetails( @@ -888,7 +896,7 @@ Please note that there are a number of breaking changes in this release to impro * The `DefaultStyleInformation` class now implements the `StyleInformation` class instead of extending it * Where possible, classes in the plugins have been updated to provide `const` constructors * Updates to API docs and readme -* Bump Android dependencies +* Bump Android dependencies * Fixed a grammar issue 0.9.1 changelog entry # [1.3.0] @@ -1164,7 +1172,7 @@ Please note that there are a number of breaking changes in this release to impro # [0.4.2] -* **Breaking change** Fix issue [127](https://github.com/MaikuB/flutter_local_notifications/issues/127) by changing plugin to Android Support Library version 27.1.1, compile and target SDK version to 27 due to issues Flutter has with API 28. +* **Breaking change** Fix issue [127](https://github.com/MaikuB/flutter_local_notifications/issues/127) by changing plugin to Android Support Library version 27.1.1, compile and target SDK version to 27 due to issues Flutter has with API 28. # [0.4.1+1] * Remove unused code in example app @@ -1174,7 +1182,7 @@ Please note that there are a number of breaking changes in this release to impro * **Breaking change** renamed the `selectNotification` callback exposed by the `initialize` function to `onSelectNotification` * **Breaking change** renamed the `MessageHandler` typedef to `SelectNotificationCallback` * **Breaking change** updated plugin to Android Support Library version 28.0, compile and target SDK version to 28 -* Address issue [115](https://github.com/MaikuB/flutter_local_notifications/issues/115) by adding validation to the notification ID values. This ensure they're within the range of a 32-bit integer as notification IDs on Android need to be within that range. Note that an `ArgumentError` is thrown when a value is out of range. +* Address issue [115](https://github.com/MaikuB/flutter_local_notifications/issues/115) by adding validation to the notification ID values. This ensure they're within the range of a 32-bit integer as notification IDs on Android need to be within that range. Note that an `ArgumentError` is thrown when a value is out of range. * Updated the Android Integration section around registering receivers via the Android manifest as per the suggestion in [116](https://github.com/MaikuB/flutter_local_notifications/issues/116) * Updated version of the http dependency for used by the example app diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index bdb1122f3..02e0c0380 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -218,9 +218,6 @@ class _HomePageState extends State { final TextEditingController _linuxIconPathController = TextEditingController(); - final TextEditingController _windowsRawXmlController = - TextEditingController(); - bool _notificationsEnabled = false; @override @@ -966,11 +963,7 @@ class _HomePageState extends State { }, ), ], - if (!kIsWeb && Platform.isWindows) - ...windows.examples( - xmlController: _windowsRawXmlController, - showXmlNotification: _showWindowsNotificationWithRawXml, - ), + if (!kIsWeb && Platform.isWindows) ...windows.examples(), ], ), ), @@ -1061,7 +1054,7 @@ class _HomePageState extends State { WindowsAction( content: 'Image', arguments: 'image', - image: File('icons/coworker.png').absolute, + imageUri: WindowsImage.getAssetUri('icons/coworker.png'), ), const WindowsAction( content: 'Context', @@ -1331,8 +1324,10 @@ class _HomePageState extends State { ); final WindowsNotificationDetails windowsNotificationDetails = WindowsNotificationDetails( - audio: WindowsNotificationAudio.preset( - sound: WindowsNotificationSound.alarm5), + audio: WindowsNotificationAudio.asset( + 'sound/slow_spring_board.mp3', + fallback: WindowsNotificationSound.alarm5, + ), ); final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, @@ -2700,16 +2695,6 @@ class _HomePageState extends State { platformChannelSpecifics, ); } - - Future? _showWindowsNotificationWithRawXml() => - flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - FlutterLocalNotificationsWindows>() - ?.showRawXml( - id: id++, - xml: _windowsRawXmlController.text, - bindings: {'message': 'Hello, World!'}, - ); } Future _showLinuxNotificationWithBodyMarkup() async { diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index c80a4e70d..025c8b186 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -14,15 +13,73 @@ const WindowsInitializationSettings initSettings = guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb', ); -List examples({ - required TextEditingController xmlController, - required VoidCallback showXmlNotification, -}) => - [ +class _WindowsXmlBuilder extends StatefulWidget { + @override + _WindowsXmlBuilderState createState() => _WindowsXmlBuilderState(); +} + +class _WindowsXmlBuilderState extends State<_WindowsXmlBuilder> { + final TextEditingController xmlController = TextEditingController(); + final Map bindings = { + 'message': 'Hello, World!' + }; + + final FlutterLocalNotificationsWindows? plugin = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + FlutterLocalNotificationsWindows>(); + + String get xml => xmlController.text; + + bool isValid = true; + + void onPressed() { + setState(() => isValid = plugin?.isValidXml(xml) ?? false); + if (isValid) { + plugin?.showRawXml(id: id++, xml: xml, bindings: bindings); + } + } + + @override + Widget build(BuildContext context) => SizedBox( + width: 500, + child: ExpansionTile( + title: const Text('Click to expand raw XML'), + children: [ + TextField( + maxLines: 20, + style: const TextStyle(fontFamily: 'RobotoMono'), + controller: xmlController, + onSubmitted: (_) => onPressed, + decoration: InputDecoration( + hintText: 'Enter the raw xml', + errorText: isValid ? null : 'Invalid XML', + helperText: 'Bindings: {message} --> Hello, World!', + constraints: + const BoxConstraints.tightFor(width: 600, height: 480), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => xmlController.clear(), + ), + ), + ), + const SizedBox(height: 8), + PaddedElevatedButton( + buttonText: 'Show notification with raw XML', + onPressed: onPressed, + ), + ]), + ); +} + +List examples() => [ const Text( 'Windows-specific examples', style: TextStyle(fontWeight: FontWeight.bold), ), + if (MsixUtils.hasPackageIdentity()) + const Text('Running as an MSIX, all features are available') + else + const Text('Running as an EXE, some features are not available'), PaddedElevatedButton( buttonText: 'Show short and long notifications notification', onPressed: () async { @@ -83,33 +140,7 @@ List examples({ await _showWindowsNotificationWithHeader(); }, ), - PaddedElevatedButton( - buttonText: 'Show notification with raw XML', - onPressed: showXmlNotification, - ), - const SizedBox(height: 8), - SizedBox( - width: 500, - child: ExpansionTile( - title: const Text('Click to expand raw XML'), - children: [ - TextField( - maxLines: 20, - style: const TextStyle(fontFamily: 'RobotoMono'), - controller: xmlController, - decoration: InputDecoration( - hintText: 'Enter the raw xml', - helperText: 'Bindings: {message} --> Hello, World!', - constraints: - const BoxConstraints.tightFor(width: 600, height: 480), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => xmlController.clear(), - ), - ), - ), - ]), - ), + _WindowsXmlBuilder(), ]; Future _showWindowsNotificationWithDuration() async { @@ -140,10 +171,11 @@ Future _showWindowsNotificationWithScenarios() async { null, const NotificationDetails( windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.alarm, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ]), + scenario: WindowsNotificationScenario.alarm, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ], + ), ), ); await flutterLocalNotificationsPlugin.show( @@ -152,10 +184,11 @@ Future _showWindowsNotificationWithScenarios() async { null, const NotificationDetails( windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.incomingCall, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ]), + scenario: WindowsNotificationScenario.incomingCall, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ], + ), ), ); await flutterLocalNotificationsPlugin.show( @@ -202,12 +235,12 @@ Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPlugin.show( id++, 'This notification has an image', - 'You can only show images from files', + 'You can show images from assets or the network. See the columns example as well.', NotificationDetails( windows: WindowsNotificationDetails( images: [ - WindowsImage.file( - File('./icons/4.0x/app_icon_density.png').absolute, + WindowsImage( + WindowsImage.getAssetUri('icons/4.0x/app_icon_density.png'), altText: 'A beautiful image', ), ], @@ -219,23 +252,28 @@ Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPlugin.show( id++, 'This notification has many groups', - 'Each group stays together', + 'Each group stays together. Web images only load in MSIX builds', NotificationDetails( windows: WindowsNotificationDetails( subtitle: 'Caption text is fainter', rows: [ WindowsRow([ WindowsColumn([ - WindowsImage.file(File('icons/coworker.png').absolute, - altText: 'A coworker'), + WindowsImage( + WindowsImage.getAssetUri('icons/coworker.png'), + altText: 'A local image', + ), const WindowsNotificationText( - text: 'A coworker', isCaption: true), + text: 'A local image', + isCaption: true, + ), ]), WindowsColumn([ - WindowsImage.file( - File('icons/4.0x/app_icon_density.png').absolute, - altText: 'The icon'), - const WindowsNotificationText(text: 'The icon'), + WindowsImage( + Uri.parse('https://picsum.photos/100'), + altText: 'A web image', + ), + const WindowsNotificationText(text: 'A web image'), ]), ]), ], diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index ef112b444..87ec44f7c 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -2,7 +2,7 @@ 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: 19.0.0-dev.3 +version: 19.0.0-dev.4 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications issue_tracker: https://github.com/MaikuB/flutter_local_notifications/issues @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter flutter_local_notifications_linux: ^5.0.1-dev.1 - flutter_local_notifications_windows: ^1.0.0-dev.2 + flutter_local_notifications_windows: ^1.0.0-dev.3 flutter_local_notifications_platform_interface: ^8.1.0-dev.1 timezone: ^0.10.0 diff --git a/flutter_local_notifications_windows/CHANGELOG.md b/flutter_local_notifications_windows/CHANGELOG.md index 3fa86b430..ff6a01855 100644 --- a/flutter_local_notifications_windows/CHANGELOG.md +++ b/flutter_local_notifications_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## [1.0.0-dev.3] + +* **Breaking change** Reworked the APIs around custom images and audio. Check the updated example for more details, but in short: + * Instead of `WindowsNotificationAudio.fromFile()`, use `WindowsNotificationAudio.asset()` + * Instead of `WindowsImage.file()`, use `WindowsImage()`. See the docs for what URIs are supported +* [Windows] Added `MsixUtils.hasPackageIdentity()` and `MsixUtils.assetUri()`. You shouldn't need to use `.assetUri()` directly, but it may be helpful to check `.hasPackageIdentity()` to know what features your application can support. +* [Windows] Added `FlutterLocalNotificationsWindows.isValidXml()` for testing raw XML. + ## [1.0.0-dev.2] * Fixed an issue that happens with MSIX app builds. Thanks to PR from [Levi Lesches](https://github.com/Levi-Lesches) diff --git a/flutter_local_notifications_windows/bin/crash.dart b/flutter_local_notifications_windows/bin/crash.dart deleted file mode 100644 index 7354403cd..000000000 --- a/flutter_local_notifications_windows/bin/crash.dart +++ /dev/null @@ -1,72 +0,0 @@ -// This file demonstrates how the plugin is _not_ thread safe. -// -// This crash can happen when running `dart test -j 1`, which would otherwise -// fix other concurrency issues with the tests. This crash is not significant -// for users as it depends on having two plugins instantiated at the same time, -// which is not recommended, but I left it here as a demonstration if needed. -// -// The experimental function `enableMultithreading()` can fix the issues -// demonstrated by this file, but when testing with `dart test -j 1`, a crash -// occurs as `XmlDocument doc;`, a seemingly harmless statement. I have not -// been able to deduce the cause, and `enableMultithreading()` does not fix it. -// If we can figure that out, tests can be run with `-j 1` and race conditions -// would be eliminated from the tests. - -// ignore_for_file: avoid_print - -import 'dart:isolate'; - -import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; -import 'package:timezone/standalone.dart'; - -const WindowsInitializationSettings settings = WindowsInitializationSettings( - appName: 'Test app', - appUserModelId: 'com.test.test', - guid: 'a8c22b55-049e-422f-b30f-863694de08c8', -); - -void main() async { - print('Starting tests'); - await Isolate.spawn(bindingsTest, null); - await Isolate.spawn(scheduledTest, null); - - // This is the critical line. Removing this causes crashes in the Windows SDK - // ignore: invalid_use_of_visible_for_testing_member - FlutterLocalNotificationsWindows().enableMultithreading(); - - await Future.delayed(const Duration(seconds: 5)); - print('Done. Scheduled and binding tests should have completed'); -} - -Future scheduledTest(_) async { - print('Starting scheduled test'); - await Future.delayed(const Duration(seconds: 4)); - final FlutterLocalNotificationsWindows plugin = - FlutterLocalNotificationsWindows(); - await plugin.initialize(settings); - await initializeTimeZone(); - final Location location = getLocation('US/Eastern'); - final TZDateTime now = TZDateTime.now(location); - final TZDateTime later = now.add(const Duration(days: 1)); - await plugin.zonedSchedule(300, null, null, later, null); - await plugin.zonedSchedule(301, null, null, later, null); - await plugin.zonedSchedule(302, null, null, later, null); - print('Scheduled test complete'); -} - -Future bindingsTest(_) async { - print('Starting bindings test'); - final Map bindings = { - 'title': 'Bindings title', - 'body': 'Bindings body' - }; - await Future.delayed(const Duration(seconds: 1)); - final FlutterLocalNotificationsWindows plugin = - FlutterLocalNotificationsWindows(); - await plugin.initialize(settings); - await plugin.show(503, '{title}', '{body}'); - await Future.delayed(const Duration(milliseconds: 100)); - await plugin.updateBindings(id: 503, bindings: bindings); - await plugin.updateBindings(id: 503, bindings: bindings); - print('Bindings test complete'); -} diff --git a/flutter_local_notifications_windows/dart_test.yaml b/flutter_local_notifications_windows/dart_test.yaml index 2305630e4..0675cf5ba 100644 --- a/flutter_local_notifications_windows/dart_test.yaml +++ b/flutter_local_notifications_windows/dart_test.yaml @@ -1,3 +1,2 @@ platforms: [vm] test_on: windows -retry: 5 # These tests have concurrency issues. See bin/crash.dart diff --git a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart index 06a875848..ed3b9c824 100644 --- a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart +++ b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart @@ -1,2 +1,3 @@ export 'src/details.dart'; +export 'src/msix/stub.dart' if (dart.library.ffi) 'src/msix/ffi.dart'; export 'src/plugin/stub.dart' if (dart.library.ffi) 'src/plugin/ffi.dart'; diff --git a/flutter_local_notifications_windows/lib/src/details/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart index 946785470..49ac5dc6c 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_action.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart @@ -1,4 +1,4 @@ -import 'dart:io'; +import 'notification_parts.dart'; // NOTE: All enum values in this file have Windows RT-specific names. // If you change their Dart names, be sure to override [Enum.name]. @@ -57,7 +57,7 @@ class WindowsAction { this.activationType = WindowsActivationType.foreground, this.activationBehavior = WindowsNotificationBehavior.dismiss, this.placement, - this.image, + this.imageUri, this.inputId, this.buttonStyle, this.tooltip, @@ -89,7 +89,9 @@ class WindowsAction { /// Images must be white with a transparent background, and should be /// 16x16 pixels with no padding. If you provide an image for one button, /// you should provide images for all your buttons. - final File? image; + /// + /// Check the docs for [WindowsImage] for an explanation of supported URIs. + final Uri? imageUri; /// The ID of an input box. /// diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart index cd2c2e8a9..4c5ff8474 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -1,7 +1,4 @@ -extension on Uri { - String get filename => pathSegments.last; - String get extension => pathSegments.last.split('.').last; -} +import '../../flutter_local_notifications_windows.dart'; /// A preset sound for a Windows notification. enum WindowsNotificationSound { @@ -101,41 +98,22 @@ class WindowsNotificationAudio { }) : isSilent = false, source = sound.name; - /// Audio from a file. See [allowedSchemes] and [allowedExtensions]. - WindowsNotificationAudio.fromFile({ - required Uri file, + /// Uses an audio file from a Flutter asset. + /// + /// Note that this will only work in release builds that have been packaged as + /// an MSIX installer. If you pass a [WindowsNotificationSound] for `fallback` + /// it will be used in debug and releases without MSIX. + /// + /// Windows supports the following formats: `.aac`, `.flac`, `.m4a`, `.mp3`, + /// `.wav`, and `.wma`. + WindowsNotificationAudio.asset( + String assetName, { this.shouldLoop = false, + WindowsNotificationSound fallback = WindowsNotificationSound.defaultSound, }) : isSilent = false, - source = file.toFilePath() { - if (!allowedSchemes.contains(file.scheme)) { - throw ArgumentError.value( - file.toString(), - 'WindowsNotificationAudio.file', - 'URI scheme must be one of the following schemes: $allowedSchemes', - ); - } - if (!file.filename.contains('.') || - !allowedExtensions.contains(file.extension)) { - throw ArgumentError.value( - file.toString(), - 'WindowsNotificationAudio.file', - 'File extension must be one of the following: $allowedExtensions', - ); - } - } - - /// Allowed Uri schemes for [WindowsNotificationAudio.fromFile]. - static const Set allowedSchemes = {'ms-appx', 'ms-resource'}; - - /// Allowed file extensions for [WindowsNotificationAudio.fromFile]. - static const Set allowedExtensions = { - 'aac', - 'flac', - 'm4a', - 'mp3', - 'wav', - 'wma' - }; + source = MsixUtils.hasPackageIdentity() + ? MsixUtils.getAssetUri(assetName).toString() + : fallback.name; /// Whether this audio should loop. final bool shouldLoop; diff --git a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart index 43dc29d69..54e079292 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart @@ -1,5 +1,8 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import '../../flutter_local_notifications_windows.dart'; + /// A text or image element in a Windows notification. /// /// Note: This should not be used for anything else as notification @@ -29,16 +32,59 @@ enum WindowsImageCrop { } /// An image in a Windows notification. +/// +/// Windows supports a few different URI types, and supports them differently +/// depending on if your app is packaged as an MSIX. Refer to the following: +/// +/// | URI | Debug | Release (EXE) | Release (MSIX) | +/// |--------|--------|--------|--------| +/// | `http(s)://` | ❌ | ❌ | ✅ | +/// | `ms-appx://` | ❌ | ❌ | ✅ | +/// | `file:///` | ✅ | ✅ | 🟨 | +/// | `getAssetUri()` | ✅ | ✅ | ✅ | +/// +/// Each URI type has different uses: +/// - For Flutter assets, use [getAssetUri], which return the correct file URI +/// for debug and release (exe) builds, and an `ms-appx` URI in MSIX builds. +/// - For images from the web, use an `https` or `http` URI, but note that +/// these only work in MSIX apps. If you need a network image without using +/// MSIX, consider downloading it directly and using a file URI after. Also +/// note that showing the notification will cause the image to be downloaded, +/// which could cause a small delay. Try to use small images. +/// - For images that come from the user's device, or have to be retrieved at +/// runtime, use a file URI, but as always, be aware of how paths might change +/// from your device to your users. Note that file URIs must be absolute +/// paths, not relative, which can be complicated if referring to MSIX assets. +/// - For images that are bundled with your app but not through Flutter, use +/// an `ms-appx` URI. class WindowsImage extends WindowsNotificationPart { - /// Creates a Windows notification image. - const WindowsImage.file( - this.file, { + /// Creates a Windows notification image from an image URI. + const WindowsImage( + this.uri, { required this.altText, this.addQueryParams = false, this.placement, this.crop, }); + /// Returns a URI for a [Flutter asset](https://docs.flutter.dev/ui/assets/assets-and-images#loading-images). + /// + /// - In debug mode, resolves to a file URI to the asset itself + /// - In non-MSIX release builds, resolves to a file URI to the bundled asset + /// - In MSIX releases, resolves to an `ms-appx` URI from [Msix.getAssetUri]. + static Uri getAssetUri(String assetName) { + if (kDebugMode) { + return Uri.file(File(assetName).absolute.path, windows: true); + } else if (MsixUtils.hasPackageIdentity()) { + return MsixUtils.getAssetUri(assetName); + } else { + return Uri.file( + File('data/flutter_assets/$assetName').absolute.path, + windows: true, + ); + } + } + /// Whether Windows should add URL query parameters when fetching the image. final bool addQueryParams; @@ -46,7 +92,7 @@ class WindowsImage extends WindowsNotificationPart { final String altText; /// The source of the image. - final File file; + final Uri uri; /// Where this image will be placed. Null indicates below the notification. final WindowsImagePlacement? placement; diff --git a/flutter_local_notifications_windows/lib/src/details/xml/action.dart b/flutter_local_notifications_windows/lib/src/details/xml/action.dart index 67a39d024..d31a3468a 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/action.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/action.dart @@ -7,13 +7,6 @@ extension ActionToXml on WindowsAction { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax void buildXml(XmlBuilder builder) { - if (image != null && !image!.isAbsolute) { - throw ArgumentError.value( - image!.path, - 'WindowsImage.file', - 'File path must be absolute', - ); - } builder.element( 'action', attributes: { @@ -22,9 +15,7 @@ extension ActionToXml on WindowsAction { 'activationType': activationType.name, 'afterActivationBehavior': activationBehavior.name, if (placement != null) 'placement': placement!.name, - if (image != null) - 'imageUri': - Uri.file(image!.absolute.path, windows: true).toFilePath(), + if (imageUri != null) 'imageUri': imageUri!.toString(), if (inputId != null) 'hint-inputId': inputId!, if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, if (tooltip != null) 'hint-toolTip': tooltip!, diff --git a/flutter_local_notifications_windows/lib/src/details/xml/image.dart b/flutter_local_notifications_windows/lib/src/details/xml/image.dart index e652f3db5..cbf869654 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/image.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/image.dart @@ -8,17 +8,10 @@ extension ImageToXml on WindowsImage { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image void buildXml(XmlBuilder builder) { - if (!file.isAbsolute) { - throw ArgumentError.value( - file.path, - 'WindowsImage.file', - 'File path must be absolute', - ); - } builder.element( 'image', attributes: { - 'src': Uri.file(file.absolute.path, windows: true).toFilePath(), + 'src': uri.toString(), 'alt': altText, 'addImageQuery': addQueryParams.toString(), if (placement != null) 'placement': placement!.name, diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart index 90c788061..475670480 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -28,6 +28,34 @@ class NotificationsPluginBindings { lookup) : _lookup = lookup; + /// Checks whether the current application has package identity. + /// + /// This impacts whether apps can query active notifications or cancel them. + /// For more details, see + /// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. + bool hasPackageIdentity() { + return _hasPackageIdentity(); + } + + late final _hasPackageIdentityPtr = + _lookup>('hasPackageIdentity'); + late final _hasPackageIdentity = + _hasPackageIdentityPtr.asFunction(); + + bool isValidXml( + ffi.Pointer xml, + ) { + return _isValidXml( + xml, + ); + } + + late final _isValidXmlPtr = + _lookup)>>( + 'isValidXml'); + late final _isValidXml = + _isValidXmlPtr.asFunction)>(); + /// Allocates a new plugin that must be released with [disposePlugin]. ffi.Pointer createPlugin() { return _createPlugin(); @@ -91,7 +119,8 @@ class NotificationsPluginBindings { ffi.Pointer, NativeNotificationCallback)>(); - /// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings. + /// Shows the XML as a notification with the given ID. See [updateNotification] for details on + /// bindings. bool showNotification( ffi.Pointer plugin, int id, @@ -269,18 +298,6 @@ class NotificationsPluginBindings { 'freeLaunchDetails'); late final _freeLaunchDetails = _freeLaunchDetailsPtr.asFunction(); - - /// EXPERIMENTAL: Enables multithreading for this application. - /// - /// NOTE: This is only to make tests more stable and is not intended to be used in applications. - void enableMultithreading() { - return _enableMultithreading(); - } - - late final _enableMultithreadingPtr = - _lookup>('enableMultithreading'); - late final _enableMultithreading = - _enableMultithreadingPtr.asFunction(); } final class NativePlugin extends ffi.Opaque {} diff --git a/flutter_local_notifications_windows/lib/src/msix/ffi.dart b/flutter_local_notifications_windows/lib/src/msix/ffi.dart new file mode 100644 index 000000000..a80eb122d --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/msix/ffi.dart @@ -0,0 +1,50 @@ +import 'dart:ffi'; +import 'dart:io'; + +import '../../flutter_local_notifications_windows.dart'; +import '../ffi/bindings.dart'; + +/// Helpful methods to support MSIX and package identity. +class MsixUtils { + /// Returns whether the current app was installed with an MSIX installer. + /// + /// Using an MSIX grants your application [package identity](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview), + /// which allows it to use [certain APIs](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/modernize-packaged-apps). + /// + /// Specifically, using an MSIX installer allows your app to: + /// - use [FlutterLocalNotificationsWindows.getActiveNotifications] + /// - use [FlutterLocalNotificationsWindows.cancel] + /// - use custom files for notification sounds + /// - use network sources for notifications + /// - use `ms-appx:///` URIs for resources + /// + /// These functions will simply do nothing or return empty data in apps + /// without package identity. Additionally: + /// - [WindowsImage.getAssetUri] will return a `file:///` or `ms-appx:///` URI, + /// depending on whether the app is running in debug, release, or as an MSIX. + /// - [WindowsNotificationAudio.asset] takes an audio file to use for apps + /// with package identity, and a preset fallbacks for apps without. + static bool hasPackageIdentity() { + final bool? cached = _hasPackageIdentity; + if (cached != null) { + return cached; + } else if (!Platform.isWindows) { + return false; + } else { + final DynamicLibrary lib = DynamicLibrary.open( + 'flutter_local_notifications_windows.dll', + ); + final NotificationsPluginBindings bindings = + NotificationsPluginBindings(lib); + final bool result = bindings.hasPackageIdentity(); + _hasPackageIdentity = result; + return result; + } + } + + static bool? _hasPackageIdentity; + + /// Returns an `ms-appx:///` URI from a [Flutter asset](https://docs.flutter.dev/ui/assets/assets-and-images). + static Uri getAssetUri(String path) => + Uri.parse('ms-appx:///data/flutter_assets/$path'); +} diff --git a/flutter_local_notifications_windows/lib/src/msix/stub.dart b/flutter_local_notifications_windows/lib/src/msix/stub.dart new file mode 100644 index 000000000..49f148a8e --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/msix/stub.dart @@ -0,0 +1,28 @@ +import '../../flutter_local_notifications_windows.dart'; + +/// Helpful methods to support MSIX and package identity. +class MsixUtils { + /// Returns whether the current app was installed with an MSIX installer. + /// + /// Using an MSIX grants your application [package identity](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview), + /// which allows it to use [certain APIs](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/modernize-packaged-apps). + /// + /// Specifically, using an MSIX installer allows your app to: + /// - use [FlutterLocalNotificationsWindows.getActiveNotifications] + /// - use [FlutterLocalNotificationsWindows.cancel] + /// - use custom files for notification sounds + /// - use network sources for notifications + /// - use `ms-appx:///` URIs for resources + /// + /// These functions will simply do nothing or return empty data in apps + /// without package identity. Additionally: + /// - [WindowsImage.getAssetUri] will return a `file:///` or `ms-appx:///` URI, + /// depending on whether the app is running in debug, release, or as an MSIX. + /// - [WindowsNotificationAudio.asset] takes an audio file to use for apps + /// with package identity, and a preset fallbacks for apps without. + static bool hasPackageIdentity() => false; // platforms without FFI + + /// Gets an `ms-appx:///` URI from a [Flutter asset](https://docs.flutter.dev/ui/assets/assets-and-images). + static Uri getAssetUri(String path) => + Uri.parse('ms-appx:///data/flutter_assets/$path'); +} diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index a86128aaf..96df12713 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -1,5 +1,4 @@ import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; -import 'package:meta/meta.dart'; import 'package:timezone/timezone.dart'; import '../details.dart'; @@ -22,8 +21,7 @@ abstract class WindowsNotificationsBase /// Shows a notification using raw XML passed to the Windows APIs. /// - /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. - /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). + /// To check if the XML is valid, use [isValidXml]. Future showRawXml({ required int id, required String xml, @@ -83,11 +81,9 @@ abstract class WindowsNotificationsBase required Map bindings, }); - /// EXPERIMENTAL: Enables multithreading + /// Checks if some XML is a valid Windows notification. /// - /// NOTE: This is only here to make tests more stable. This has not been - /// tested in an application as it conflicts with Flutter's preferred - /// configuration for Windows APIs. - @visibleForTesting - void enableMultithreading(); + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). + bool isValidXml(String xml); } diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 2a2967317..9e13258eb 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -1,6 +1,6 @@ import 'dart:ffi'; + import 'package:ffi/ffi.dart'; -import 'package:meta/meta.dart'; import '../details.dart'; import '../details/notification_to_xml.dart'; @@ -86,11 +86,10 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { final Pointer guid = settings.guid.toNativeUtf8(allocator: arena); final Pointer iconPath = settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; - final Pointer> - callback = + final NativeNotificationCallback callback = NativeCallable.listener( - _globalLaunchCallback) - .nativeFunction; + _globalLaunchCallback, + ).nativeFunction; final bool result = _bindings.init(_plugin, appName, aumId, guid, iconPath, callback); _isReady = result; @@ -280,6 +279,12 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } }); + @override + bool isValidXml(String xml) => using((Arena arena) { + final Pointer nativeXml = xml.toNativeUtf8(allocator: arena); + return _bindings.isValidXml(nativeXml); + }); + @override Future zonedSchedule( int id, @@ -361,8 +366,4 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { _plugin, id, bindings.toNativeMap(arena)); return getUpdateResult(result); }); - - @override - @visibleForTesting - void enableMultithreading() => _bindings.enableMultithreading(); } diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index 7aba9e34a..9fc1aeefb 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -1,5 +1,3 @@ -import 'package:meta/meta.dart'; - import '../details.dart'; import 'base.dart'; @@ -94,6 +92,5 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { NotificationUpdateResult.success; @override - @visibleForTesting - void enableMultithreading() {} + bool isValidXml(String xml) => false; } diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index c85e23de6..6352692f6 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_local_notifications_windows description: Windows implementation of the flutter_local_notifications plugin -version: 1.0.0-dev.2 +version: 1.0.0-dev.3 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_windows environment: @@ -8,6 +8,8 @@ environment: flutter: ">=3.19.0" dependencies: + flutter: + sdk: flutter ffi: ^2.1.2 flutter_local_notifications_platform_interface: ^8.1.0-dev.1 meta: ^1.11.0 diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index 33bd3b1e7..df4e4cb1f 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -8,6 +8,13 @@ using winrt::Windows::Data::Xml::Dom::XmlDocument; +bool hasPackageIdentity() { + if (!IsWindows8OrGreater()) return false; + uint32_t length = 0; + int error = GetCurrentPackageFullName(&length, nullptr); + return error != APPMODEL_ERROR_NO_PACKAGE; +} + NativePlugin* createPlugin() { return new NativePlugin(); } void disposePlugin(NativePlugin* plugin) { delete plugin; } @@ -20,9 +27,7 @@ bool init( if (iconPath != nullptr) icon = string(iconPath); const auto didRegister = plugin->registerApp(aumId, appName, guid, icon, callback); if (!didRegister) return false; - const auto identity = plugin->checkIdentity(); - if (!identity.has_value()) return false; - plugin->hasIdentity = identity.value(); + plugin->hasIdentity = hasPackageIdentity(); plugin->aumid = winrt::to_hstring(aumId); plugin->notifier = plugin->hasIdentity ? ToastNotificationManager::CreateToastNotifier() @@ -32,6 +37,16 @@ bool init( return true; } +bool isValidXml(char* xml) { + XmlDocument doc = XmlDocument(); + try { + doc.LoadXml(winrt::to_hstring(xml)); + return true; + } catch (winrt::hresult_error error) { + return false; + } +} + bool showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings) { if (!plugin->isReady) return false; XmlDocument doc; @@ -143,5 +158,3 @@ void freeLaunchDetails(NativeLaunchDetails details) { } if (details.data.entries != nullptr) delete[] details.data.entries; } - -void enableMultithreading() { CoInitializeEx(nullptr, COINIT_MULTITHREADED); } diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index 8cfbfa8b4..b2cfe4d7f 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -71,6 +71,15 @@ typedef enum NativeUpdateResult { notFound = 2, } NativeUpdateResult; +/// Checks whether the current application has package identity. +/// +/// This impacts whether apps can query active notifications or cancel them. +/// For more details, see +/// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. +FFI_PLUGIN_EXPORT bool hasPackageIdentity(); + +FFI_PLUGIN_EXPORT bool isValidXml(char* xml); + /// Allocates a new plugin that must be released with [disposePlugin]. FFI_PLUGIN_EXPORT NativePlugin* createPlugin(); @@ -127,11 +136,6 @@ FFI_PLUGIN_EXPORT void freeDetailsArray(NativeNotificationDetails* ptr); /// Releases the memory associated with a [NativeLaunchDetails]. FFI_PLUGIN_EXPORT void freeLaunchDetails(NativeLaunchDetails details); -/// EXPERIMENTAL: Enables multithreading for this application. -/// -/// NOTE: This is only to make tests more stable and is not intended to be used in applications. -FFI_PLUGIN_EXPORT void enableMultithreading(); - #ifdef __cplusplus } #endif diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index e1ed6bd7e..c96b17b28 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -176,18 +176,3 @@ bool NativePlugin::registerApp( UpdateRegistry(aumid, appName, guid, iconPath); return RegisterCallback(guid, callback); } - -std::optional NativePlugin::checkIdentity() { - if (!IsWindows8OrGreater()) return false; - uint32_t length = 0; - auto error = GetCurrentPackageFullName(&length, nullptr); - if (error == APPMODEL_ERROR_NO_PACKAGE) { - return false; - } else if (error != ERROR_INSUFFICIENT_BUFFER) { - return std::nullopt; - } - std::vector fullName(length); - error = GetCurrentPackageFullName(&length, fullName.data()); - if (error != ERROR_SUCCESS) return std::nullopt; - return true; -} diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp index fba730a50..97d53dbc3 100644 --- a/flutter_local_notifications_windows/src/plugin.hpp +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -22,9 +22,7 @@ struct NativePlugin { /// Whether the current application has package identity (ie, was packaged with an MSIX). /// - /// This impacts whether apps can query active notifications or cancel them. - /// For more details, see - /// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. + /// See [hasPackageIdentity]. bool hasIdentity = false; /// The app user model ID. Used instead of package identity when [hasIdentity] is false. @@ -44,11 +42,6 @@ struct NativePlugin { NativePlugin() {} ~NativePlugin() {} - /// Checks whether the current application has package identity. See [hasIdentity] for details. - /// - /// Returns true or false if the package has identity, or null if an error occurred. - std::optional checkIdentity(); - /// Registers the given [callback] to run when a notification is pressed. bool registerApp( const string& aumid, const string& appName, const string& guid, diff --git a/flutter_local_notifications_windows/src/utils.hpp b/flutter_local_notifications_windows/src/utils.hpp index fa4ca5780..0e0500359 100644 --- a/flutter_local_notifications_windows/src/utils.hpp +++ b/flutter_local_notifications_windows/src/utils.hpp @@ -4,6 +4,8 @@ #include #include // <-- This must be the first Windows header +#include +#include #include #include "ffi_api.h" diff --git a/flutter_local_notifications_windows/test/bindings_test.dart b/flutter_local_notifications_windows/test/bindings_test.dart index 506ad202e..0f1c921ab 100644 --- a/flutter_local_notifications_windows/test/bindings_test.dart +++ b/flutter_local_notifications_windows/test/bindings_test.dart @@ -13,7 +13,6 @@ const Map bindings = { }; void main() => group('Bindings', () { - FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); @@ -45,7 +44,7 @@ void main() => group('Bindings', () { ); }); - test('fail when notification has been cancelled', retry: 5, () async { + test('fail when notification has been cancelled', () async { await Future.delayed(const Duration(milliseconds: 200)); await plugin.show(503, '{title}', '{body}'); final NotificationUpdateResult result = diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index ebfc553d7..ea90159cd 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -1,6 +1,5 @@ -import 'dart:io'; - import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:flutter_local_notifications_windows/src/details/notification_to_xml.dart'; import 'package:test/test.dart'; const WindowsInitializationSettings settings = WindowsInitializationSettings( @@ -12,15 +11,20 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( extension PluginUtils on FlutterLocalNotificationsWindows { static int id = 15; - Future showDetails(WindowsNotificationDetails details) => - show(id++, 'Title', 'Body', details: details); - - void testDetails(WindowsNotificationDetails details) => - expect(showDetails(details), completes); + void testDetails(WindowsNotificationDetails details) => expect( + isValidXml( + notificationToXml( + title: 'title', + body: 'body', + payload: 'payload', + details: details, + ), + ), + isTrue, + ); } void main() => group('Details:', () { - FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); @@ -62,7 +66,7 @@ void main() => group('Details:', () { buttonStyle: WindowsButtonStyle.success, inputId: 'input-id', tooltip: 'tooltip', - image: File('test/icon.png').absolute, + imageUri: WindowsImage.getAssetUri('test/icon.png'), ); plugin ..testDetails(const WindowsNotificationDetails( @@ -72,11 +76,10 @@ void main() => group('Details:', () { ..testDetails(WindowsNotificationDetails( actions: List.filled(5, simpleAction))); expect( - plugin.showDetails( - WindowsNotificationDetails( - actions: List.filled(6, simpleAction), - ), - ), + () => notificationToXml( + details: WindowsNotificationDetails( + actions: List.filled(6, simpleAction), + )), throwsArgumentError, ); }); @@ -93,9 +96,10 @@ void main() => group('Details:', () { test('Rows', () { const WindowsColumn emptyColumn = WindowsColumn([]); - final WindowsImage image = WindowsImage.file( - File('test/icon.png').absolute, - altText: 'an icon'); + final WindowsImage image = WindowsImage( + WindowsImage.getAssetUri('test/icon.png'), + altText: 'an icon', + ); const WindowsNotificationText text = WindowsNotificationText(text: 'Text'); final WindowsColumn simpleColumn = @@ -131,12 +135,12 @@ void main() => group('Details:', () { }); test('Images', () async { - final WindowsImage simpleImage = WindowsImage.file( - File('test/icon.png').absolute, + final WindowsImage simpleImage = WindowsImage( + WindowsImage.getAssetUri('asset.png'), altText: 'an icon', ); - final WindowsImage complexImage = WindowsImage.file( - File('test/icon.png').absolute, + final WindowsImage complexImage = WindowsImage( + Uri.parse('https://picsum.photos/500'), altText: 'an icon', addQueryParams: true, crop: WindowsImageCrop.circle, @@ -190,8 +194,8 @@ void main() => group('Details:', () { inputs: [selection, textInput], actions: [action])); expect( - plugin.showDetails( - WindowsNotificationDetails( + () => notificationToXml( + details: WindowsNotificationDetails( inputs: List.filled(6, textInput), ), ), @@ -199,7 +203,7 @@ void main() => group('Details:', () { ); }); - test('Progress', retry: 5, () async { + test('Progress', () async { final WindowsProgressBar simple = WindowsProgressBar( id: 'simple', status: 'Testing...', @@ -244,5 +248,10 @@ void main() => group('Details:', () { expect(result, NotificationUpdateResult.success); await Future.delayed(const Duration(milliseconds: 10)); } + expect( + await plugin.updateProgressBar( + notificationId: 202, progressBar: dynamic), + NotificationUpdateResult.notFound, + ); }); }); diff --git a/flutter_local_notifications_windows/test/plugin_test.dart b/flutter_local_notifications_windows/test/plugin_test.dart index 06de35d27..7564ec322 100644 --- a/flutter_local_notifications_windows/test/plugin_test.dart +++ b/flutter_local_notifications_windows/test/plugin_test.dart @@ -6,15 +6,18 @@ import 'package:timezone/standalone.dart'; const WindowsInitializationSettings goodSettings = WindowsInitializationSettings( - appName: 'test', - appUserModelId: 'com.test.test', - guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); + appName: 'test', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8', +); + const WindowsInitializationSettings badSettings = WindowsInitializationSettings( - appName: 'test', appUserModelId: 'com.test.test', guid: '123'); + appName: 'test', + appUserModelId: 'com.test.test', + guid: '123', +); void main() => group('Plugin', () { - FlutterLocalNotificationsWindows().enableMultithreading(); - setUpAll(initializeTimeZones); test('initializes safely', () async { @@ -45,13 +48,18 @@ void main() => group('Plugin', () { expect(plugin.pendingNotificationRequests(), throwsStateError); expect(plugin.show(0, 'Title', 'Body'), throwsStateError); expect(plugin.showRawXml(id: 0, xml: ''), throwsStateError); - expect(plugin.updateBindings(id: 0, bindings: {}), - throwsStateError); expect( - plugin.updateProgressBar(progressBar: progress, notificationId: 0), - throwsStateError); + plugin.updateBindings(id: 0, bindings: {}), + throwsStateError, + ); expect( - plugin.zonedSchedule(0, null, null, now, null), throwsStateError); + plugin.updateProgressBar(progressBar: progress, notificationId: 0), + throwsStateError, + ); + expect( + plugin.zonedSchedule(0, null, null, now, null), + throwsStateError, + ); plugin.dispose(); }); @@ -70,11 +78,14 @@ void main() => group('Plugin', () { expect(plugin.pendingNotificationRequests(), throwsStateError); expect(plugin.show(0, 'Title', 'Body'), throwsStateError); expect(plugin.showRawXml(id: 0, xml: ''), throwsStateError); - expect(plugin.updateBindings(id: 0, bindings: {}), - throwsStateError); expect( - plugin.updateProgressBar(progressBar: progress, notificationId: 0), - throwsStateError); + plugin.updateBindings(id: 0, bindings: {}), + throwsStateError, + ); + expect( + plugin.updateProgressBar(progressBar: progress, notificationId: 0), + throwsStateError, + ); expect( plugin.zonedSchedule(0, null, null, now, null), throwsStateError); plugin.dispose(); @@ -85,11 +96,13 @@ void main() => group('Plugin', () { FlutterLocalNotificationsWindows(); await plugin.initialize(goodSettings); expect( - plugin.periodicallyShow(0, null, null, RepeatInterval.everyMinute), - throwsUnsupportedError); + plugin.periodicallyShow(0, null, null, RepeatInterval.everyMinute), + throwsUnsupportedError, + ); expect( - plugin.periodicallyShowWithDuration(0, null, null, Duration.zero), - throwsUnsupportedError); + plugin.periodicallyShowWithDuration(0, null, null, Duration.zero), + throwsUnsupportedError, + ); plugin.dispose(); }); }); diff --git a/flutter_local_notifications_windows/test/scheduled_test.dart b/flutter_local_notifications_windows/test/scheduled_test.dart index f810b8da3..08e2cbbf9 100644 --- a/flutter_local_notifications_windows/test/scheduled_test.dart +++ b/flutter_local_notifications_windows/test/scheduled_test.dart @@ -9,7 +9,6 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); void main() => group('Schedules', () { - FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(initializeTimeZones); @@ -23,19 +22,6 @@ void main() => group('Schedules', () { (await plugin.pendingNotificationRequests()).length; late final Location location = getLocation('US/Eastern'); - test('work with basic times', () async { - await plugin.cancelAll(); - expect(await countPending(), 0); - final TZDateTime now = TZDateTime.now(location); - final TZDateTime later = now.add(const Duration(days: 1)); - expect(plugin.zonedSchedule(300, null, null, later, null), completes); - expect(await countPending(), 1); - expect(plugin.zonedSchedule(301, null, null, later, null), completes); - expect(await countPending(), 2); - expect(plugin.zonedSchedule(302, null, null, later, null), completes); - expect(await countPending(), 3); - }); - test('do not work with earlier time', () async { final TZDateTime now = TZDateTime.now(location); final TZDateTime earlier = now.subtract(const Duration(days: 1)); diff --git a/flutter_local_notifications_windows/test/xml_test.dart b/flutter_local_notifications_windows/test/xml_test.dart index c9b100c58..e2998f190 100644 --- a/flutter_local_notifications_windows/test/xml_test.dart +++ b/flutter_local_notifications_windows/test/xml_test.dart @@ -2,9 +2,11 @@ import 'package:flutter_local_notifications_windows/flutter_local_notifications_ import 'package:test/test.dart'; const WindowsInitializationSettings settings = WindowsInitializationSettings( - appName: 'test', - appUserModelId: 'com.test.test', - guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); + appName: 'test', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8', +); + const String emptyXml = ''; const String invalidXml = 'Blah blah blah'; const String notWindowsXml = 'Hi'; @@ -56,25 +58,24 @@ const String complexXml = ''' '''; -void main() => group('XML', () { - FlutterLocalNotificationsWindows().enableMultithreading(); +void main() { + group('XML', () { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); - final FlutterLocalNotificationsWindows plugin = - FlutterLocalNotificationsWindows(); - setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { - await plugin.cancelAll(); - plugin.dispose(); - }); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); - test('catches invalid XML', () async { - expect(plugin.showRawXml(id: 0, xml: emptyXml), throwsArgumentError); - expect(plugin.showRawXml(id: 1, xml: invalidXml), throwsArgumentError); - expect( - plugin.showRawXml(id: 2, xml: notWindowsXml), throwsArgumentError); - expect( - plugin.showRawXml(id: 3, xml: unmatchedXml), throwsArgumentError); - expect(plugin.showRawXml(id: 4, xml: validXml), completes); - expect(plugin.showRawXml(id: 5, xml: complexXml), completes); - }); + test('catches invalid XML', () async { + expect(plugin.isValidXml(emptyXml), isFalse); + expect(plugin.isValidXml(invalidXml), isFalse); + expect(plugin.isValidXml(notWindowsXml), isFalse); + expect(plugin.isValidXml(unmatchedXml), isFalse); + expect(plugin.isValidXml(validXml), isTrue); + expect(plugin.isValidXml(complexXml), isTrue); }); + }); +} diff --git a/melos.yaml b/melos.yaml index c51fd59f0..69d137022 100644 --- a/melos.yaml +++ b/melos.yaml @@ -32,7 +32,7 @@ scripts: scope: "*example*" test:unit:windows: description: Runs Windows-specific unit tests - run: melos exec -c 1 -- "dart test" + run: melos exec -c 1 -- "flutter test -j 1" packageFilters: scope: '*_windows' test:integration: