diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 928c3d480..0f898ba52 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -283,31 +283,35 @@ jobs: .\build.bat - name: Run Tests run: melos run test:unit:windows - integration_tests_android: - name: Run integration tests (Android) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - - name: Install Tools - run: ./.github/workflows/scripts/install-tools.sh - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 29 - script: melos run test:integration + # integration_tests_android: + # name: Run integration tests (Android) + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-java@v4 + # with: + # distribution: 'zulu' + # java-version: '17' + # - uses: subosito/flutter-action@v2 + # with: + # channel: stable + # cache: true + # cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + # - name: Install Tools + # run: ./.github/workflows/scripts/install-tools.sh + # - name: Install Linux dependendencies + # run: | + # sudo apt-get update -y + # sudo apt-get install -y libgtk-3-dev + # - name: Enable KVM + # run: | + # echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + # sudo udevadm control --reload-rules + # sudo udevadm trigger --name-match=kvm + # - uses: reactivecircus/android-emulator-runner@v2 + # with: + # api-level: 29 + # script: melos run test:integration integration_tests_ios: name: Run integration tests (iOS) runs-on: macos-latest diff --git a/flutter_local_notifications/CHANGELOG.md b/flutter_local_notifications/CHANGELOG.md index fb4b8408f..0e587e7d8 100644 --- a/flutter_local_notifications/CHANGELOG.md +++ b/flutter_local_notifications/CHANGELOG.md @@ -1,5 +1,33 @@ -## [vNext] +## [19.4.1] +* [Android] fixed issue [#2675](https://github.com/MaikuB/flutter_local_notifications/issues/2675) where addition of `invisible` flag to notification actions could cause scheduled notifications with actions created prior to 19.4.0 to fail to show +* Updated the Android release build configuration section to point to the latest [location](https://developer.android.com/topic/performance/app-optimization/customize-which-resources-to-keep) of the official Android docs on how to configure which resources (e.g. notification icons) are kept so they are not discarded by the Android compiler. It has also been reworded to make it clearer that this applies to all Android resources + +## [19.4.0] + +* [Android] added ability to read `dataMimeType` and `dataUri` when calling `getActiveNotifications()` to read details of an active Android notification using the messaging style. Thanks to the PR from [Matt Bajorek](https://github.com/mattbajorek) +* [Android] added support for Android semantic actions. Thanks to the PR from [Jared Szechy](https://github.com/szechyjs) + +## [19.3.1] + +* [Windows] fixed issue [#2648](https://github.com/MaikuB/flutter_local_notifications/issues/2648) where non-ASCII characters in the notification payload were not being handled properly. Thanks to the PR from [yoyoIU](https://github.com/yoyo930021) +* [Windows] fixed issue [#2651](https://github.com/MaikuB/flutter_local_notifications/issues/2651) where unresolved symbols occurred with changes in introduced in newer Windows SDKs. Thanks to the PR from [Sebastien](https://github.com/Sebastien-VZN) + +## [19.3.0] + +* [Android][iOS][macOS] added `cancelAllPendingNotifications()` method for cancelling all pending notifications that have been scheduled. Thanks to the PR from [Kwon Tae Hyung](https://github.com/TaeBbong) + +## [19.2.1] + +* [macOS] removed redundant code that was only applicable on macOS versions lower than 10.14. This should be a non-functional change since 18.0.0 bumped the minimum Flutter SDK requirements that in turn required macOS 10.14 at a minimum. Thanks to the PR from [Blin Qipa](https://github.com/bqubique) +* [Android] bumped robolectric dependency. This fixes an issue where some users reported receiving instances of `java.lang.NoClassDefFoundError` around the plugin's Android unit tests. Thanks to the PR from [Turtlepaw](https://github.com/Turtlepaw) + +## [19.2.0] + +* [Android] added support to bypass have notifications bypass the device's Do Not Disturb (DnD) settings. Thanks the PR from [Michel v. Varendorff](https://github.com/mvarendorff2) that added the following changes + * The `hasNotificationPolicyAccess()` method that checks if the application can modify the notification policy + * The `requestNotificationPolicyAccess()` method that was added the `AndroidFlutterNotificationsPlugin` class. This can be used request access for the calling application modify the notification policy + * Added `bypassDnd` the property of the `AndroidNotificationChannel` class and `channelBypassDnd` to the `AndroidNotificationDetails` class. These can used to indicate if notifications associated with the channel can bypass the DnD settings of the device * Bumped `msix` dev dependency in example app. This to fix the [issue](https://github.com/YehudaKremer/msix/issues/303) where the `msix` package stopped being able to created MSIX installers ## [19.1.0] diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index 7f58d6727..b3eca84eb 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -386,6 +386,7 @@ For apps that need the following functionality please complete the following in android:stopWithTask="false" android:foregroundServiceType=""> ``` +* To be able to create channels that ignore the device's Do Not Disturb mode, specify the `` permission between the `` tags. Developers will also need to follow the instructions documented [here](#bypassing-do-not-disturb-dnd) Developers can refer to the example app's `AndroidManifest.xml` to help see what the end result may look like. Do note that the example app covers all the plugin's supported functionality so will request more permissions than your own app may need @@ -428,13 +429,21 @@ Note that when a full-screen intent notification actually occurs (as opposed to Developers should also be across Google's requirements on using full-screen intents. Please refer to their documentation [here](https://source.android.com/docs/core/permissions/fsi-limits) for more information. Should you app need request permissions, the `AndroidFlutterNotificationsPlugin` class exposes the `requestFullScreenIntentPermission()` method that can be used to do so. +### Bypassing Do Not Disturb (DnD) + +If your application needs the ability to send notifications that ignore the device's DnD mode settings, you must request these permissions from Android 6.0 onwards. You can do this using `AndroidFlutterNotificationsPlugin` by calling the `requestNotificationPolicyAccess()` method which will redirect the user to a settings page where your application may be explicitly whitelisted to bypass DnD. **This method _must_ be called before attempting to create a notification channel with `bypassDnd: true`.** Failing to do so will cause the `bypassDnd` argument to be treated as `false`. + +For notifications to then actually ignore the DnD-status of a device, the channel must be created with `bypassDnd: true`, or the first notification on a channel that creates it must be sent with `channelBypassDnd: true`. + +**NOTE:** This does _not_ ignore the device's silent mode! Should you have a use case where you must notify your users (for instance in an emergency), you might want to use a package like [`sound_mode`](https://pub.dev/packages/sound_mode) or write your own platform-specific code to set the `RingerMode` of the device as well as change the notification stream's volume before and after the notification. + ### Release build configuration -⚠️ Ensure that you have configured the resources that should be kept so that resources like your notification icons aren't discarded by the R8 compiler by following the instructions [here](https://developer.android.com/studio/build/shrink-code#keep-resources). If you have chosen to use `@mipmap/ic_launcher` as the notification icon (against the official Android guidance), be sure to include this in the `keep.xml` file. If you fail to do this, notifications might be broken. In the worst case they will never show, instead silently failing when the system looks for a resource that has been removed. If they do still show, you might not see the icon you specified. The configuration used by the example app can be found [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/res/raw/keep.xml) where it is specifying that all drawable resources should be kept, as well as the file used to play a custom notification sound (sound file is located [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/res/raw/slow_spring_board.mp3)). +⚠️ Ensure that you have configured the resources that should be kept so that resources like your notification icons aren't discarded by the R8 compiler by following the instructions [here](https://developer.android.com/topic/performance/app-optimization/customize-which-resources-to-keep). If you have chosen to use `@mipmap/ic_launcher` as the notification icon (against the official Android guidance), be sure to include this in the `keep.xml` file too. If you do not ensure resources like the notification icon kept, notifications might be broken. In the worst case they will never show, instead silently failing when the system looks for a resource that has been removed. If they do still show, you might not see the icon you specified. The configuration used by the example app can be found [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/res/raw/keep.xml) where it is specifying that all drawable resources should be kept, as well as the file used to play a custom notification sound (sound file is located [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/res/raw/slow_spring_board.mp3)). #### ProGuard rules -For flutter_local_notificaiton v19 and higher, the ProGuard rules are automatically provided by the GSON. The following documentation is for v18 and lower versions. +For flutter_local_notifications v19 and higher, the ProGuard rules are automatically provided by the GSON. The following documentation is for v18 and lower versions. Before creating the release build of your app (which is the default setting when building an APK or app bundle) you will need to customise your ProGuard configuration file as per this [link](https://developer.android.com/studio/build/shrink-code#keep-code). Rules specific to the GSON dependency being used by the plugin will need to be added. These rules can be found [here](https://github.com/google/gson/blob/main/examples/android-proguard-example/proguard.cfg). Whilst the example app has a Proguard rules (`proguard-rules.pro`) [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/proguard-rules.pro), it is recommended that developers refer to the rules on the GSON repository in case they get updated over time. @@ -593,7 +602,7 @@ Developers should also note that whilst accessing plugins will work, on Android **Specifying actions on notifications**: -The notification actions are platform specific and you have to specify them differently for each platform. +The notification actions are platform-specific and you have to specify them differently for each platform. On iOS/macOS, the actions are defined on a category, please see the configuration section for details. diff --git a/flutter_local_notifications/android/build.gradle b/flutter_local_notifications/android/build.gradle index 9ba085eec..5d4b5e697 100644 --- a/flutter_local_notifications/android/build.gradle +++ b/flutter_local_notifications/android/build.gradle @@ -50,5 +50,5 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:3.10.0' testImplementation 'androidx.test:core:1.2.0' - testImplementation "org.robolectric:robolectric:4.7.3" + testImplementation "org.robolectric:robolectric:4.14.1" } 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 bd3bbd9ea..1e0754955 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 @@ -7,6 +7,7 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlarmManager; +import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -26,6 +27,7 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; +import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.text.Html; import android.text.Spannable; @@ -156,9 +158,12 @@ public class FlutterLocalNotificationsPlugin 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 CANCEL_ALL_PENDING_NOTIFICATIONS_METHOD = + "cancelAllPendingNotifications"; private static final String ZONED_SCHEDULE_METHOD = "zonedSchedule"; private static final String PERIODICALLY_SHOW_METHOD = "periodicallyShow"; - private static final String PERIODICALLY_SHOW_WITH_DURATION = "periodicallyShowWithDuration"; + private static final String PERIODICALLY_SHOW_WITH_DURATION_METHOD = + "periodicallyShowWithDuration"; private static final String GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD = "getNotificationAppLaunchDetails"; private static final String REQUEST_NOTIFICATIONS_PERMISSION_METHOD = @@ -168,6 +173,9 @@ public class FlutterLocalNotificationsPlugin private static final String REQUEST_FULL_SCREEN_INTENT_PERMISSION_METHOD = "requestFullScreenIntentPermission"; + private static final String REQUEST_NOTIFICATION_POLICY_ACCESS_METHOD = + "requestNotificationPolicyAccess"; + private static final String HAS_NOTIFICATION_POLICY_ACCESS_METHOD = "hasNotificationPolicyAccess"; private static final String METHOD_CHANNEL = "dexterous.com/flutter/local_notifications"; private static final String INVALID_ICON_ERROR_CODE = "invalid_icon"; private static final String INVALID_LARGE_ICON_ERROR_CODE = "invalid_large_icon"; @@ -211,6 +219,7 @@ public class FlutterLocalNotificationsPlugin static final int EXACT_ALARM_PERMISSION_REQUEST_CODE = 2; static final int FULL_SCREEN_INTENT_PERMISSION_REQUEST_CODE = 3; + static final int NOTIFICATION_POLICY_ACCESS_REQUEST_CODE = 4; private PermissionRequestListener callback; @@ -260,7 +269,7 @@ protected static Notification createNotification( if (canCreateNotificationChannel(context, notificationChannelDetails)) { setupNotificationChannel(context, notificationChannelDetails); } - Intent intent = getLaunchIntent(context); + Intent intent = getLaunchIntent(context, notificationDetails); intent.setAction(SELECT_NOTIFICATION); intent.putExtra(NOTIFICATION_ID, notificationDetails.id); intent.putExtra(PAYLOAD, notificationDetails.payload); @@ -301,7 +310,7 @@ protected static Notification createNotification( Intent actionIntent; if (action.showsUserInterface != null && action.showsUserInterface) { - actionIntent = getLaunchIntent(context); + actionIntent = getLaunchIntent(context, notificationDetails); actionIntent.setAction(SELECT_FOREGROUND_NOTIFICATION_ACTION); } else { actionIntent = new Intent(context, ActionBroadcastReceiver.class); @@ -349,6 +358,10 @@ protected static Notification createNotification( actionBuilder.setAllowGeneratedReplies(action.allowGeneratedReplies); } + if (action.semanticAction != null) { + actionBuilder.setSemanticAction(action.semanticAction); + } + if (action.actionInputs != null) { for (NotificationActionInput input : action.actionInputs) { RemoteInput.Builder remoteInput = @@ -368,7 +381,11 @@ protected static Notification createNotification( actionBuilder.addRemoteInput(remoteInput.build()); } } - builder.addAction(actionBuilder.build()); + if (BooleanUtils.getValue(action.invisible)) { + builder.addInvisibleAction(actionBuilder.build()); + } else { + builder.addAction(actionBuilder.build()); + } } } @@ -984,7 +1001,14 @@ private static void setTimeoutAfter( builder.setTimeoutAfter(notificationDetails.timeoutAfter); } - private static Intent getLaunchIntent(Context context) { + private static Intent getLaunchIntent(Context context, NotificationDetails notificationDetails) { + if (isKeyguardLocked(context) && notificationDetails.startActivityClassName != null) { + try { + return new Intent(context, Class.forName(notificationDetails.startActivityClassName)); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } String packageName = context.getPackageName(); PackageManager packageManager = context.getPackageManager(); return packageManager.getLaunchIntentForPackage(packageName); @@ -1215,6 +1239,21 @@ private static void setupNotificationChannel( } else { notificationChannel.setSound(null, null); } + + if (BooleanUtils.getValue(notificationChannelDetails.bypassDnd)) { + boolean isAccessGranted = notificationManager.isNotificationPolicyAccessGranted(); + + if (isAccessGranted) { + notificationChannel.setBypassDnd(true); + } else { + Log.w( + TAG, + "Channel '" + + notificationChannelDetails.name + + "' was set to bypass Do Not Disturb but the OS prevents it."); + } + } + notificationChannel.enableVibration( BooleanUtils.getValue(notificationChannelDetails.enableVibration)); if (notificationChannelDetails.vibrationPattern != null @@ -1231,7 +1270,7 @@ private static void setupNotificationChannel( } } - private static Uri retrieveSoundResourceUri( + protected static Uri retrieveSoundResourceUri( Context context, String sound, SoundSource soundSource) { Uri uri = null; if (StringUtils.isNullOrEmpty(sound)) { @@ -1352,6 +1391,20 @@ private static String getNextFireDateMatchingDateTimeComponents( return null; } + static void startAlarmActivity(final Context context, NotificationDetails notificationDetails) { + Intent intent = getLaunchIntent(context, notificationDetails); + intent.setAction(SELECT_NOTIFICATION); + intent.putExtra(PAYLOAD, notificationDetails.payload); + intent.addFlags(Intent.FLAG_FROM_BACKGROUND); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + static boolean isKeyguardLocked(Context context) { + KeyguardManager myKM = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + return myKM.isKeyguardLocked(); + } + private static NotificationManagerCompat getNotificationManager(Context context) { return NotificationManagerCompat.from(context); } @@ -1475,10 +1528,27 @@ public void fail(String message) { } }); break; + case REQUEST_NOTIFICATION_POLICY_ACCESS_METHOD: + requestNotificationPolicyAccess( + new PermissionRequestListener() { + @Override + public void complete(boolean granted) { + result.success(granted); + } + + @Override + public void fail(String message) { + result.error(PERMISSION_REQUEST_IN_PROGRESS_ERROR_CODE, message, null); + } + }); + break; + case HAS_NOTIFICATION_POLICY_ACCESS_METHOD: + hasNotificationPolicyAccess(result); + break; case PERIODICALLY_SHOW_METHOD: repeat(call, result); break; - case PERIODICALLY_SHOW_WITH_DURATION: + case PERIODICALLY_SHOW_WITH_DURATION_METHOD: repeat(call, result); break; case CANCEL_METHOD: @@ -1487,6 +1557,9 @@ public void fail(String message) { case CANCEL_ALL_METHOD: cancelAllNotifications(result); break; + case CANCEL_ALL_PENDING_NOTIFICATIONS_METHOD: + cancelAllPendingNotifications(result); + break; case PENDING_NOTIFICATION_REQUESTS_METHOD: pendingNotificationRequests(result); break; @@ -1571,6 +1644,7 @@ private void getActiveNotifications(Result result) { activeNotificationPayload.put("body", notification.extras.getCharSequence("android.text")); activeNotificationPayload.put( "bigText", notification.extras.getCharSequence("android.bigText")); + activeNotificationsPayload.add(activeNotificationPayload); } result.success(activeNotificationsPayload); @@ -1802,6 +1876,28 @@ private void cancelAllNotifications(Result result) { result.success(null); } + private void cancelAllPendingNotifications(Result result) { + ArrayList scheduledNotifications = + loadScheduledNotifications(applicationContext); + + if (scheduledNotifications == null || scheduledNotifications.isEmpty()) { + result.success(null); + return; + } + + AlarmManager alarmManager = getAlarmManager(applicationContext); + Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); + + for (NotificationDetails scheduledNotification : scheduledNotifications) { + PendingIntent pendingIntent = + getBroadcastPendingIntent(applicationContext, scheduledNotification.id, intent); + alarmManager.cancel(pendingIntent); + } + + saveScheduledNotifications(applicationContext, new ArrayList<>()); + result.success(null); + } + public void requestNotificationsPermission(@NonNull PermissionRequestListener callback) { if (permissionRequestProgress != PermissionRequestProgress.None) { callback.fail(PERMISSION_REQUEST_IN_PROGRESS_ERROR_MESSAGE); @@ -1888,6 +1984,46 @@ public void requestFullScreenIntentPermission(@NonNull PermissionRequestListener } } + public void requestNotificationPolicyAccess(@NonNull PermissionRequestListener callback) { + if (VERSION.SDK_INT < VERSION_CODES.M) { + callback.complete(false); + return; + } + + if (permissionRequestProgress != PermissionRequestProgress.None) { + callback.fail(PERMISSION_REQUEST_IN_PROGRESS_ERROR_MESSAGE); + return; + } + + this.callback = callback; + + NotificationManager notificationManager = + (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); + boolean permissionGranted = notificationManager.isNotificationPolicyAccessGranted(); + + if (permissionGranted) { + this.callback.complete(true); + permissionRequestProgress = PermissionRequestProgress.None; + } else { + permissionRequestProgress = PermissionRequestProgress.RequestingNotificationPolicyAccess; + mainActivity.startActivityForResult( + new Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS), + NOTIFICATION_POLICY_ACCESS_REQUEST_CODE); + } + } + + public void hasNotificationPolicyAccess(Result result) { + if (VERSION.SDK_INT < VERSION_CODES.M) { + result.success(false); + return; + } + + NotificationManager notificationManager = + (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); + boolean isGranted = notificationManager.isNotificationPolicyAccessGranted(); + result.success(isGranted); + } + @Override public boolean onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { @@ -2028,6 +2164,12 @@ private void getActiveNotificationMessagingStyle(MethodCall call, Result result) msgPayload.put("text", msg.getText()); msgPayload.put("timestamp", msg.getTimestamp()); msgPayload.put("person", describePerson(msg.getPerson())); + if (msg.getDataUri() != null) { + msgPayload.put("dataUri", msg.getDataUri().toString()); + } + if (msg.getDataMimeType() != null) { + msgPayload.put("dataMimeType", msg.getDataMimeType()); + } messagesPayload.add(msgPayload); } stylePayload.put("messages", messagesPayload); @@ -2147,6 +2289,7 @@ private HashMap getMappedNotificationChannel(NotificationChannel channelPayload.put("sound", soundUri.toString()); } } + channelPayload.put("bypassDnd", channel.canBypassDnd()); channelPayload.put("enableVibration", channel.shouldVibrate()); channelPayload.put("vibrationPattern", channel.getVibrationPattern()); channelPayload.put("enableLights", channel.shouldShowLights()); @@ -2226,7 +2369,8 @@ private void setCanScheduleExactNotifications(Result result) { public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode != NOTIFICATION_PERMISSION_REQUEST_CODE && requestCode != EXACT_ALARM_PERMISSION_REQUEST_CODE - && requestCode != FULL_SCREEN_INTENT_PERMISSION_REQUEST_CODE) { + && requestCode != FULL_SCREEN_INTENT_PERMISSION_REQUEST_CODE + && requestCode != NOTIFICATION_POLICY_ACCESS_REQUEST_CODE) { return false; } @@ -2247,6 +2391,15 @@ public boolean onActivityResult(int requestCode, int resultCode, @Nullable Inten permissionRequestProgress = PermissionRequestProgress.None; } + if (permissionRequestProgress == PermissionRequestProgress.RequestingNotificationPolicyAccess + && requestCode == NOTIFICATION_POLICY_ACCESS_REQUEST_CODE + && VERSION.SDK_INT >= VERSION_CODES.M) { + NotificationManager notificationManager = + (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE); + this.callback.complete(notificationManager.isNotificationPolicyAccessGranted()); + permissionRequestProgress = PermissionRequestProgress.None; + } + return true; } @@ -2268,6 +2421,7 @@ public ExactAlarmPermissionException() { enum PermissionRequestProgress { None, RequestingNotificationPermission, + RequestingNotificationPolicyAccess, RequestingExactAlarmsPermission, RequestingFullScreenIntentPermission } 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 ad2046fe2..10897b659 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,6 +15,7 @@ import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; +import java.time.LocalDateTime; /** Created by michaelbui on 24/3/18. */ @Keep @@ -57,9 +58,20 @@ public void onReceive(final Context context, Intent intent) { Gson gson = FlutterLocalNotificationsPlugin.buildGson(); Type type = new TypeToken() {}.getType(); NotificationDetails notificationDetails = gson.fromJson(notificationDetailsJson, type); - - FlutterLocalNotificationsPlugin.showNotification(context, notificationDetails); + final boolean locked = FlutterLocalNotificationsPlugin.isKeyguardLocked(context); + if (notificationDetails.showNotification == null + || notificationDetails.showNotification + || locked) { + FlutterLocalNotificationsPlugin.showNotification(context, notificationDetails); + } FlutterLocalNotificationsPlugin.scheduleNextNotification(context, notificationDetails); + + boolean firstAlarm = + LocalDateTime.parse(notificationDetails.scheduledDateTime).getSecond() == 0; + boolean hasStartActivity = notificationDetails.startActivityClassName != null; + if (hasStartActivity && (!locked || !firstAlarm)) { + FlutterLocalNotificationsPlugin.startAlarmActivity(context, notificationDetails); + } } } } 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 bd72db630..a6efff674 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 @@ -83,6 +83,8 @@ public int hashCode() { private static final String SHOWS_USER_INTERFACE = "showsUserInterface"; private static final String ALLOW_GENERATED_REPLIES = "allowGeneratedReplies"; private static final String CANCEL_NOTIFICATION = "cancelNotification"; + private static final String SEMANTIC_ACTION = "semanticAction"; + private static final String INVISIBLE = "invisible"; public final String id; public final String title; @@ -92,6 +94,8 @@ public int hashCode() { @Nullable public final Boolean contextual; @Nullable public final Boolean showsUserInterface; @Nullable public final Boolean allowGeneratedReplies; + @Nullable public final Integer semanticAction; + @Nullable public final Boolean invisible; @Nullable public final IconSource iconSource; // actionInputs is annotated as nullable as the Flutter API use to allow this to be nullable // before null-safety was added in @@ -116,6 +120,8 @@ public NotificationAction(Map arguments) { contextual = (Boolean) arguments.get(CONTEXTUAL); showsUserInterface = (Boolean) arguments.get(SHOWS_USER_INTERFACE); allowGeneratedReplies = (Boolean) arguments.get(ALLOW_GENERATED_REPLIES); + semanticAction = (Integer) arguments.get(SEMANTIC_ACTION); + invisible = (Boolean) arguments.get(INVISIBLE); Integer iconSourceIndex = (Integer) arguments.get(ICON_SOURCE); if (iconSourceIndex != null) { 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 ffa4e6e70..983e2f839 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 @@ -13,6 +13,7 @@ public class NotificationChannelDetails implements Serializable { private static final String GROUP_ID = "groupId"; private static final String SHOW_BADGE = "showBadge"; private static final String IMPORTANCE = "importance"; + private static final String BYPASS_DND = "bypassDnd"; private static final String PLAY_SOUND = "playSound"; private static final String SOUND = "sound"; private static final String SOUND_SOURCE = "soundSource"; @@ -32,6 +33,7 @@ public class NotificationChannelDetails implements Serializable { public String groupId; public Boolean showBadge; public Integer importance; + public Boolean bypassDnd; public Boolean playSound; public String sound; public SoundSource soundSource; @@ -49,6 +51,7 @@ public static NotificationChannelDetails from(Map arguments) { notificationChannel.description = (String) arguments.get(DESCRIPTION); notificationChannel.groupId = (String) arguments.get(GROUP_ID); notificationChannel.importance = (Integer) arguments.get(IMPORTANCE); + notificationChannel.bypassDnd = (Boolean) arguments.get(BYPASS_DND); notificationChannel.showBadge = (Boolean) arguments.get(SHOW_BADGE); notificationChannel.channelAction = NotificationChannelAction.values()[(Integer) arguments.get(CHANNEL_ACTION)]; @@ -82,6 +85,7 @@ public static NotificationChannelDetails fromNotificationDetails( notificationChannel.name = notificationDetails.channelName; notificationChannel.description = notificationDetails.channelDescription; notificationChannel.importance = notificationDetails.importance; + notificationChannel.bypassDnd = notificationDetails.channelBypassDnd; notificationChannel.showBadge = notificationDetails.channelShowBadge; if (notificationDetails.channelAction == null) { notificationChannel.channelAction = NotificationChannelAction.CreateIfNotExists; 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 dcb59648a..abd15db4d 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 @@ -54,6 +54,7 @@ public class NotificationDetails implements Serializable { 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 CHANNEL_BYPASS_DND = "channelBypassDnd"; private static final String STYLE_INFORMATION = "styleInformation"; private static final String BIG_TEXT = "bigText"; private static final String HTML_FORMAT_BIG_TEXT = "htmlFormatBigText"; @@ -122,6 +123,8 @@ public class NotificationDetails implements Serializable { private static final String FULL_SCREEN_INTENT = "fullScreenIntent"; private static final String SHORTCUT_ID = "shortcutId"; + private static final String START_ACTIVITY_CLASS_NAME = "startActivityClassName"; + private static final String SHOW_NOTIFICATION = "showNotification"; private static final String SUB_TEXT = "subText"; private static final String ACTIONS = "actions"; private static final String COLORIZED = "colorized"; @@ -137,6 +140,7 @@ public class NotificationDetails implements Serializable { public String channelDescription; public Boolean channelShowBadge; public Integer importance; + public Boolean channelBypassDnd; public Integer priority; public Boolean playSound; public String sound; @@ -190,6 +194,8 @@ public class NotificationDetails implements Serializable { public Long when; public Boolean fullScreenIntent; public String shortcutId; + public String startActivityClassName; + public Boolean showNotification; public String subText; public @Nullable List actions; public String tag; @@ -291,6 +297,10 @@ private static void readPlatformSpecifics( notificationDetails.shortcutId = (String) platformChannelSpecifics.get(SHORTCUT_ID); notificationDetails.additionalFlags = (int[]) platformChannelSpecifics.get(ADDITIONAL_FLAGS); notificationDetails.subText = (String) platformChannelSpecifics.get(SUB_TEXT); + notificationDetails.startActivityClassName = + (String) platformChannelSpecifics.get(START_ACTIVITY_CLASS_NAME); + notificationDetails.showNotification = + (Boolean) platformChannelSpecifics.get(SHOW_NOTIFICATION); notificationDetails.tag = (String) platformChannelSpecifics.get(TAG); notificationDetails.colorized = (Boolean) platformChannelSpecifics.get(COLORIZED); notificationDetails.number = (Integer) platformChannelSpecifics.get(NUMBER); @@ -391,6 +401,8 @@ private static void readChannelInformation( notificationDetails.channelDescription = (String) platformChannelSpecifics.get(CHANNEL_DESCRIPTION); notificationDetails.importance = (Integer) platformChannelSpecifics.get(IMPORTANCE); + notificationDetails.channelBypassDnd = + (Boolean) platformChannelSpecifics.get(CHANNEL_BYPASS_DND); notificationDetails.channelShowBadge = (Boolean) platformChannelSpecifics.get(CHANNEL_SHOW_BADGE); notificationDetails.channelAction = diff --git a/flutter_local_notifications/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java b/flutter_local_notifications/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java index 7f5e61553..d9c962924 100644 --- a/flutter_local_notifications/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java +++ b/flutter_local_notifications/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java @@ -30,6 +30,7 @@ public void constructor_populatesAllFields() { raw.put("showsUserInterface", true); raw.put("allowGeneratedReplies", true); raw.put("iconBitmapSource", 4); + raw.put("semanticAction", 1); final List> inputs = new ArrayList<>(); final Map aInput = new HashMap<>(); @@ -46,11 +47,12 @@ public void constructor_populatesAllFields() { assertEquals("id123", action.id); assertEquals(true, action.cancelNotification); assertEquals("abc", action.title); - assertEquals(new Integer(2071756158), action.titleColor); + assertEquals(Integer.valueOf(2071756158), action.titleColor); assertEquals("icon", action.icon); assertEquals(true, action.contextual); assertEquals(true, action.showsUserInterface); assertEquals(true, action.allowGeneratedReplies); + assertEquals(Integer.valueOf(1), action.semanticAction); assertEquals(IconSource.ByteArray, action.iconSource); assertEquals( new NotificationActionInput( diff --git a/flutter_local_notifications/example/analysis_options.yaml b/flutter_local_notifications/example/analysis_options.yaml index fba01321b..cce009986 100644 --- a/flutter_local_notifications/example/analysis_options.yaml +++ b/flutter_local_notifications/example/analysis_options.yaml @@ -9,7 +9,6 @@ linter: - always_specify_types - annotate_overrides - avoid_annotating_with_dynamic - # - avoid_as - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses - avoid_catching_errors @@ -24,7 +23,6 @@ linter: - avoid_init_to_null - avoid_js_rounded_ints - avoid_null_checks_in_equality_operators - # - avoid_positional_boolean_parameters - avoid_print - avoid_private_typedef_functions - avoid_redundant_argument_values @@ -66,7 +64,6 @@ linter: - leading_newlines_in_multiline_strings - library_names - library_prefixes - # - lines_longer_than_80_chars - literal_only_boolean_expressions - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list @@ -78,13 +75,11 @@ linter: - one_member_abstracts - only_throw_errors - overridden_fields - - package_api_docs - package_names - package_prefixed_library_names - parameter_assignments - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - # - prefer_asserts_with_message - prefer_collection_literals - prefer_conditional_assignment - prefer_const_constructors @@ -132,11 +127,9 @@ linter: - type_annotate_public_apis - type_init_formals - unawaited_futures - # - unnecessary_await_in_return - unnecessary_brace_in_string_interps - unnecessary_const - unnecessary_getters_setters - # - unnecessary_lambdas - unnecessary_new - unnecessary_null_aware_assignments - unnecessary_null_in_if_null_operators @@ -148,7 +141,6 @@ linter: - unnecessary_string_interpolations - unnecessary_this - unrelated_type_equality_checks - - unsafe_html - use_full_hex_values_for_flutter_colors - use_function_type_syntax_for_parameters - use_key_in_widget_constructors diff --git a/flutter_local_notifications/example/android/app/build.gradle b/flutter_local_notifications/example/android/app/build.gradle index e729f6b3e..2bcbe588c 100644 --- a/flutter_local_notifications/example/android/app/build.gradle +++ b/flutter_local_notifications/example/android/app/build.gradle @@ -25,6 +25,7 @@ if (flutterVersionName == null) { android { namespace 'com.dexterous.flutter_local_notifications_example' compileSdk 35 + ndkVersion = flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' 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 595628cbb..266a44217 100644 --- a/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml +++ b/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + diff --git a/flutter_local_notifications/example/android/gradle.properties b/flutter_local_notifications/example/android/gradle.properties index 597cd5cc1..5e6ab6b2c 100644 --- a/flutter_local_notifications/example/android/gradle.properties +++ b/flutter_local_notifications/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 150c81c31..0c7919ce4 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -296,6 +296,13 @@ class _HomePageState extends State { } } + Future _requestNotificationPolicyAccess() async { + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + await androidImplementation?.requestNotificationPolicyAccess(); + } + void _configureSelectNotificationSubject() { selectNotificationStream.stream .listen((NotificationResponse? response) async { @@ -435,6 +442,13 @@ class _HomePageState extends State { await _cancelAllNotifications(); }, ), + if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) + PaddedElevatedButton( + buttonText: 'Cancel all pending notifications', + onPressed: () async { + await _cancelAllPendingNotifications(); + }, + ), if (!Platform.isWindows) ...repeating.examples(context), const Divider(), const Text( @@ -476,6 +490,16 @@ class _HomePageState extends State { style: TextStyle(fontWeight: FontWeight.bold), ), Text('notifications enabled: $_notificationsEnabled'), + PaddedElevatedButton( + buttonText: + 'No notification but alarm sound after 2 seconds ' + 'based on local time zone ' + 'with hasStartActivity ' + 'with custom sound running 30 seconds', + onPressed: () async { + await _noNotificationWithStartActivity(); + }, + ), PaddedElevatedButton( buttonText: 'Check if notifications are enabled for this app', @@ -485,6 +509,10 @@ class _HomePageState extends State { buttonText: 'Request permission (API 33+)', onPressed: () => _requestPermissions(), ), + PaddedElevatedButton( + buttonText: 'Request notification policy access', + onPressed: () => _requestNotificationPolicyAccess(), + ), PaddedElevatedButton( buttonText: 'Show plain notification with payload and update ' @@ -702,6 +730,19 @@ class _HomePageState extends State { await _deleteNotificationChannel(); }, ), + PaddedElevatedButton( + buttonText: + 'Create notification channel that ignores dnd', + onPressed: () async { + await _createNotificationChannelWithDndBypass(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification that ignores dnd', + onPressed: () async { + await _showNotificationWithDndBypass(); + }, + ), PaddedElevatedButton( buttonText: 'Get notification channels', onPressed: () async { @@ -1050,6 +1091,8 @@ class _HomePageState extends State { // user tapped on a action (this mimics the behavior on iOS). cancelNotification: false, ), + AndroidNotificationAction('read', 'Mark as read', + semanticAction: SemanticAction.markAsRead, invisible: true), ], ); @@ -1129,6 +1172,7 @@ class _HomePageState extends State { label: 'Enter a message', ), ], + semanticAction: SemanticAction.reply, ), ], ); @@ -1867,6 +1911,10 @@ class _HomePageState extends State { await flutterLocalNotificationsPlugin.cancelAll(); } + Future _cancelAllPendingNotifications() async { + await flutterLocalNotificationsPlugin.cancelAllPendingNotifications(); + } + Future _showOngoingNotification() async { const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails('your channel id', 'your channel name', @@ -2347,6 +2395,57 @@ class _HomePageState extends State { )); } + Future _createNotificationChannelWithDndBypass() async { + const AndroidNotificationChannel androidNotificationChannel = + AndroidNotificationChannel('your channel id 3', 'your channel name 3', + description: 'your channel description 3', + bypassDnd: true, + importance: Importance.max); + + final AndroidFlutterLocalNotificationsPlugin? androidPlugin = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + + final bool? hasPolicyAccess = + await androidPlugin?.hasNotificationPolicyAccess(); + if (hasPolicyAccess ?? false) { + await androidPlugin?.requestNotificationPolicyAccess(); + } + + await androidPlugin?.createNotificationChannel(androidNotificationChannel); + + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + content: Text( + 'Channel with name ${androidNotificationChannel.name} created'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ) + ], + ), + ); + } + + Future _showNotificationWithDndBypass() async { + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails('your channel id 3', 'your channel name 3', + channelDescription: 'your channel description 3', + channelBypassDnd: true, + importance: Importance.max); + const NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + + await flutterLocalNotificationsPlugin.show( + id++, + 'I ignored dnd', + 'I completely ignored dnd', + notificationDetails, + ); + } + Future _areNotifcationsEnabledOnAndroid() async { final bool? areEnabled = await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< @@ -2553,7 +2652,9 @@ class _HomePageState extends State { children: [ Text('text: ${msg.text}\n' 'timestamp: ${msg.timestamp}\n' - 'person: ${_formatPerson(msg.person)}'), + 'person: ${_formatPerson(msg.person)}\n' + 'dataMimeType: ${msg.dataMimeType}\n' + 'dataUri: ${msg.dataUri}'), const Divider(color: Colors.black), ], ), @@ -2673,6 +2774,7 @@ class _HomePageState extends State { 'description: ${channel.description}\n' 'groupId: ${channel.groupId}\n' 'importance: ${channel.importance.value}\n' + 'bypassDnd: ${channel.bypassDnd}\n' 'playSound: ${channel.playSound}\n' 'sound: ${channel.sound?.sound}\n' 'enableVibration: ${channel.enableVibration}\n' 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 27695dc6d..429a40961 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 @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m index 1d6f1a212..c15be9ba4 100644 --- a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m @@ -26,7 +26,9 @@ @implementation FlutterLocalNotificationsPlugin { @"periodicallyShowWithDuration"; NSString *const CANCEL_METHOD = @"cancel"; NSString *const CANCEL_ALL_METHOD = @"cancelAll"; -NSString *const PENDING_NOTIFICATIONS_REQUESTS_METHOD = +NSString *const CANCEL_ALL_PENDING_NOTIFICATIONS_METHOD = + @"cancelAllPendingNotifications"; +NSString *const PENDING_NOTIFICATION_REQUESTS_METHOD = @"pendingNotificationRequests"; NSString *const GET_ACTIVE_NOTIFICATIONS_METHOD = @"getActiveNotifications"; NSString *const GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD = @@ -185,6 +187,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call [self cancel:((NSNumber *)call.arguments) result:result]; } else if ([CANCEL_ALL_METHOD isEqualToString:call.method]) { [self cancelAll:result]; + } else if ([CANCEL_ALL_PENDING_NOTIFICATIONS_METHOD + isEqualToString:call.method]) { + [self cancelAllPendingNotifications:result]; } else if ([GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD isEqualToString:call.method]) { @@ -195,7 +200,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call notificationAppLaunchDetails[@"notificationResponse"] = _launchNotificationResponseDict; result(notificationAppLaunchDetails); - } else if ([PENDING_NOTIFICATIONS_REQUESTS_METHOD + } else if ([PENDING_NOTIFICATION_REQUESTS_METHOD isEqualToString:call.method]) { [self pendingNotificationRequests:result]; } else if ([GET_ACTIVE_NOTIFICATIONS_METHOD isEqualToString:call.method]) { @@ -576,6 +581,14 @@ - (void)cancelAll:(FlutterResult _Nonnull)result API_AVAILABLE(ios(10.0)) { result(nil); } +- (void)cancelAllPendingNotifications:(FlutterResult _Nonnull)result + API_AVAILABLE(ios(10.0)) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllPendingNotificationRequests]; + result(nil); +} + - (UNMutableNotificationContent *) buildStandardNotificationContent:(NSDictionary *)arguments result:(FlutterResult _Nonnull)result 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 bc866a073..4a9650131 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -302,6 +302,17 @@ class FlutterLocalNotificationsPlugin { await FlutterLocalNotificationsPlatform.instance.cancelAll(); } + /// Cancels/removes all pending notifications. + /// + /// This only applies to notifications that have been scheduled. + /// + /// The method is supported on Android, iOS and macOS. + /// On other platforms, an [UnimplementedError] will be thrown. + Future cancelAllPendingNotifications() async { + await FlutterLocalNotificationsPlatform.instance + .cancelAllPendingNotifications(); + } + /// Schedules a notification to be shown at the specified date and time /// relative to a specific time zone. /// 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 7781f4f21..bae3d29fc 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -42,6 +42,11 @@ class MethodChannelFlutterLocalNotificationsPlugin @override Future cancelAll() => _channel.invokeMethod('cancelAll'); + @override + Future cancelAllPendingNotifications() async { + await _channel.invokeMethod('cancelAllPendingNotifications'); + } + @override Future getNotificationAppLaunchDetails() async { @@ -188,6 +193,35 @@ class AndroidFlutterLocalNotificationsPlugin Future requestNotificationsPermission() async => _channel.invokeMethod('requestNotificationsPermission'); + /// Requests access to notification policy. + /// + /// Returns whether the permission was granted. + /// + /// This is required for channels that bypass DnD settings. Any attempt at + /// creating a notification channel with `bypassDnd: true` before access is + /// granted will print a warning and create the channel *without setting + /// bypassDnd*. + /// + /// On Android versions before API level 23, this is a no-op and returns + /// false. + /// + /// See also: + /// + /// * https://developer.android.com/reference/android/app/NotificationManager#isNotificationPolicyAccessGranted() + Future requestNotificationPolicyAccess() async => + _channel.invokeMethod('requestNotificationPolicyAccess'); + + /// Whether the app has access to notification policy. + /// + /// On Android versions before API level 23, this will always return false. + /// + /// See also: + /// + /// * https://developer.android.com/reference/android/app/NotificationManager#isNotificationPolicyAccessGranted() + /// * [requestNotificationPolicyAccess] + Future hasNotificationPolicyAccess() async => + _channel.invokeMethod('hasNotificationPolicyAccess'); + /// Schedules a notification to be shown at the specified date and time /// relative to a specific time zone. /// @@ -499,6 +533,8 @@ class AndroidFlutterLocalNotificationsPlugin m['text'], DateTime.fromMillisecondsSinceEpoch(m['timestamp']), _personFromMap(m['person']), + dataMimeType: m['dataMimeType'], + dataUri: m['dataUri'], ); AndroidIcon? _iconFromMap(Map? m) { @@ -534,6 +570,7 @@ class AndroidFlutterLocalNotificationsPlugin importance: Importance.values // ignore: always_specify_types .firstWhere((i) => i.value == a['importance']), + bypassDnd: a['bypassDnd'], playSound: a['playSound'], sound: _getNotificationChannelSound(a), enableLights: a['enableLights'], 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 33deb3780..3af775d16 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/android/enums.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/android/enums.dart @@ -269,6 +269,48 @@ enum AudioAttributesUsage { final int value; } +/// The available meanings that can be associated with a notification action. +enum SemanticAction { + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_NONE`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_NONE). + none(0), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_REPLY`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_REPLY). + reply(1), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_MARK_AS_READ`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_MARK_AS_READ). + markAsRead(2), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_MARK_AS_UNREAD`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_MARK_AS_UNREAD). + markAsUnread(3), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_DELETE`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_DELETE). + delete(4), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_ARCHIVE`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_ARCHIVE). + archive(5), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_MUTE`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_MUTE). + mute(6), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_UNMUTE`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_UNMUTE). + unmute(7), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_THUMBS_UP`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_THUMBS_UP). + thumbsUp(8), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_THUMBS_DOWN`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_THUMBS_DOWN). + thumbsDown(9), + + /// Corresponds to [`Notification.Action.SEMANTIC_ACTION_CALL`](https://developer.android.com/reference/android/app/Notification.Action#SEMANTIC_ACTION_CALL). + call(10); + + /// Constructs an instance of [SemanticAction]. + const SemanticAction(this.value); + + /// The integer representation of [SemanticAction]. + final int value; +} + /// The available categories for Android notifications. enum AndroidNotificationCategory { /// Alarm or timer. 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 cc1fa1994..62ce9fb68 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 @@ -45,6 +45,7 @@ extension AndroidNotificationChannelMapper on AndroidNotificationChannel { 'groupId': groupId, 'showBadge': showBadge, 'importance': importance.value, + 'bypassDnd': bypassDnd, 'playSound': playSound, 'enableVibration': enableVibration, 'vibrationPattern': vibrationPattern, @@ -180,6 +181,7 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'channelShowBadge': channelShowBadge, 'channelAction': channelAction.index, 'importance': importance.value, + 'channelBypassDnd': channelBypassDnd, 'priority': priority.value, 'playSound': playSound, 'enableVibration': enableVibration, @@ -216,6 +218,8 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { 'category': category?.name, 'fullScreenIntent': fullScreenIntent, 'shortcutId': shortcutId, + 'startActivityClassName': startActivityClassName, + 'showNotification': showNotification, 'additionalFlags': additionalFlags, 'subText': subText, 'tag': tag, @@ -310,6 +314,8 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails { _convertInputToMap(input)) .toList(), 'cancelNotification': e.cancelNotification, + 'semanticAction': e.semanticAction.value, + 'invisible': e.invisible, }, ) .toList(), 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 a240d8c86..de628f836 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 @@ -13,6 +13,7 @@ class AndroidNotificationChannel { this.description, this.groupId, this.importance = Importance.defaultImportance, + this.bypassDnd = false, this.playSound = true, this.sound, this.enableVibration = true, @@ -38,6 +39,14 @@ class AndroidNotificationChannel { /// The importance of the notification. final Importance importance; + /// Whether the notification channel should attempt to bypass Do Not Disturb + /// settings. + /// + /// You must acquire notification policy access by calling + /// [AndroidFlutterLocalNotificationsPlugin.requestNotificationPolicyAccess] + /// before setting this to true. Otherwise this value is ignored. + final bool bypassDnd; + /// Indicates if a sound should be played when the notification is displayed. /// /// Tied to the specified channel and cannot be changed after the channel has @@ -54,7 +63,7 @@ class AndroidNotificationChannel { /// Indicates if vibration should be enabled when the notification is /// displayed. - // + /// /// Tied to the specified channel and cannot be changed after the channel has /// been created for the first time. final bool enableVibration; @@ -79,11 +88,11 @@ class AndroidNotificationChannel { final Color? ledColor; /// Whether notifications posted to this channel can appear as application - /// icon badges in a Launcher + /// icon badges in a Launcher. final bool showBadge; /// The attribute describing what is the intended use of the audio signal, - /// such as alarm or ringtone set in [`AudioAttributes.Builder`](https://developer.android.com/reference/android/media/AudioAttributes.Builder#setUsage(int)) + /// such as alarm or ringtone set in [`AudioAttributes.Builder`](https://developer.android.com/reference/android/media/AudioAttributes.Builder#setUsage(int)). /// https://developer.android.com/reference/android/media/AudioAttributes final AudioAttributesUsage audioAttributesUsage; } 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 c3a23f1d2..65afb96e9 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 @@ -56,6 +56,8 @@ class AndroidNotificationAction { this.allowGeneratedReplies = false, this.inputs = const [], this.cancelNotification = true, + this.semanticAction = SemanticAction.none, + this.invisible = false, }); /// This ID will be sent back in the action handler defined in @@ -93,6 +95,13 @@ class AndroidNotificationAction { /// Set whether the notification should be canceled when this action is /// selected. final bool cancelNotification; + + /// The meaning to the action that hints at what the associated + /// PedingIntent will do. + final SemanticAction semanticAction; + + /// Sets the visibility of the action in the notification. + final bool invisible; } /// Contains notification details specific to Android. @@ -104,6 +113,7 @@ class AndroidNotificationDetails { this.channelDescription, this.icon, this.importance = Importance.defaultImportance, + this.channelBypassDnd = false, this.priority = Priority.defaultPriority, this.styleInformation, this.playSound = true, @@ -139,6 +149,8 @@ class AndroidNotificationDetails { this.category, this.fullScreenIntent = false, this.shortcutId, + this.startActivityClassName, + this.showNotification = true, this.additionalFlags, this.subText, this.tag, @@ -170,12 +182,20 @@ class AndroidNotificationDetails { final String? channelDescription; /// Whether notifications posted to this channel can appear as application - /// icon badges in a Launcher + /// icon badges in a Launcher. final bool channelShowBadge; /// The importance of the notification. final Importance importance; + /// Whether the notification channel should attempt to bypass Do Not Disturb + /// settings. + /// + /// You must acquire notification policy access by calling + /// [AndroidFlutterLocalNotificationsPlugin.requestNotificationPolicyAccess] + /// before setting this to true. Otherwise this value is ignored. + final bool channelBypassDnd; + /// The priority of the notification final Priority priority; @@ -317,7 +337,7 @@ class AndroidNotificationDetails { /// The action to take for managing notification channels. /// /// Defaults to creating the notification channel using the provided details - /// if it doesn't exist + /// if it doesn't exist. final AndroidNotificationChannelAction channelAction; /// Defines the notification visibility on the lockscreen. @@ -342,6 +362,12 @@ class AndroidNotificationDetails { /// page for your application. final bool fullScreenIntent; + /// The class name to start intent on lockscreen. + final String? startActivityClassName; + + /// If any notification should be shown if false only sound will play + final bool showNotification; + /// Specifies the id of a published, long-lived sharing that the notification /// will be linked to. /// @@ -405,7 +431,7 @@ class AndroidNotificationDetails { final int? number; /// The attribute describing what is the intended use of the audio signal, - /// such as alarm or ringtone set in [`AudioAttributes.Builder`](https://developer.android.com/reference/android/media/AudioAttributes.Builder#setUsage(int)) + /// such as alarm or ringtone set in [`AudioAttributes.Builder`](https://developer.android.com/reference/android/media/AudioAttributes.Builder#setUsage(int)). /// https://developer.android.com/reference/android/media/AudioAttributes final AudioAttributesUsage audioAttributesUsage; } diff --git a/flutter_local_notifications/macos/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.swift b/flutter_local_notifications/macos/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.swift index 264ff85f3..982b22d6e 100644 --- a/flutter_local_notifications/macos/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.swift +++ b/flutter_local_notifications/macos/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.swift @@ -2,7 +2,7 @@ import Cocoa import FlutterMacOS import UserNotifications -public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNotificationCenterDelegate, NSUserNotificationCenterDelegate { +public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNotificationCenterDelegate { struct MethodCallArguments { static let presentAlert = "presentAlert" @@ -57,14 +57,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot static let criticalSoundVolume = "criticalSoundVolume" } - struct ErrorMessages { - static let getActiveNotificationsErrorMessage = "macOS version must be 10.14 or newer to use getActiveNotifications" - } - - struct ErrorCodes { - static let unsupportedOSVersion = "unsupported_os_version" - } - struct DateFormatStrings { static let isoFormat = "yyyy-MM-dd'T'HH:mm:ss" } @@ -103,19 +95,13 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot 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) - if #available(macOS 10.14, *) { - let center = UNUserNotificationCenter.current() - center.delegate = instance - } else { - let center = NSUserNotificationCenter.default - center.delegate = instance - } + let center = UNUserNotificationCenter.current() + center.delegate = instance registrar.addMethodCallDelegate(instance, channel: channel) } // MARK: - UNUserNotificationCenterDelegate - @available(macOS 10.14, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { if !isAFlutterLocalNotification(userInfo: notification.request.content.userInfo) { return @@ -147,7 +133,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot completionHandler(options) } - @available(macOS 10.14, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if !isAFlutterLocalNotification(userInfo: response.notification.request.content.userInfo) { return @@ -179,20 +164,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } } - // MARK: - NSUserNotificationCenterDelegate - - @available(macOS, introduced: 10.8, deprecated: 11.0) - public func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { - if notification.activationType == .contentsClicked && notification.userInfo != nil && isAFlutterLocalNotification(userInfo: notification.userInfo!) { - handleSelectNotification(notificationId: Int(notification.identifier!)!, payload: notification.userInfo![MethodCallArguments.payload] as? String) - } - } - - @available(macOS, introduced: 10.8, deprecated: 11.0) - public func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool { - return true - } - // MARK: - FlutterPlugin implementation public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -209,6 +180,8 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot cancel(call, result) case "cancelAll": cancelAll(result) + case "cancelAllPendingNotifications": + cancelAllPendingNotifications(result) case "pendingNotificationRequests": pendingNotificationRequests(result) case "getActiveNotifications": @@ -234,297 +207,183 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot let defaultPresentBanner = arguments[MethodCallArguments.defaultPresentBanner] as! Bool let defaultPresentList = arguments[MethodCallArguments.defaultPresentList] as! Bool UserDefaults.standard.set([MethodCallArguments.presentAlert: defaultPresentAlert, MethodCallArguments.presentBadge: defaultPresentBadge, MethodCallArguments.presentSound: defaultPresentSound, MethodCallArguments.presentBanner: defaultPresentBanner, MethodCallArguments.presentList: defaultPresentList], forKey: presentationOptionsUserDefaults) - if #available(macOS 10.14, *) { - let requestedAlertPermission = arguments[MethodCallArguments.requestAlertPermission] as! Bool - let requestedSoundPermission = arguments[MethodCallArguments.requestSoundPermission] as! Bool - let requestedBadgePermission = arguments[MethodCallArguments.requestBadgePermission] as! Bool - let requestProvisionalPermission = arguments[MethodCallArguments.requestProvisionalPermission] as! Bool - let requestedCriticalPermission = arguments[MethodCallArguments.requestCriticalPermission] as! Bool - - configureNotificationCategories(arguments) { - self.requestPermissionsImpl( - soundPermission: requestedSoundPermission, - alertPermission: requestedAlertPermission, - badgePermission: requestedBadgePermission, - provisionalPermission: requestProvisionalPermission, - criticalPermission: requestedCriticalPermission, - result: result - ) - } + let requestedAlertPermission = arguments[MethodCallArguments.requestAlertPermission] as! Bool + let requestedSoundPermission = arguments[MethodCallArguments.requestSoundPermission] as! Bool + let requestedBadgePermission = arguments[MethodCallArguments.requestBadgePermission] as! Bool + let requestProvisionalPermission = arguments[MethodCallArguments.requestProvisionalPermission] as! Bool + let requestedCriticalPermission = arguments[MethodCallArguments.requestCriticalPermission] as! Bool - initialized = true - } else { - result(true) - initialized = true + configureNotificationCategories(arguments) { + self.requestPermissionsImpl( + soundPermission: requestedSoundPermission, + alertPermission: requestedAlertPermission, + badgePermission: requestedBadgePermission, + provisionalPermission: requestProvisionalPermission, + criticalPermission: requestedCriticalPermission, + result: result + ) } + + initialized = true } func configureNotificationCategories(_ arguments: [String: AnyObject], withCompletionHandler completionHandler: @escaping () -> Void) { - if #available(macOS 10.14, *) { - if let categories = arguments["notificationCategories"] as? [[String: AnyObject]] { - var notificationCategories = Set() - - for category in categories { - var newActions = [UNNotificationAction]() - - if let actions = category["actions"] as? [[String: AnyObject]] { - for action in actions { - let type = action["type"] as! String - let identifier = action["identifier"] as! String - let title = action["title"] as! String - let options = action["options"] as! [Any] - if type == "plain" { - newActions.append(UNNotificationAction( - identifier: identifier, - title: title, - options: FlutterLocalNotificationsConverters.parseNotificationActionOptions(options) - )) - } else if type == "text" { - let buttonTitle = action["buttonTitle"] as! String - let placeholder = action["placeholder"] as! String - newActions.append(UNTextInputNotificationAction( - identifier: identifier, - title: title, - options: FlutterLocalNotificationsConverters.parseNotificationActionOptions(options), - textInputButtonTitle: buttonTitle, - textInputPlaceholder: placeholder - )) - } + + if let categories = arguments["notificationCategories"] as? [[String: AnyObject]] { + var notificationCategories = Set() + + for category in categories { + var newActions = [UNNotificationAction]() + + if let actions = category["actions"] as? [[String: AnyObject]] { + for action in actions { + let type = action["type"] as! String + let identifier = action["identifier"] as! String + let title = action["title"] as! String + let options = action["options"] as! [Any] + if type == "plain" { + newActions.append(UNNotificationAction( + identifier: identifier, + title: title, + options: FlutterLocalNotificationsConverters.parseNotificationActionOptions(options) + )) + } else if type == "text" { + let buttonTitle = action["buttonTitle"] as! String + let placeholder = action["placeholder"] as! String + newActions.append(UNTextInputNotificationAction( + identifier: identifier, + title: title, + options: FlutterLocalNotificationsConverters.parseNotificationActionOptions(options), + textInputButtonTitle: buttonTitle, + textInputPlaceholder: placeholder + )) } } - - let notificationCategory = UNNotificationCategory( - identifier: category["identifier"] as! String, - actions: newActions, - intentIdentifiers: [], - hiddenPreviewsBodyPlaceholder: nil, - categorySummaryFormat: nil, - options: FlutterLocalNotificationsConverters.parseNotificationCategoryOptions(category["options"] as! [NSNumber]) - ) - - notificationCategories.insert(notificationCategory) } - if !notificationCategories.isEmpty { - let center = UNUserNotificationCenter.current() - center.setNotificationCategories(notificationCategories) - } + let notificationCategory = UNNotificationCategory( + identifier: category["identifier"] as! String, + actions: newActions, + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: nil, + categorySummaryFormat: nil, + options: FlutterLocalNotificationsConverters.parseNotificationCategoryOptions(category["options"] as! [NSNumber]) + ) - completionHandler() + notificationCategories.insert(notificationCategory) + } - } else { - completionHandler() + if !notificationCategories.isEmpty { + let center = UNUserNotificationCenter.current() + center.setNotificationCategories(notificationCategories) } + + completionHandler() + } else { completionHandler() } } func requestPermissions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - 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 - let requestedProvisionalPermission = arguments[MethodCallArguments.provisional] as! Bool - let requestedCriticalPermission = arguments[MethodCallArguments.critical] as! Bool + 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 + let requestedProvisionalPermission = arguments[MethodCallArguments.provisional] as! Bool + let requestedCriticalPermission = arguments[MethodCallArguments.critical] as! Bool - requestPermissionsImpl(soundPermission: requestedSoundPermission, alertPermission: requestedAlertPermission, badgePermission: requestedBadgePermission, provisionalPermission: requestedProvisionalPermission, criticalPermission: requestedCriticalPermission, result: result) - } else { - result(nil) - } + requestPermissionsImpl(soundPermission: requestedSoundPermission, alertPermission: requestedAlertPermission, badgePermission: requestedBadgePermission, provisionalPermission: requestedProvisionalPermission, criticalPermission: requestedCriticalPermission, result: result) } func getNotificationAppLaunchDetails(_ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - let appLaunchDetails: [String: Any?] = [MethodCallArguments.notificationLaunchedApp: launchingAppFromNotification, "notificationResponse": launchNotificationResponseDict] - result(appLaunchDetails) - } else { - result(nil) - } + let appLaunchDetails: [String: Any?] = [MethodCallArguments.notificationLaunchedApp: launchingAppFromNotification, "notificationResponse": launchNotificationResponseDict] + result(appLaunchDetails) } func cancel(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - let center = UNUserNotificationCenter.current() - let idsToRemove = [String(call.arguments as! Int)] - center.removePendingNotificationRequests(withIdentifiers: idsToRemove) - center.removeDeliveredNotifications(withIdentifiers: idsToRemove) - result(nil) - } else { - let id = String(call.arguments as! Int) - let center = NSUserNotificationCenter.default - for scheduledNotification in center.scheduledNotifications { - if scheduledNotification.identifier == id { - center.removeScheduledNotification(scheduledNotification) - break - } - } - let notification = NSUserNotification.init() - notification.identifier = id - center.removeDeliveredNotification(notification) - result(nil) - } + let center = UNUserNotificationCenter.current() + let idsToRemove = [String(call.arguments as! Int)] + center.removePendingNotificationRequests(withIdentifiers: idsToRemove) + center.removeDeliveredNotifications(withIdentifiers: idsToRemove) + result(nil) } func cancelAll(_ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - let center = UNUserNotificationCenter.current() - center.removeAllPendingNotificationRequests() - center.removeAllDeliveredNotifications() - result(nil) - } else { - let center = NSUserNotificationCenter.default - for scheduledNotification in center.scheduledNotifications { - center.removeScheduledNotification(scheduledNotification) - } - center.removeAllDeliveredNotifications() - result(nil) - } + let center = UNUserNotificationCenter.current() + center.removeAllPendingNotificationRequests() + center.removeAllDeliveredNotifications() + result(nil) + } + + func cancelAllPendingNotifications(_ result: @escaping FlutterResult) { + let center = UNUserNotificationCenter.current() + center.removeAllPendingNotificationRequests() + result(nil) } func pendingNotificationRequests(_ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - UNUserNotificationCenter.current().getPendingNotificationRequests { (requests) in - 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]]) - } - result(requestDictionaries) - } - } else { + UNUserNotificationCenter.current().getPendingNotificationRequests { (requests) in 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]]) + 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]]) } result(requestDictionaries) } } func getActiveNotifications(_ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - UNUserNotificationCenter.current().getDeliveredNotifications { (requests) in - var requestDictionaries: [[String: Any?]] = [] - for request in requests { - requestDictionaries.append([MethodCallArguments.id: Int(request.request.identifier) as Any, MethodCallArguments.title: request.request.content.title, MethodCallArguments.body: request.request.content.body, MethodCallArguments.payload: request.request.content.userInfo[MethodCallArguments.payload]]) - } - result(requestDictionaries) + UNUserNotificationCenter.current().getDeliveredNotifications { (requests) in + var requestDictionaries: [[String: Any?]] = [] + for request in requests { + requestDictionaries.append([MethodCallArguments.id: Int(request.request.identifier) as Any, MethodCallArguments.title: request.request.content.title, MethodCallArguments.body: request.request.content.body, MethodCallArguments.payload: request.request.content.userInfo[MethodCallArguments.payload]]) } - } else { - result(FlutterError.init(code: ErrorCodes.unsupportedOSVersion, message: ErrorMessages.getActiveNotificationsErrorMessage, details: nil)) + result(requestDictionaries) } } func show(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - do { - 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) - center.add(request) - result(nil) - } catch { - result(buildFlutterError(forMethodCallName: call.method, withError: error)) - } - } else { + do { let arguments = call.arguments as! [String: AnyObject] - let notification = buildNSUserNotification(fromArguments: arguments) - NSUserNotificationCenter.default.deliver(notification) + let content = try buildUserNotificationContent(fromArguments: arguments) + let center = UNUserNotificationCenter.current() + let request = UNNotificationRequest(identifier: getIdentifier(fromArguments: arguments), content: content, trigger: nil) + center.add(request) result(nil) + } catch { + result(buildFlutterError(forMethodCallName: call.method, withError: error)) } } func zonedSchedule(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - do { - let arguments = call.arguments as! [String: AnyObject] - let content = try buildUserNotificationContent(fromArguments: arguments) - let trigger = buildUserNotificationCalendarTrigger(fromArguments: arguments) - let center = UNUserNotificationCenter.current() - let request = UNNotificationRequest(identifier: getIdentifier(fromArguments: arguments), content: content, trigger: trigger) - center.add(request) - result(nil) - } catch { - result(buildFlutterError(forMethodCallName: call.method, withError: error)) - } - } else { + do { 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 - let timeZone = TimeZone.init(identifier: timeZoneName) - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withFractionalSeconds, .withInternetDateTime] - let date = dateFormatter.date(from: scheduledDateTime)! - notification.deliveryDate = date - notification.deliveryTimeZone = timeZone - if let rawDateTimeComponents = arguments[MethodCallArguments.matchDateTimeComponents] as? Int { - let dateTimeComponents = DateTimeComponents.init(rawValue: rawDateTimeComponents)! - switch dateTimeComponents { - case .time: - notification.deliveryRepeatInterval = DateComponents.init(day: 1) - case .dayOfWeekAndTime: - notification.deliveryRepeatInterval = DateComponents.init(weekOfYear: 1) - case .dayOfMonthAndTime: - notification.deliveryRepeatInterval = DateComponents.init(month: 1) - case .dateAndTime: - notification.deliveryRepeatInterval = DateComponents.init(year: 1) - } - } - NSUserNotificationCenter.default.scheduleNotification(notification) + let content = try buildUserNotificationContent(fromArguments: arguments) + let trigger = buildUserNotificationCalendarTrigger(fromArguments: arguments) + let center = UNUserNotificationCenter.current() + let request = UNNotificationRequest(identifier: getIdentifier(fromArguments: arguments), content: content, trigger: trigger) + center.add(request) result(nil) + } catch { + result(buildFlutterError(forMethodCallName: call.method, withError: error)) } } func periodicallyShow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - do { - let arguments = call.arguments as! [String: AnyObject] - let content = try buildUserNotificationContent(fromArguments: arguments) - let trigger = buildUserNotificationTimeIntervalTrigger(fromArguments: arguments) - let center = UNUserNotificationCenter.current() - let request = UNNotificationRequest(identifier: getIdentifier(fromArguments: arguments), content: content, trigger: trigger) - center.add(request) - result(nil) - } catch { - result(buildFlutterError(forMethodCallName: call.method, withError: error)) - } - } else { + do { let arguments = call.arguments as! [String: AnyObject] - let notification = buildNSUserNotification(fromArguments: arguments) - let rawRepeatInterval = arguments[MethodCallArguments.repeatInterval] as? Int - let repeatIntervalMilliseconds = arguments[MethodCallArguments.repeatIntervalMilliseconds] as? Int - - if rawRepeatInterval != nil { - let repeatInterval = RepeatInterval.init(rawValue: rawRepeatInterval!)! - switch repeatInterval { - case .everyMinute: - notification.deliveryDate = Date.init(timeIntervalSinceNow: 60) - notification.deliveryRepeatInterval = DateComponents.init(minute: 1) - case .hourly: - notification.deliveryDate = Date.init(timeIntervalSinceNow: 60 * 60) - notification.deliveryRepeatInterval = DateComponents.init(hour: 1) - case .daily: - notification.deliveryDate = Date.init(timeIntervalSinceNow: 60 * 60 * 24) - notification.deliveryRepeatInterval = DateComponents.init(day: 1) - case .weekly: - notification.deliveryDate = Date.init(timeIntervalSinceNow: 60 * 60 * 24 * 7) - notification.deliveryRepeatInterval = DateComponents.init(weekOfYear: 1) - } - } else if repeatIntervalMilliseconds != nil { - let repeatIntervalSeconds = repeatIntervalMilliseconds! / 1000 - notification.deliveryDate = Date.init(timeIntervalSinceNow: TimeInterval(repeatIntervalSeconds)) - notification.deliveryRepeatInterval = DateComponents.init(second: repeatIntervalSeconds) - } - NSUserNotificationCenter.default.scheduleNotification(notification) + let content = try buildUserNotificationContent(fromArguments: arguments) + let trigger = buildUserNotificationTimeIntervalTrigger(fromArguments: arguments) + let center = UNUserNotificationCenter.current() + let request = UNNotificationRequest(identifier: getIdentifier(fromArguments: arguments), content: content, trigger: trigger) + center.add(request) result(nil) + } catch { + result(buildFlutterError(forMethodCallName: call.method, withError: error)) } } - @available(macOS 10.14, *) func buildUserNotificationContent(fromArguments arguments: [String: AnyObject]) throws -> UNNotificationContent { let content = UNMutableNotificationContent() if let title = arguments[MethodCallArguments.title] as? String { @@ -547,15 +406,14 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init(sound)) } - if #available(macOS 10.14, *) { - if let volume = platformSpecifics[MethodCallArguments.criticalSoundVolume] as? NSNumber { - if let sound = platformSpecifics[MethodCallArguments.sound] as? String { - content.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(sound), withAudioVolume: volume.floatValue) - } else { - content.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: volume.floatValue) - } + if let volume = platformSpecifics[MethodCallArguments.criticalSoundVolume] as? NSNumber { + if let sound = platformSpecifics[MethodCallArguments.sound] as? String { + content.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(sound), withAudioVolume: volume.floatValue) + } else { + content.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: volume.floatValue) } } + if let badgeNumber = platformSpecifics[MethodCallArguments.badgeNumber] as? NSNumber { content.badge = badgeNumber } @@ -620,7 +478,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot return FlutterError.init(code: "\(methodCallName)_error", message: error.localizedDescription, details: "\(error)") } - @available(macOS 10.14, *) func buildUserNotificationCalendarTrigger(fromArguments arguments: [String: AnyObject]) -> UNCalendarNotificationTrigger { let scheduledDateTime = arguments[MethodCallArguments.scheduledDateTime] as! String let timeZoneName = arguments[MethodCallArguments.timeZoneName] as! String @@ -652,7 +509,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot return UNCalendarNotificationTrigger.init(dateMatching: dateComponents, repeats: false) } - @available(macOS 10.14, *) func buildUserNotificationTimeIntervalTrigger(fromArguments arguments: [String: AnyObject]) -> UNTimeIntervalNotificationTrigger { let rawRepeatInterval = arguments[MethodCallArguments.repeatInterval] as? Int let repeatIntervalMilliseconds = arguments[MethodCallArguments.repeatIntervalMilliseconds] as? Int @@ -675,7 +531,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } } - @available(macOS 10.14, *) func requestPermissionsImpl(soundPermission: Bool, alertPermission: Bool, badgePermission: Bool, provisionalPermission: Bool, criticalPermission: Bool, result: @escaping FlutterResult) { if !soundPermission && !alertPermission && !badgePermission && !provisionalPermission && !criticalPermission { result(false) @@ -703,53 +558,18 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot } func checkPermissions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - if #available(macOS 10.14, *) { - UNUserNotificationCenter.current().getNotificationSettings { settings in - let dict = [ - MethodCallArguments.isNotificationsEnabled: settings.authorizationStatus == .authorized, - MethodCallArguments.isSoundEnabled: settings.soundSetting == .enabled, - MethodCallArguments.isAlertEnabled: settings.alertSetting == .enabled, - MethodCallArguments.isBadgeEnabled: settings.badgeSetting == .enabled, - MethodCallArguments.isProvisionalEnabled: settings.authorizationStatus == .provisional, - MethodCallArguments.isCriticalEnabled: settings.criticalAlertSetting == .enabled - ] - - result(dict) - } - } else { - result(nil) - } - } - - @available(macOS, introduced: 10.8, deprecated: 11.0) - func buildNSUserNotification(fromArguments arguments: [String: AnyObject]) -> NSUserNotification { - let notification = NSUserNotification.init() - notification.identifier = getIdentifier(fromArguments: arguments) - if let title = arguments[MethodCallArguments.title] as? String { - notification.title = title - } - if let subtitle = arguments[MethodCallArguments.subtitle] as? String { - notification.subtitle = subtitle - } - if let body = arguments[MethodCallArguments.body] as? String { - notification.informativeText = body - } - var presentSound = false - 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 - } + UNUserNotificationCenter.current().getNotificationSettings { settings in + let dict = [ + MethodCallArguments.isNotificationsEnabled: settings.authorizationStatus == .authorized, + MethodCallArguments.isSoundEnabled: settings.soundSetting == .enabled, + MethodCallArguments.isAlertEnabled: settings.alertSetting == .enabled, + MethodCallArguments.isBadgeEnabled: settings.badgeSetting == .enabled, + MethodCallArguments.isProvisionalEnabled: settings.authorizationStatus == .provisional, + MethodCallArguments.isCriticalEnabled: settings.criticalAlertSetting == .enabled + ] + result(dict) } - notification.userInfo = [MethodCallArguments.payload: arguments[MethodCallArguments.payload] as Any] - if presentSound && notification.soundName == nil { - notification.soundName = NSUserNotificationDefaultSoundName - } - return notification } func getIdentifier(fromArguments arguments: [String: AnyObject]) -> String { @@ -768,7 +588,6 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot channel.invokeMethod("didReceiveNotificationResponse", arguments: arguments) } - @available(macOS 10.14, *) func extractNotificationResponseDict(response: UNNotificationResponse) -> [String: Any?] { var notificationResponseDict: [String: Any?] = [:] notificationResponseDict["notificationId"] = Int(response.notification.request.identifier)! diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 5db51063e..4972fbc8e 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.1.0 +version: 19.4.1 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications issue_tracker: https://github.com/MaikuB/flutter_local_notifications/issues @@ -11,8 +11,8 @@ dependencies: flutter: sdk: flutter flutter_local_notifications_linux: ^6.0.0 - flutter_local_notifications_windows: ^1.0.0 - flutter_local_notifications_platform_interface: ^9.0.0 + flutter_local_notifications_windows: ^1.0.2 + flutter_local_notifications_platform_interface: ^9.1.0 timezone: ^0.10.0 dev_dependencies: diff --git a/flutter_local_notifications/test/android_flutter_local_notifications_test.dart b/flutter_local_notifications/test/android_flutter_local_notifications_test.dart index ebfc62797..d8e101061 100644 --- a/flutter_local_notifications/test/android_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/android_flutter_local_notifications_test.dart @@ -93,6 +93,8 @@ void main() { showsUserInterface: true, allowGeneratedReplies: true, cancelNotification: false, + semanticAction: SemanticAction.markAsRead, + invisible: true, ), AndroidNotificationAction( 'action2', @@ -130,6 +132,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -178,6 +181,8 @@ void main() { }, 'tag': null, 'colorized': false, + 'startActivityClassName': null, + 'showNotification': true, 'number': null, 'audioAttributesUsage': 5, 'actions': >[ @@ -192,7 +197,9 @@ void main() { 'showsUserInterface': true, 'allowGeneratedReplies': true, 'inputs': [], - 'cancelNotification': false + 'cancelNotification': false, + 'semanticAction': SemanticAction.markAsRead.value, + 'invisible': true, }, { 'id': 'action2', @@ -213,6 +220,8 @@ void main() { } ], 'cancelNotification': true, + 'semanticAction': SemanticAction.none.value, + 'invisible': false, } ], }, @@ -252,6 +261,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -291,6 +301,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, @@ -336,6 +348,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -374,6 +387,8 @@ void main() { 'timeoutAfter': null, 'category': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'additionalFlags': [4, 32], @@ -421,6 +436,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -460,6 +476,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, @@ -507,6 +525,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -547,6 +566,8 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'startActivityClassName': null, + 'showNotification': true, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { @@ -595,6 +616,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -636,6 +658,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, @@ -684,6 +708,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -726,6 +751,8 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'startActivityClassName': null, + 'showNotification': true, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { @@ -773,6 +800,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -812,6 +840,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, @@ -861,6 +891,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -900,6 +931,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, @@ -951,6 +984,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -990,6 +1024,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, @@ -1056,6 +1092,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1095,6 +1132,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, @@ -1155,6 +1194,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1194,6 +1234,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, @@ -1260,6 +1302,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1299,6 +1342,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.bigPicture.index, @@ -1357,6 +1402,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1396,6 +1442,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.inbox.index, @@ -1458,6 +1506,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1497,6 +1546,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.inbox.index, @@ -1550,6 +1601,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1589,6 +1641,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.media.index, @@ -1639,6 +1693,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1678,6 +1733,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.media.index, @@ -1735,6 +1792,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1774,6 +1832,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.messaging.index, @@ -1860,6 +1920,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -1899,6 +1960,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.messaging.index, @@ -1974,6 +2037,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction .createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -2023,6 +2087,8 @@ void main() { 'tag': null, 'colorized': false, 'number': null, + 'startActivityClassName': null, + 'showNotification': true, 'audioAttributesUsage': 5, }, })); @@ -2108,6 +2174,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction .createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -2147,6 +2214,8 @@ void main() { 'category': null, 'additionalFlags': null, 'fullScreenIntent': false, + 'startActivityClassName': null, + 'showNotification': true, 'shortcutId': null, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, @@ -2204,6 +2273,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -2244,6 +2314,8 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'startActivityClassName': null, + 'showNotification': true, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { @@ -2299,6 +2371,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -2339,6 +2412,8 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'startActivityClassName': null, + 'showNotification': true, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { @@ -2395,6 +2470,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -2435,6 +2511,8 @@ void main() { 'additionalFlags': null, 'fullScreenIntent': false, 'shortcutId': null, + 'startActivityClassName': null, + 'showNotification': true, 'subText': null, 'style': AndroidNotificationStyle.defaultStyle.index, 'styleInformation': { @@ -2501,6 +2579,7 @@ void main() { 'groupId': null, 'showBadge': true, 'importance': Importance.defaultImportance.value, + 'bypassDnd': false, 'playSound': true, 'enableVibration': true, 'vibrationPattern': null, @@ -2526,6 +2605,7 @@ void main() { description: 'channelDescription', groupId: 'channelGroupId', showBadge: false, + bypassDnd: true, importance: Importance.max, playSound: false, enableLights: true, @@ -2541,6 +2621,7 @@ void main() { 'groupId': 'channelGroupId', 'showBadge': false, 'importance': Importance.max.value, + 'bypassDnd': true, 'playSound': false, 'enableVibration': false, 'vibrationPattern': null, @@ -2591,6 +2672,13 @@ void main() { expect(log, [isMethodCall('cancelAll', arguments: null)]); }); + test('cancelAllPendingNotifications', () async { + await flutterLocalNotificationsPlugin.cancelAllPendingNotifications(); + expect(log, [ + isMethodCall('cancelAllPendingNotifications', arguments: null) + ]); + }); + test('pendingNotificationRequests', () async { await flutterLocalNotificationsPlugin.pendingNotificationRequests(); expect(log, [ @@ -2695,6 +2783,7 @@ void main() { 'channelName': 'channelName', 'channelDescription': 'channelDescription', 'channelShowBadge': true, + 'channelBypassDnd': false, 'channelAction': AndroidNotificationChannelAction.createIfNotExists.index, 'importance': Importance.defaultImportance.value, @@ -2744,6 +2833,8 @@ void main() { 'tag': null, 'colorized': true, 'number': null, + 'startActivityClassName': null, + 'showNotification': true, 'audioAttributesUsage': 5, }, }, diff --git a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart index 5c8aff3e4..0b990f5e4 100644 --- a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart @@ -748,6 +748,13 @@ void main() { expect(log, [isMethodCall('cancelAll', arguments: null)]); }); + test('cancelAllPendingNotifications', () async { + await flutterLocalNotificationsPlugin.cancelAllPendingNotifications(); + expect(log, [ + isMethodCall('cancelAllPendingNotifications', arguments: null) + ]); + }); + test('pendingNotificationRequests', () async { await flutterLocalNotificationsPlugin.pendingNotificationRequests(); expect(log, [ diff --git a/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart index e41f71be8..290beb8b1 100644 --- a/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart @@ -669,6 +669,13 @@ void main() { expect(log, [isMethodCall('cancelAll', arguments: null)]); }); + test('cancelAllPendingNotifications', () async { + await flutterLocalNotificationsPlugin.cancelAllPendingNotifications(); + expect(log, [ + isMethodCall('cancelAllPendingNotifications', arguments: null) + ]); + }); + test('pendingNotificationRequests', () async { await flutterLocalNotificationsPlugin.pendingNotificationRequests(); expect(log, [ diff --git a/flutter_local_notifications_platform_interface/CHANGELOG.md b/flutter_local_notifications_platform_interface/CHANGELOG.md index 33ecc7d11..c94648e43 100644 --- a/flutter_local_notifications_platform_interface/CHANGELOG.md +++ b/flutter_local_notifications_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## [9.1.0] + +* Added `cancelAllPendingNotifications()` method for cancelling all pending notifications that have been scheduled. Thanks to the PR from [Kwon Tae Hyung](https://github.com/TaeBbong) + ## [9.0.0] * **Breaking change** bumped minimum Flutter SDK requirement to 3.22.0 and Dart SDK requirement to 3.4.0 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 387968afb..f5ee169bb 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 @@ -78,6 +78,14 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface { throw UnimplementedError('cancelAll() has not been implemented'); } + /// Cancels/removes all pending notifications. + /// + /// This only applies to notifications that have been scheduled. + Future cancelAllPendingNotifications() async { + throw UnimplementedError( + 'cancelAllPendingNotifications() has not been implemented'); + } + /// Returns a list of notifications pending to be delivered/shown Future> pendingNotificationRequests() { throw UnimplementedError( diff --git a/flutter_local_notifications_platform_interface/pubspec.yaml b/flutter_local_notifications_platform_interface/pubspec.yaml index a46407cb9..f598f657d 100644 --- a/flutter_local_notifications_platform_interface/pubspec.yaml +++ b/flutter_local_notifications_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_local_notifications_platform_interface description: A common platform interface for the flutter_local_notifications plugin. -version: 9.0.0 +version: 9.1.0 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface issue_tracker: https://github.com/MaikuB/flutter_local_notifications/issues diff --git a/flutter_local_notifications_windows/CHANGELOG.md b/flutter_local_notifications_windows/CHANGELOG.md index 3b64dbca6..cd2fa1530 100644 --- a/flutter_local_notifications_windows/CHANGELOG.md +++ b/flutter_local_notifications_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## [1.0.2] + +* Fixed issue [#2648](https://github.com/MaikuB/flutter_local_notifications/issues/2648) where non-ASCII characters in the notification payload were not being handled properly. Thanks to the PR from [yoyoIU](https://github.com/yoyo930021) + +## [1.0.1] + +* Fixed issue [#2651](https://github.com/MaikuB/flutter_local_notifications/issues/2651) where unresolved symbols occurred with changes in introduced in newer Windows SDKs. Thanks to the PR from [Sebastien](https://github.com/Sebastien-VZN) + ## [1.0.0] * Initial release for Windows. Thanks to PR [Levi Lesches](https://github.com/Levi-Lesches) that continued the work done initially done by [Kenneth](https://github.com/kennethnym) and [lightrabbit](https://github.com/lightrabbit) \ No newline at end of file diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 32bd11d0c..b037d0ba7 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 +version: 1.0.2 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_windows issue_tracker: https://github.com/MaikuB/flutter_local_notifications/issues diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index c96b17b28..9c7fc270b 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -32,14 +32,14 @@ struct NotificationActivationCallback : vector entries; for (ULONG i = 0; i < count; i++) { auto item = data[i]; - const std::string key = CW2A(item.Key); - const std::string value = CW2A(item.Value); + const std::string key = CW2A(item.Key, CP_UTF8); + const std::string value = CW2A(item.Value, CP_UTF8); const auto pair = StringMapEntry {toNativeString(key), toNativeString(value)}; entries.push_back(pair); } const auto openedWithAction = args != nullptr; - const auto payload = string(CW2A(args)); + const auto payload = string(CW2A(args, CP_UTF8)); const auto launchType = openedWithAction ? NativeLaunchType::action : NativeLaunchType::notification; NativeLaunchDetails launchDetails; diff --git a/flutter_local_notifications_windows/windows/CMakeLists.txt b/flutter_local_notifications_windows/windows/CMakeLists.txt index 3be2b5cf4..33c938e4a 100644 --- a/flutter_local_notifications_windows/windows/CMakeLists.txt +++ b/flutter_local_notifications_windows/windows/CMakeLists.txt @@ -21,3 +21,4 @@ set(flutter_local_notifications_windows_bundled_libraries $ PARENT_SCOPE ) +target_link_libraries(flutter_local_notifications_windows PRIVATE runtimeobject)