├── .gitignore ├── .github ├── FUNDING.yml ├── workflows │ └── npm-publish.yml └── ISSUE_TEMPLATE │ └── report_issue.yml ├── images ├── logo.png ├── android-chat.png ├── ios-actions.png ├── android-inbox.png ├── android-stack.png ├── android-actions.png ├── android-progress.png ├── ios-notification.png ├── ios-actions-with-input.png ├── ios-request-permission.png ├── android-request-permission.png ├── android-notification-example.png ├── ios-attachments-image-folded.png ├── android-attachments-image-folded.png ├── ios-attachments-image-unfolded.png ├── android-actions-with-input-clicked.png ├── android-attachments-image-unfolded.png ├── android-alarms-and-reminders-setting.png ├── android-app-hibernation-notification.png ├── android-actions-with-input-not-clicked.png ├── android-alarms-and-reminders-in-app-settings.png ├── android-app-hibernation-settings-android-12.png ├── android-app-hibernation-settings-android-15.png ├── android-app-hibernation-settings-android-13-14.png ├── apple-icon.svg └── android-icon.svg ├── src ├── ios │ ├── APPNotificationCategory.h │ ├── UNNotificationRequest+APPLocalNotification.h │ ├── APPNotificationContent.h │ ├── APPNotificationOptions.h │ ├── APPLocalNotification.h │ ├── UNUserNotificationCenter+APPLocalNotification.h │ ├── UNNotificationRequest+APPLocalNotification.m │ ├── APPNotificationContent.m │ ├── APPNotificationCategory.m │ ├── UNUserNotificationCenter+APPLocalNotification.m │ ├── APPNotificationOptions.m │ └── APPLocalNotification.m └── android │ ├── build │ └── localnotification.gradle │ ├── util │ ├── PluginFileProvider.java │ ├── CallbackContextUtil.java │ └── AssetUtil.java │ ├── xml │ └── shared_files_provider_paths.xml │ ├── trigger │ ├── TriggerHandlerAt.java │ ├── TriggerHandlerIn.java │ ├── TriggerHandler.java │ └── TriggerHandlerEvery.java │ ├── receiver │ ├── ClearReceiver.java │ ├── RestoreReceiver.java │ └── TriggerReceiver.java │ ├── ClickActivity.java │ ├── OptionsTrigger.java │ ├── action │ ├── ActionGroup.java │ └── Action.java │ └── Manager.java ├── package.json ├── LICENSE └── plugin.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: GitToTheHub -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/android-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-chat.png -------------------------------------------------------------------------------- /images/ios-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/ios-actions.png -------------------------------------------------------------------------------- /images/android-inbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-inbox.png -------------------------------------------------------------------------------- /images/android-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-stack.png -------------------------------------------------------------------------------- /images/android-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-actions.png -------------------------------------------------------------------------------- /images/android-progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-progress.png -------------------------------------------------------------------------------- /images/ios-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/ios-notification.png -------------------------------------------------------------------------------- /images/ios-actions-with-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/ios-actions-with-input.png -------------------------------------------------------------------------------- /images/ios-request-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/ios-request-permission.png -------------------------------------------------------------------------------- /images/android-request-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-request-permission.png -------------------------------------------------------------------------------- /images/android-notification-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-notification-example.png -------------------------------------------------------------------------------- /images/ios-attachments-image-folded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/ios-attachments-image-folded.png -------------------------------------------------------------------------------- /images/android-attachments-image-folded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-attachments-image-folded.png -------------------------------------------------------------------------------- /images/ios-attachments-image-unfolded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/ios-attachments-image-unfolded.png -------------------------------------------------------------------------------- /images/android-actions-with-input-clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-actions-with-input-clicked.png -------------------------------------------------------------------------------- /images/android-attachments-image-unfolded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-attachments-image-unfolded.png -------------------------------------------------------------------------------- /images/android-alarms-and-reminders-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-alarms-and-reminders-setting.png -------------------------------------------------------------------------------- /images/android-app-hibernation-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-app-hibernation-notification.png -------------------------------------------------------------------------------- /images/android-actions-with-input-not-clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-actions-with-input-not-clicked.png -------------------------------------------------------------------------------- /images/android-alarms-and-reminders-in-app-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-alarms-and-reminders-in-app-settings.png -------------------------------------------------------------------------------- /images/android-app-hibernation-settings-android-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-app-hibernation-settings-android-12.png -------------------------------------------------------------------------------- /images/android-app-hibernation-settings-android-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-app-hibernation-settings-android-15.png -------------------------------------------------------------------------------- /images/android-app-hibernation-settings-android-13-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/HEAD/images/android-app-hibernation-settings-android-13-14.png -------------------------------------------------------------------------------- /images/apple-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/android-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ios/APPNotificationCategory.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | @import UserNotifications; 23 | 24 | @interface APPNotificationCategory : NSObject 25 | 26 | + (UNNotificationCategory*) parse:(NSArray*)list withId:(NSString*)groupId; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | # Run workflow, when a new release is created 8 | release: 9 | types: [created] 10 | 11 | # Allows to run this workflow manually 12 | workflow_dispatch: 13 | 14 | jobs: 15 | 16 | publish-npm: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | registry-url: https://registry.npmjs.org/ 24 | - run: npm ci 25 | - run: npm publish 26 | env: 27 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 28 | # Purge the cache of the GitHub CDN to update the NPM badge in the readme 29 | - run: curl -X PURGE https://camo.githubusercontent.com/11f744ab82ca8017011e9aba93139ea753fa9bc6d6c7d96b81cbde52205ec5fd/68747470733a2f2f62616467652e667572792e696f2f6a732f636f72646f76612d706c7567696e2d6c6f63616c2d6e6f74696669636174696f6e2e737667 30 | -------------------------------------------------------------------------------- /src/android/build/localnotification.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains Original Code and/or Modifications of Original Code 3 | * as defined in and that are subject to the Apache License 4 | * Version 2.0 (the 'License'). You may not use this file except in 5 | * compliance with the License. Please obtain a copy of the License at 6 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 7 | * file. 8 | * 9 | * The Original Code and all software distributed under the License are 10 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 11 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 12 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 13 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 14 | * Please see the License for the specific language governing rights and 15 | * limitations under the License. 16 | */ 17 | 18 | repositories { 19 | mavenCentral() 20 | maven { 21 | url "https://maven.google.com" 22 | } 23 | } 24 | 25 | dependencies { 26 | // Needed for getUnusedAppRestrictionsStatus: 27 | // - Class ListenableFuture 28 | implementation("com.google.guava:guava:33.4.0-android") 29 | } -------------------------------------------------------------------------------- /src/ios/UNNotificationRequest+APPLocalNotification.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationOptions.h" 23 | 24 | @import UserNotifications; 25 | 26 | @interface UNNotificationRequest (APPLocalNotification) 27 | 28 | - (APPNotificationOptions*) options; 29 | - (BOOL) wasUpdated; 30 | - (NSString*) encodeToJSON; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /src/ios/APPNotificationContent.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationOptions.h" 23 | 24 | @import UserNotifications; 25 | 26 | @interface APPNotificationContent : UNMutableNotificationContent 27 | 28 | - (id) initWithOptions:(NSDictionary*)dict; 29 | - (APPNotificationOptions*) options; 30 | - (UNNotificationRequest*) request; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /src/android/util/PluginFileProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package de.appplant.cordova.plugin.localnotification.util; 21 | 22 | import androidx.core.content.FileProvider; 23 | 24 | /** 25 | * FileProvider is a special subclass of ContentProvider that facilitates secure sharing of 26 | * files associated with an app by creating a content:// Uri for a file instead of a file:/// Uri. 27 | * 28 | * It is possible to use FileProvider directly instead of extending it. 29 | * However, this is not reliable and will causes crashes on some devices. 30 | */ 31 | public class PluginFileProvider extends FileProvider { 32 | public PluginFileProvider() { 33 | } 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-local-notification", 3 | "version": "1.2.3", 4 | "description": "Schedules and queries for local notifications", 5 | "cordova": { 6 | "id": "cordova-plugin-local-notification", 7 | "platforms": [ 8 | "android", 9 | "ios" 10 | ] 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/katzer/cordova-plugin-local-notifications.git" 15 | }, 16 | "keywords": [ 17 | "notification", 18 | "local notification", 19 | "user notification", 20 | "schedule notification", 21 | "ecosystem:cordova", 22 | "cordova-ios", 23 | "cordova-android" 24 | ], 25 | "engines": { 26 | "cordovaDependencies": { 27 | "0.9.0-beta.3": { 28 | "cordova": ">=3.6.0", 29 | "cordova-android": ">=6.0.0", 30 | "cordova-ios": ">=4.3.0", 31 | "cordova-plugin-device": ">=2.0.0" 32 | }, 33 | "1.0.0": { 34 | "cordova": ">=12.0.0", 35 | "cordova-android": ">=13.0.0", 36 | "cordova-ios": ">=7.0.0", 37 | "cordova-plugin-device": ">=3.0.0" 38 | } 39 | } 40 | }, 41 | "author": "Sebastián Katzer", 42 | "contributors": [{ 43 | "name": "Manuel Beck", 44 | "email": "email@manuelbeck.software", 45 | "url": "https://manuelbeck.software" 46 | }], 47 | "funding": "https://github.com/sponsors/GitToTheHub", 48 | "license": "Apache 2.0", 49 | "bugs": { 50 | "url": "https://github.com/katzer/cordova-plugin-local-notifications/issues" 51 | }, 52 | "homepage": "https://github.com/katzer/cordova-plugin-local-notifications#readme" 53 | } 54 | -------------------------------------------------------------------------------- /src/android/xml/shared_files_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 23 | 24 | 25 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/ios/APPNotificationOptions.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | @import UserNotifications; 23 | 24 | @interface APPNotificationOptions : NSObject 25 | 26 | @property (readonly, getter=id) NSNumber* id; 27 | @property (readonly, getter=identifier) NSString* identifier; 28 | @property (readonly, getter=actionGroupId) NSString* actionGroupId; 29 | @property (readonly, getter=title) NSString* title; 30 | @property (readonly, getter=subtitle) NSString* subtitle; 31 | @property (readonly, getter=badgeNumber) int badgeNumber; 32 | @property (readonly, getter=text) NSString* text; 33 | @property (readonly, getter=iOSForeground) BOOL iOSForeground; 34 | @property (readonly, getter=silent) BOOL silent; 35 | @property (readonly, getter=sound) UNNotificationSound* sound; 36 | @property (readonly, getter=userInfo) NSDictionary* userInfo; 37 | @property (readonly, getter=attachments) NSArray*attachments; 38 | 39 | - (id) initWithDict:(NSDictionary*) dict; 40 | - (UNNotificationTrigger*) trigger; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /src/ios/APPLocalNotification.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import 23 | 24 | @import UserNotifications; 25 | 26 | @interface APPLocalNotification : CDVPlugin 27 | 28 | - (void) launch:(CDVInvokedUrlCommand*)command; 29 | - (void) ready:(CDVInvokedUrlCommand*)command; 30 | 31 | - (void) actions:(CDVInvokedUrlCommand*)command; 32 | 33 | - (void) hasPermission:(CDVInvokedUrlCommand*)command; 34 | - (void) requestPermission:(CDVInvokedUrlCommand*)command; 35 | 36 | - (void) schedule:(CDVInvokedUrlCommand*)command; 37 | - (void) update:(CDVInvokedUrlCommand*)command; 38 | 39 | - (void) clear:(CDVInvokedUrlCommand*)command; 40 | - (void) clearAll:(CDVInvokedUrlCommand*)command; 41 | 42 | - (void) cancel:(CDVInvokedUrlCommand*)command; 43 | - (void) cancelAll:(CDVInvokedUrlCommand*)command; 44 | 45 | - (void) type:(CDVInvokedUrlCommand*)command; 46 | 47 | - (void) ids:(CDVInvokedUrlCommand*)command; 48 | 49 | - (void) notification:(CDVInvokedUrlCommand*)command; 50 | - (void) notifications:(CDVInvokedUrlCommand*)command; 51 | 52 | - (void) openNotificationSettings:(CDVInvokedUrlCommand*)command; 53 | 54 | - (void) clearBadge:(CDVInvokedUrlCommand*)command; 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /src/android/trigger/TriggerHandlerAt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification.trigger; 23 | 24 | import android.util.Log; 25 | 26 | import java.util.Calendar; 27 | import java.util.Date; 28 | 29 | import de.appplant.cordova.plugin.localnotification.Options; 30 | import de.appplant.cordova.plugin.localnotification.OptionsTrigger; 31 | 32 | /** 33 | * Handles trigger.at 34 | * 35 | * Example: 36 | * trigger: { at: new Date(2017, 10, 27, 15) } 37 | */ 38 | public class TriggerHandlerAt extends TriggerHandler { 39 | 40 | public static final String TAG = "TriggerHandlerAt"; 41 | 42 | public TriggerHandlerAt(Options options) { 43 | super(options); 44 | } 45 | 46 | public boolean isLastOccurrence() { 47 | // Can only be scheduled one time 48 | return occurrence == 1; 49 | } 50 | 51 | /** 52 | * Calculates the next trigger. 53 | * @param baseCalendar The base calendar from where to calculate the next trigger. 54 | */ 55 | public Date calculateNextTrigger(Calendar baseCalendar) { 56 | // All occurrences are done 57 | if (isLastOccurrence()) return null; 58 | 59 | // trigger: { at: new Date(2017, 10, 27, 15) } 60 | return new Date(optionsTrigger.getAt()); 61 | } 62 | } -------------------------------------------------------------------------------- /src/ios/UNUserNotificationCenter+APPLocalNotification.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationContent.h" 23 | 24 | @interface UNUserNotificationCenter (APPLocalNotification) 25 | 26 | extern NSString * const kAPPGeneralCategory; 27 | 28 | typedef NS_ENUM(NSUInteger, APPNotificationType) { 29 | NotifcationTypeAll = 0, 30 | NotifcationTypeScheduled = 1, 31 | NotifcationTypeTriggered = 2, 32 | NotifcationTypeUnknown = 3 33 | }; 34 | 35 | #define APPNotificationType_DEFINED 36 | 37 | @property (readonly, getter=getNotifications) NSArray* localNotifications; 38 | @property (readonly, getter=getNotificationIds) NSArray* localNotificationIds; 39 | 40 | - (void) registerGeneralNotificationCategory; 41 | - (void) addActionGroup:(UNNotificationCategory*)category; 42 | - (void) removeActionGroup:(NSString*)identifier; 43 | - (BOOL) hasActionGroup:(NSString*)identifier; 44 | 45 | - (NSArray*) getNotificationIdsByType:(APPNotificationType)type; 46 | 47 | - (UNNotificationRequest*) getNotificationWithId:(NSNumber*)id; 48 | - (APPNotificationType) getTypeOfNotificationWithId:(NSNumber*)id; 49 | 50 | - (NSArray*) getNotificationOptions; 51 | - (NSArray*) getNotificationOptionsById:(NSArray*)ids; 52 | - (NSArray*) getNotificationOptionsByType:(APPNotificationType)type; 53 | 54 | - (void) clearNotification:(UNNotificationRequest*)notification; 55 | - (void) clearNotifications; 56 | 57 | - (void) cancelNotification:(UNNotificationRequest*)notification; 58 | - (void) cancelNotifications; 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /src/android/receiver/ClearReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * Copyright (c) Manuel Beck 2024 6 | * 7 | * This file contains Original Code and/or Modifications of Original Code 8 | * as defined in and that are subject to the Apache License 9 | * Version 2.0 (the 'License'). You may not use this file except in 10 | * compliance with the License. Please obtain a copy of the License at 11 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 12 | * file. 13 | * 14 | * The Original Code and all software distributed under the License are 15 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 16 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 17 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 19 | * Please see the License for the specific language governing rights and 20 | * limitations under the License. 21 | */ 22 | 23 | package de.appplant.cordova.plugin.localnotification.receiver; 24 | 25 | import android.content.BroadcastReceiver; 26 | import android.content.Context; 27 | import android.content.Intent; 28 | import android.os.Bundle; 29 | import android.util.Log; 30 | 31 | import de.appplant.cordova.plugin.localnotification.Notification; 32 | 33 | /** 34 | * The clear intent receiver is triggered when the user clears a 35 | * notification manually. It removes the notification from the 36 | * SharedPreferences if it was the last onex to trigger. 37 | */ 38 | public class ClearReceiver extends BroadcastReceiver { 39 | 40 | public static final String TAG = "ClearReceiver"; 41 | 42 | /** 43 | * Called when the notification was cleared from the notification center. 44 | * @param context Application context 45 | * @param intent Received intent with content data 46 | */ 47 | @Override 48 | public void onReceive(Context context, Intent intent) { 49 | Notification notification = Notification.getFromSharedPreferences(context, intent.getExtras().getInt(Notification.EXTRA_ID)); 50 | 51 | // Notification not found for id in SharedPreferences 52 | if (notification == null) { 53 | Log.w(TAG, "Notification not found for id, doing nothing, id=" + intent.getExtras().getInt(Notification.EXTRA_ID)); 54 | return; 55 | } 56 | 57 | // Will remove the notification from SharedPreferences if it is the last one 58 | notification.clear(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/android/trigger/TriggerHandlerIn.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification.trigger; 23 | 24 | import android.util.Log; 25 | 26 | import java.util.Calendar; 27 | import java.util.Date; 28 | 29 | import de.appplant.cordova.plugin.localnotification.Options; 30 | import de.appplant.cordova.plugin.localnotification.OptionsTrigger; 31 | 32 | /** 33 | * Handles trigger.in 34 | * 35 | * Example: 36 | * trigger: { in: 1, unit: 'hour' } 37 | */ 38 | public class TriggerHandlerIn extends TriggerHandler { 39 | 40 | public static final String TAG = "TriggerHandlerIn"; 41 | 42 | public TriggerHandlerIn(Options options) { 43 | super(options); 44 | } 45 | 46 | public boolean isLastOccurrence() { 47 | // trigger.in can only be scheduled one time 48 | return occurrence == 1; 49 | } 50 | 51 | /** 52 | * Calculates the next trigger. 53 | * @param baseCalendar The base calendar from where to calculate the next trigger. 54 | */ 55 | public Date calculateNextTrigger(Calendar baseCalendar) { 56 | // All occurrences are done 57 | if (isLastOccurrence()) return null; 58 | 59 | // trigger: { in: 1, unit: 'hour' } 60 | // Catch wrong trigger units 61 | try { 62 | addInterval(baseCalendar, optionsTrigger.getUnit(), optionsTrigger.getIn()); 63 | return baseCalendar.getTime(); 64 | } catch (IllegalArgumentException exception) { 65 | Log.e(TAG, "Error calculating trigger, trigger unit is wrong: " + exception.getMessage()); 66 | return null; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/android/receiver/RestoreReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2015 by appPlant UG. All rights reserved. 3 | * 4 | * @APPPLANT_LICENSE_HEADER_START@ 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | * 21 | * @APPPLANT_LICENSE_HEADER_END@ 22 | */ 23 | 24 | package de.appplant.cordova.plugin.localnotification.receiver; 25 | 26 | import android.app.AlarmManager; 27 | import android.content.BroadcastReceiver; 28 | import android.content.Context; 29 | import android.content.Intent; 30 | import android.util.Log; 31 | 32 | import java.util.List; 33 | 34 | import de.appplant.cordova.plugin.localnotification.Manager; 35 | import de.appplant.cordova.plugin.localnotification.Notification; 36 | 37 | /** 38 | * This class is triggered, when the system has cleared the alarms and notifications, 39 | * e.g. because of a device reboot, app update or granting the SCHEDULE_EXACT_ALARM permission. 40 | * The alarms and notifications needs to be restored. 41 | */ 42 | public class RestoreReceiver extends BroadcastReceiver { 43 | 44 | public static final String TAG = "RestoreReceiver"; 45 | 46 | /** 47 | * Called when alarms and notifications need to be restored. 48 | * @param context Application context 49 | * @param intent Received intent with content data 50 | */ 51 | @Override 52 | public void onReceive(Context context, Intent intent) { 53 | Log.d(TAG, "Received action: " + intent.getAction()); 54 | 55 | // The device was booted and is unlocked 56 | if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED) || 57 | // The app was updated 58 | intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED) || 59 | // The app is granted the SCHEDULE_EXACT_ALARM permission 60 | intent.getAction().equals(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED)) { 61 | 62 | List notifications = new Manager(context).getNotificationsFromSharedPreferences(); 63 | Log.d(TAG, "Restoring notifications, count: " + notifications.size()); 64 | 65 | for (Notification notification : notifications) { 66 | notification.schedule(); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/android/ClickActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * Copyright (c) Manuel Beck 2024-2025 6 | * 7 | * This file contains Original Code and/or Modifications of Original Code 8 | * as defined in and that are subject to the Apache License 9 | * Version 2.0 (the 'License'). You may not use this file except in 10 | * compliance with the License. Please obtain a copy of the License at 11 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 12 | * file. 13 | * 14 | * The Original Code and all software distributed under the License are 15 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 16 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 17 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 19 | * Please see the License for the specific language governing rights and 20 | * limitations under the License. 21 | */ 22 | 23 | package de.appplant.cordova.plugin.localnotification; 24 | 25 | import android.app.Activity; 26 | import android.os.Bundle; 27 | import android.util.Log; 28 | 29 | import de.appplant.cordova.plugin.localnotification.Notification; 30 | import de.appplant.cordova.plugin.localnotification.action.Action; 31 | 32 | /** 33 | * Handle a notification or action click. 34 | * To be able to launch the app on Android 12 and newer, an Activity must be used, 35 | * instead of a BroadcastReceiver, otherwise a trampoline error would occur, 36 | * if the app is in background or killed, see: 37 | * https://developer.android.com/about/versions/12/behavior-changes-12#notification-trampolines 38 | */ 39 | public class ClickActivity extends Activity { 40 | 41 | private static final String TAG = "ClickActivity"; 42 | 43 | @Override 44 | public void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | 47 | int notificationId = getIntent().getExtras().getInt(Notification.EXTRA_ID); 48 | // Get the clicked action id, if an action was clicked, otherwise it is null 49 | String actionId = getIntent().getStringExtra(Action.EXTRA_ID); 50 | Notification notification = Notification.getFromSharedPreferences(getApplicationContext(), notificationId); 51 | 52 | Log.d(TAG, "Notification clicked, id=" + notificationId + ", actionId=" + actionId); 53 | 54 | // Check if the notification data is available 55 | // Normally it should be available, but in some cases it isn't 56 | if (notification != null) { 57 | // Handle action click 58 | if (actionId != null) { 59 | notification.handleActionClick(getIntent(), actionId); 60 | 61 | // Handle notification click 62 | } else { 63 | notification.handleClick(); 64 | } 65 | } else { 66 | Log.w(TAG, "Notification data not found, id=" + notificationId); 67 | } 68 | 69 | finish(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/android/receiver/TriggerReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * Copyright (c) Manuel Beck 2024 6 | * 7 | * This file contains Original Code and/or Modifications of Original Code 8 | * as defined in and that are subject to the Apache License 9 | * Version 2.0 (the 'License'). You may not use this file except in 10 | * compliance with the License. Please obtain a copy of the License at 11 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 12 | * file. 13 | * 14 | * The Original Code and all software distributed under the License are 15 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 16 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 17 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 19 | * Please see the License for the specific language governing rights and 20 | * limitations under the License. 21 | */ 22 | 23 | package de.appplant.cordova.plugin.localnotification.receiver; 24 | 25 | import android.content.BroadcastReceiver; 26 | import android.content.Context; 27 | import android.content.Intent; 28 | import android.util.Log; 29 | 30 | import de.appplant.cordova.plugin.localnotification.Notification; 31 | 32 | /** 33 | * The alarm receiver is triggered when a scheduled alarm is fired. This class 34 | * reads the information in the intent and displays this information in the 35 | * Android notification bar. The notification uses the default notification 36 | * sound and it vibrates the phone. 37 | */ 38 | public class TriggerReceiver extends BroadcastReceiver { 39 | 40 | public static final String TAG = "TriggerReceiver"; 41 | 42 | /** 43 | * Called when an alarm was triggered. 44 | * @param context Application context 45 | * @param intent Received intent with content data 46 | */ 47 | @Override 48 | public void onReceive(Context context, Intent intent) { 49 | Log.d(TAG, "Received action: " + intent.getAction()); 50 | 51 | Notification notification = Notification.getFromSharedPreferences(context, intent.getExtras().getInt(Notification.EXTRA_ID)); 52 | 53 | // Notification not found for id in SharedPreferences 54 | if (notification == null) { 55 | Log.w(TAG, "Notification not found for id, doing nothing, id=" + intent.getExtras().getInt(Notification.EXTRA_ID)); 56 | return; 57 | } 58 | 59 | // Show the notification 60 | notification.show(false); 61 | 62 | // Schedule next notification if available. The notification 63 | // will not be removed from the SharedPreferences, if there is no 64 | // next trigger. So the ClickActivity 65 | // and ClearReceiver can still read the notification data. They 66 | // will remove the notification from the SharedPreferences if they are 67 | // executed. A notification can ony be cleared or clicked. 68 | notification.scheduleNext(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ios/UNNotificationRequest+APPLocalNotification.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationOptions.h" 23 | #import "UNNotificationRequest+APPLocalNotification.h" 24 | #import "APPNotificationContent.h" 25 | #import 26 | 27 | @import UserNotifications; 28 | 29 | static char optionsKey; 30 | 31 | @implementation UNNotificationRequest (APPLocalNotification) 32 | 33 | /** 34 | * Get associated option object 35 | */ 36 | - (APPNotificationOptions*) getOptions 37 | { 38 | return objc_getAssociatedObject(self, &optionsKey); 39 | } 40 | 41 | /** 42 | * Set associated option object 43 | */ 44 | - (void) setOptions:(APPNotificationOptions*)options 45 | { 46 | objc_setAssociatedObject(self, &optionsKey, 47 | options, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 48 | } 49 | 50 | /** 51 | * The options provided by the plug-in. 52 | */ 53 | - (APPNotificationOptions*) options 54 | { 55 | APPNotificationOptions* options = [self getOptions]; 56 | 57 | if (!options) { 58 | options = [[APPNotificationOptions alloc] 59 | initWithDict:[self.content userInfo]]; 60 | 61 | [self setOptions:options]; 62 | } 63 | 64 | return options; 65 | } 66 | 67 | /** 68 | * If the notification was updated. 69 | * 70 | * @return [ BOOL ] 71 | */ 72 | - (BOOL) wasUpdated 73 | { 74 | return [self.content userInfo][@"updatedAt"] != NULL; 75 | } 76 | 77 | /** 78 | * Encode the user info dict to JSON. 79 | */ 80 | - (NSString*) encodeToJSON 81 | { 82 | NSString* json; 83 | NSMutableDictionary* obj = [self.content.userInfo mutableCopy]; 84 | 85 | [obj removeObjectForKey:@"updatedAt"]; 86 | 87 | if (obj == NULL || obj.count == 0) return json; 88 | 89 | NSData* data = [NSJSONSerialization dataWithJSONObject:obj 90 | options:NSJSONWritingPrettyPrinted 91 | error:NULL]; 92 | 93 | json = [[NSString alloc] initWithData:data 94 | encoding:NSUTF8StringEncoding]; 95 | 96 | return [json stringByReplacingOccurrencesOfString:@"\n" 97 | withString:@""]; 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /src/android/OptionsTrigger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Manuel Beck 2025 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification; 23 | 24 | import org.json.JSONObject; 25 | 26 | public class OptionsTrigger { 27 | 28 | private JSONObject triggerJSON; 29 | 30 | public OptionsTrigger(JSONObject triggerJSON) { 31 | this.triggerJSON = triggerJSON; 32 | } 33 | 34 | public boolean has(String key) { 35 | return triggerJSON.has(key); 36 | } 37 | 38 | public long getAt() { 39 | return triggerJSON.optLong("at", 0); 40 | } 41 | 42 | public int getIn() { 43 | return triggerJSON.optInt("in", 0); 44 | } 45 | 46 | public String getUnit() { 47 | return triggerJSON.optString("unit", null); 48 | } 49 | 50 | /** 51 | * Only for repeating notifications, when the first notification should be triggered. 52 | */ 53 | public long getFirstAt() { 54 | return triggerJSON.optLong("firstAt", 0); 55 | } 56 | 57 | /** 58 | * Only for repeating notifications, when the first notification should be triggered. 59 | */ 60 | public long getAfter() { 61 | return triggerJSON.optLong("after", 0); 62 | } 63 | 64 | /** 65 | * If a repeating notification should be stopped after some occurrences. -1 means infinite. 66 | * @return 67 | */ 68 | public int getCount() { 69 | return triggerJSON.optInt("count", -1); 70 | } 71 | 72 | /** 73 | * Can be a {@link String} or {@link JSONObject}. 74 | */ 75 | public Object getEvery() { 76 | return triggerJSON.opt("every"); 77 | } 78 | 79 | /** 80 | * Gets trigger.every as {@link String}. If trigger.every is a {@link JSONObject}, it returns null 81 | */ 82 | public String getEveryAsString() { 83 | return getEvery() instanceof String ? (String) getEvery() : null; 84 | } 85 | 86 | /** 87 | * Gets trigger.every as {@link JSONObject}. If trigger.every is a {@link String}, it returns null 88 | */ 89 | public JSONObject getEveryAsJSONObject() { 90 | return getEvery() instanceof JSONObject ? (JSONObject) getEvery() : null; 91 | } 92 | 93 | public long getBefore() { 94 | return triggerJSON.optLong("before", 0); 95 | } 96 | 97 | public JSONObject getJSON() { 98 | return triggerJSON; 99 | } 100 | 101 | public String toString() { 102 | return triggerJSON.toString(); 103 | } 104 | } -------------------------------------------------------------------------------- /src/android/util/CallbackContextUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * Copyright (c) Manuel Beck 2025 6 | * 7 | * This file contains Original Code and/or Modifications of Original Code 8 | * as defined in and that are subject to the Apache License 9 | * Version 2.0 (the 'License'). You may not use this file except in 10 | * compliance with the License. Please obtain a copy of the License at 11 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 12 | * file. 13 | * 14 | * The Original Code and all software distributed under the License are 15 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 16 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 17 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 19 | * Please see the License for the specific language governing rights and 20 | * limitations under the License. 21 | */ 22 | 23 | package de.appplant.cordova.plugin.localnotification.util; 24 | 25 | import android.util.Log; 26 | import de.appplant.cordova.plugin.localnotification.Manager; 27 | 28 | import java.util.HashMap; 29 | 30 | import org.apache.cordova.CallbackContext; 31 | 32 | /** 33 | * Utils class to store and reuse a CallbackContext. 34 | * Most of the code in this class was copied from the Diagnostic plugin: 35 | * https://github.com/dpa99c/cordova-diagnostic-plugin 36 | */ 37 | public final class CallbackContextUtil { 38 | 39 | private static final String TAG = "CallbackContextUtil"; 40 | 41 | // Map of permission request code to callback context. 42 | protected static HashMap callbackContexts = new HashMap(); 43 | 44 | /** 45 | * Constructor 46 | */ 47 | private CallbackContextUtil() {} 48 | 49 | /** 50 | * Gets a callback context for a request code or null if not found. 51 | * @return CallbackContext or null if not found. 52 | */ 53 | public static CallbackContext getCallbackContext(int requestCode) { 54 | CallbackContext callbackContext = callbackContexts.get(requestCode); 55 | 56 | // Log error, if no context found 57 | if (callbackContexts == null) { 58 | Log.e(TAG, "No context found for request code=" + requestCode); 59 | } 60 | 61 | return callbackContext; 62 | } 63 | 64 | /** 65 | * Store a {@link CallbackContext} for later retrieval and return a random request code. 66 | * @return Random request code for the stored context. 67 | */ 68 | public static int storeContext(CallbackContext callbackContext) { 69 | return storeContext(callbackContext, Manager.getRandomRequestCode()); 70 | } 71 | 72 | /** 73 | * Store a {@link CallbackContext} for later retrieval and return the request code. 74 | * @return Request code for the stored context. 75 | */ 76 | public static int storeContext(CallbackContext callbackContext, int requestCode){ 77 | callbackContexts.put(requestCode, callbackContext); 78 | return requestCode; 79 | } 80 | 81 | /** 82 | * Removes the stored {@link CallbackContext} for a request code. 83 | */ 84 | public static void clearContext(int requestCode) { 85 | if (!callbackContexts.containsKey(requestCode)) return; 86 | callbackContexts.remove(requestCode); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/report_issue.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Create a report to help us improve 3 | title: "[BUG] " 4 | labels: ["bug"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thank you for reporting a bug! Please fill out the form below with as much detail as possible. 11 | 12 | - type: textarea 13 | id: bug_description 14 | attributes: 15 | label: Describe the bug 16 | description: A clear and concise description of what the bug is. 17 | placeholder: Describe the bug... 18 | validations: 19 | required: true 20 | 21 | - type: input 22 | id: plugin_version 23 | attributes: 24 | label: Plugin version 25 | placeholder: e.g. 1.2.3 26 | validations: 27 | required: true 28 | 29 | - type: input 30 | id: platform 31 | attributes: 32 | label: Platform 33 | placeholder: e.g. Android, iOS, Windows 34 | validations: 35 | required: true 36 | 37 | - type: input 38 | id: os_version 39 | attributes: 40 | label: OS version 41 | placeholder: e.g. iOS 14.4, Android 10 42 | validations: 43 | required: true 44 | 45 | - type: input 46 | id: device_manufacturer_model 47 | attributes: 48 | label: Device manufacturer / model 49 | placeholder: e.g. Samsung Galaxy S10 50 | validations: 51 | required: true 52 | 53 | - type: input 54 | id: cordova_version 55 | attributes: 56 | label: Cordova version 57 | placeholder: e.g. 10.0.0 58 | validations: 59 | required: true 60 | 61 | - type: input 62 | id: cordova_platform_version 63 | attributes: 64 | label: Cordova platform version 65 | placeholder: e.g. cordova-android 9.0.0 66 | validations: 67 | required: true 68 | 69 | - type: textarea 70 | id: plugin_config 71 | attributes: 72 | label: Plugin config 73 | placeholder: Add your plugin configuration here... 74 | validations: 75 | required: true 76 | 77 | - type: input 78 | id: ionic_version 79 | attributes: 80 | label: Ionic Version (if using Ionic) 81 | placeholder: e.g. 5.4.16 82 | 83 | - type: textarea 84 | id: expected_behavior 85 | attributes: 86 | label: Expected behavior 87 | description: A clear and concise description of what you expected to happen. 88 | placeholder: Describe expected behavior... 89 | validations: 90 | required: true 91 | 92 | - type: textarea 93 | id: actual_behavior 94 | attributes: 95 | label: Actual behavior 96 | description: A clear and concise description of what actually happens. 97 | placeholder: Describe actual behavior... 98 | validations: 99 | required: true 100 | 101 | - type: textarea 102 | id: steps_to_reproduce 103 | attributes: 104 | label: Steps to Reproduce 105 | description: Include code to reproduce the issue if relevant. 106 | placeholder: | 107 | 1. Go to '...' 108 | 2. Click on '...' 109 | 3. Scroll down to '...' 110 | 4. See error 111 | validations: 112 | required: true 113 | 114 | - type: textarea 115 | id: context 116 | attributes: 117 | label: Context 118 | description: What were you trying to do? 119 | placeholder: Provide context here... 120 | validations: 121 | required: true 122 | 123 | - type: textarea 124 | id: debug_logs 125 | attributes: 126 | label: Debug logs 127 | description: Include iOS / Android logs. 128 | placeholder: | 129 | * ios XCode logs 130 | * Android: $ adb logcat 131 | validations: 132 | required: false 133 | -------------------------------------------------------------------------------- /src/ios/APPNotificationContent.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationContent.h" 23 | #import "APPNotificationOptions.h" 24 | #import 25 | 26 | @import UserNotifications; 27 | 28 | static char optionsKey; 29 | 30 | @implementation APPNotificationContent : UNMutableNotificationContent 31 | 32 | #pragma mark - 33 | #pragma mark Init 34 | 35 | /** 36 | * Initialize a notification with the given options. 37 | * @param dict A key-value property map. 38 | * @return [ UNMutableNotificationContent ] 39 | */ 40 | - (id) initWithOptions:(NSDictionary*)dict 41 | { 42 | self = [self init]; 43 | 44 | [self setUserInfo:dict]; 45 | [self __init]; 46 | 47 | return self; 48 | } 49 | 50 | /** 51 | * Initialize a notification by using the options found under userInfo. 52 | */ 53 | - (void) __init 54 | { 55 | self.title = self.options.title; 56 | self.subtitle = self.options.subtitle; 57 | self.body = self.options.text; 58 | self.sound = self.options.sound; 59 | // -1 will not change the badge, 0 will clear it 60 | self.badge = self.options.badgeNumber == -1 ? nil : [NSNumber numberWithInt:self.options.badgeNumber]; 61 | self.attachments = self.options.attachments; 62 | self.categoryIdentifier = self.options.actionGroupId; 63 | } 64 | 65 | #pragma mark - 66 | #pragma mark Public 67 | 68 | /** 69 | * The options used to initialize the notification. 70 | * 71 | * @return [ APPNotificationOptions* ] options 72 | */ 73 | - (APPNotificationOptions*) options 74 | { 75 | APPNotificationOptions* options = [self getOptions]; 76 | 77 | if (!options) { 78 | options = [[APPNotificationOptions alloc] 79 | initWithDict:[self userInfo]]; 80 | 81 | [self setOptions:options]; 82 | } 83 | 84 | return options; 85 | } 86 | 87 | /** 88 | * Creates a notification request object that you use to schedule a notification. 89 | * 90 | * @return [ UNNotificationRequest* ] 91 | */ 92 | - (UNNotificationRequest*) request 93 | { 94 | APPNotificationOptions* options = [self getOptions]; 95 | 96 | return [UNNotificationRequest requestWithIdentifier:options.identifier 97 | content:self 98 | trigger:options.trigger]; 99 | } 100 | 101 | #pragma mark - 102 | #pragma mark Private 103 | 104 | /** 105 | * The options used to initialize the notification. 106 | * 107 | * @return [ APPNotificationOptions* ] 108 | */ 109 | - (APPNotificationOptions*) getOptions 110 | { 111 | return objc_getAssociatedObject(self, &optionsKey); 112 | } 113 | 114 | /** 115 | * Set the options used to initialize the notification. 116 | */ 117 | - (void) setOptions:(APPNotificationOptions*)options 118 | { 119 | objc_setAssociatedObject(self, &optionsKey, options, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /src/ios/APPNotificationCategory.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationCategory.h" 23 | 24 | @import UserNotifications; 25 | 26 | @implementation APPNotificationCategory : NSObject 27 | 28 | #pragma mark - 29 | #pragma mark Public 30 | 31 | /** 32 | * Parse the provided spec map into an action group. 33 | * @param list A key-value property map. Must contain an id and a list of actions. 34 | * @return [ UNNotificationCategory* ] 35 | */ 36 | + (UNNotificationCategory*) parse:(NSArray*)list withId:(NSString*)groupId 37 | { 38 | return [UNNotificationCategory categoryWithIdentifier:groupId 39 | actions:[self parseActions:list] 40 | intentIdentifiers:@[] 41 | options:UNNotificationCategoryOptionCustomDismissAction]; 42 | } 43 | 44 | #pragma mark - 45 | #pragma mark Private 46 | 47 | /** 48 | * The actions of the action group. 49 | * 50 | * @return [ NSArray* ] 51 | */ 52 | + (NSArray *) parseActions:(NSArray*)items 53 | { 54 | NSMutableArray* actions = [[NSMutableArray alloc] init]; 55 | 56 | for (NSDictionary* item in items) { 57 | NSString* id = item[@"id"]; 58 | NSString* title = item[@"title"]; 59 | NSString* type = item[@"type"]; 60 | 61 | UNNotificationActionOptions options = UNNotificationActionOptionNone; 62 | UNNotificationAction* action; 63 | 64 | if ([item[@"launch"] boolValue]) { 65 | options = UNNotificationActionOptionForeground; 66 | } 67 | 68 | if ([item[@"ui"] isEqualToString:@"decline"]) { 69 | options = options | UNNotificationActionOptionDestructive; 70 | } 71 | 72 | if ([item[@"needsAuth"] boolValue]) { 73 | options = options | UNNotificationActionOptionAuthenticationRequired; 74 | } 75 | 76 | if ([type isEqualToString:@"input"]) { 77 | NSString* submitTitle = item[@"submitTitle"]; 78 | NSString* placeholder = item[@"emptyText"]; 79 | 80 | if (!submitTitle.length) { 81 | submitTitle = @"Submit"; 82 | } 83 | 84 | action = [UNTextInputNotificationAction actionWithIdentifier:id 85 | title:title 86 | options:options 87 | textInputButtonTitle:submitTitle 88 | textInputPlaceholder:placeholder]; 89 | } else 90 | if (!type.length || [type isEqualToString:@"button"]) { 91 | action = [UNNotificationAction actionWithIdentifier:id 92 | title:title 93 | options:options]; 94 | } else { 95 | NSLog(@"Unknown action type: %@", type); 96 | } 97 | 98 | if (action) { 99 | NSLog(@"Adding action: %@", action); 100 | [actions addObject:action]; 101 | } 102 | } 103 | 104 | return actions; 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /src/android/action/ActionGroup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification.action; 23 | 24 | import android.content.Context; 25 | import android.util.Log; 26 | 27 | import org.json.JSONArray; 28 | import org.json.JSONException; 29 | 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | 33 | import de.appplant.cordova.plugin.localnotification.Manager; 34 | 35 | public class ActionGroup { 36 | 37 | private static final String TAG = "ActionGroup"; 38 | 39 | // Action group id 40 | private String id; 41 | 42 | // Actions JSON array, needed for storage 43 | private JSONArray actionsJSONArray; 44 | 45 | // List of actions 46 | private List actions; 47 | 48 | private Context context; 49 | 50 | public ActionGroup(Context context, String actionGroupId, JSONArray actionsJSONArray) { 51 | this.context = context; 52 | this.id = actionGroupId; 53 | this.actionsJSONArray = actionsJSONArray; 54 | this.actions = new ArrayList(actionsJSONArray.length()); 55 | 56 | for (int i = 0; i < actionsJSONArray.length(); i++) { 57 | this.actions.add(new Action(context, actionsJSONArray.optJSONObject(i))); 58 | } 59 | } 60 | 61 | /** 62 | * Gets the action group id. 63 | */ 64 | public String getId() { 65 | return id; 66 | } 67 | 68 | /** 69 | * Gets the action list. 70 | */ 71 | public List getActions() { 72 | return actions; 73 | } 74 | 75 | /** 76 | * Gets the action by id. 77 | * @param actionId The id of the action to get. 78 | * @return The action with the specified id or null if not found. 79 | */ 80 | public Action getActionById(String actionId) { 81 | for (Action action : actions) { 82 | if (action.getId().equals(actionId)) { 83 | return action; 84 | } 85 | } 86 | 87 | Log.w(TAG, "Action not found, id=" + actionId); 88 | return null; 89 | } 90 | 91 | /** 92 | * Stores this action group in the {@link SharedPreferences}. 93 | */ 94 | public void store() { 95 | Manager.getSharedPreferences(context).edit() 96 | .putString("ACTION_GROUP_" + id, actionsJSONArray.toString()) 97 | .apply(); 98 | } 99 | 100 | /** 101 | * Removes the action group from the {@link SharedPreferences}. 102 | * @param context 103 | * @param actionGroupId 104 | */ 105 | public static void remove(Context context, String actionGroupId) { 106 | Manager.getSharedPreferences(context).edit() 107 | .remove("ACTION_GROUP_" + actionGroupId) 108 | .apply(); 109 | } 110 | 111 | /** 112 | * Gets the action group with the specified actionGroupId from the 113 | * {@link SharedPreferences}. 114 | * 115 | * @param context The application context. 116 | * @param actionGroupId The id of the action group to get. 117 | * 118 | * @return The restored action group from {@link SharedPreferences} or null if not found. 119 | */ 120 | public static ActionGroup get(Context context, String actionGroupId) { 121 | String actionsJSON = Manager.getSharedPreferences(context) 122 | .getString("ACTION_GROUP_" + actionGroupId, null); 123 | 124 | if (actionsJSON == null) return null; 125 | 126 | try { 127 | JSONArray actionsJSONArray = new JSONArray(actionsJSON); 128 | return new ActionGroup(context, actionGroupId, actionsJSONArray); 129 | } catch (JSONException jsonException) { 130 | Log.e(TAG, "Failed to restore action group: " + actionGroupId, jsonException); 131 | return null; 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/android/trigger/TriggerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification.trigger; 23 | 24 | import android.util.Log; 25 | import java.util.Calendar; 26 | import java.util.Date; 27 | 28 | import org.json.JSONObject; 29 | 30 | import de.appplant.cordova.plugin.localnotification.Options; 31 | import de.appplant.cordova.plugin.localnotification.OptionsTrigger; 32 | 33 | public abstract class TriggerHandler { 34 | 35 | public static final String TAG = "TriggerHandler"; 36 | 37 | Options options; 38 | 39 | /** Helper for trigger property */ 40 | OptionsTrigger optionsTrigger; 41 | 42 | int occurrence = 0; 43 | 44 | /** 45 | * The base date from where to calculate the next trigger 46 | */ 47 | Date baseDate; 48 | 49 | /** 50 | * trigger date calculated by {@link #getNextTriggerDate()} 51 | */ 52 | Date triggerDate; 53 | 54 | /** 55 | * @param options Notification options 56 | */ 57 | public TriggerHandler(Options options) { 58 | this.options = options; 59 | this.optionsTrigger = options.getOptionsTrigger(); 60 | // Set the base date from where to calculate the next trigger 61 | // This can be set by config or is set to the current date 62 | this.baseDate = new Date(); 63 | } 64 | 65 | public abstract boolean isLastOccurrence(); 66 | 67 | /** 68 | * Calculates the next trigger. Can return null if there's no next trigger. 69 | * @param baseCalendar The base calendar from where to calculate the next trigger. 70 | */ 71 | public abstract Date calculateNextTrigger(Calendar baseCalendar); 72 | 73 | /** 74 | * Gets the next trigger date. 75 | * @param base The date from where to calculate the trigger date. 76 | * @return null if there's none next trigger date. 77 | */ 78 | public Date getNextTriggerDate() { 79 | // Use last trigger date as base date for calculating the next trigger 80 | if (triggerDate != null) baseDate = triggerDate; 81 | 82 | // Clear the last trigger date, so it's reflecting the current status of this date trigger 83 | triggerDate = null; 84 | 85 | Log.d(TAG, "Get next trigger date" + 86 | ", baseDate=" + baseDate + 87 | ", triggerOptions=" + optionsTrigger.toString() + 88 | ", occurrence=" + occurrence); 89 | 90 | // All occurrences have been run through 91 | if (isLastOccurrence()) return null; 92 | 93 | Date nextTriggerDate = calculateNextTrigger(dateToCalendar(baseDate)); 94 | 95 | Log.d(TAG, "Next trigger date: " + nextTriggerDate + ", notificationId=" + options.getId()); 96 | 97 | if (nextTriggerDate == null) return null; 98 | 99 | // Count occurrence 100 | occurrence++; 101 | 102 | // Remember trigger date 103 | triggerDate = nextTriggerDate; 104 | 105 | return nextTriggerDate; 106 | } 107 | 108 | /** 109 | * Restores the state of the trigger, when the notification is loaded from the SharedPreferences 110 | * @param occurrence 111 | * @param baseDate 112 | * @param triggerDate 113 | */ 114 | public void restoreState(int occurrence, Date baseDate, Date triggerDate) { 115 | this.occurrence = occurrence; 116 | this.baseDate = baseDate; 117 | this.triggerDate = triggerDate; 118 | } 119 | 120 | public Date getTriggerDate() { 121 | return triggerDate; 122 | } 123 | 124 | public Date getBaseDate() { 125 | return baseDate; 126 | } 127 | 128 | /** 129 | * The value of the occurrence. 130 | */ 131 | public int getOccurrence() { 132 | return occurrence; 133 | } 134 | 135 | /** 136 | * Converts a {@link Date} to {@link Calendar}. 137 | */ 138 | Calendar dateToCalendar(Date date) { 139 | Calendar calendar = Calendar.getInstance(); 140 | calendar.setTime(date); 141 | return calendar; 142 | } 143 | 144 | /** 145 | * Checks if the trigger date is within the trigger before option, if present 146 | */ 147 | public boolean isWithinTriggerbefore(Calendar calendar) { 148 | // Return true, if there is no trigger before option, otherwise compare against it 149 | return !optionsTrigger.has("before") || calendar.getTimeInMillis() < optionsTrigger.getBefore(); 150 | } 151 | 152 | /** 153 | * Adds the amount of triggerUnit to the calendar. 154 | * @param calendar The calendar to manipulate. 155 | */ 156 | public void addInterval(Calendar calendar, String triggerUnit, int amount) { 157 | switch (triggerUnit) { 158 | case "second": 159 | calendar.add(Calendar.SECOND, amount); 160 | break; 161 | 162 | case "minute": 163 | calendar.add(Calendar.MINUTE, amount); 164 | break; 165 | 166 | case "hour": 167 | calendar.add(Calendar.HOUR_OF_DAY, amount); 168 | break; 169 | 170 | case "day": 171 | calendar.add(Calendar.DAY_OF_YEAR, amount); 172 | break; 173 | 174 | case "week": 175 | calendar.add(Calendar.WEEK_OF_YEAR, amount); 176 | break; 177 | 178 | case "month": 179 | calendar.add(Calendar.MONTH, amount); 180 | break; 181 | 182 | case "quarter": 183 | calendar.add(Calendar.MONTH, amount * 3); 184 | break; 185 | 186 | case "year": 187 | calendar.add(Calendar.YEAR, amount); 188 | break; 189 | 190 | default: 191 | throw new IllegalArgumentException("Unknown trigger unit: " + triggerUnit); 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /src/android/action/Action.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification.action; 23 | 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.os.Bundle; 27 | import android.os.Looper; 28 | import android.util.Log; 29 | import androidx.core.app.RemoteInput; 30 | 31 | import org.json.JSONArray; 32 | import org.json.JSONObject; 33 | import org.json.JSONException; 34 | 35 | import de.appplant.cordova.plugin.localnotification.LocalNotification; 36 | import de.appplant.cordova.plugin.localnotification.Notification; 37 | import de.appplant.cordova.plugin.localnotification.util.AssetUtil; 38 | 39 | /** 40 | * Holds the icon and title components that would be used in a 41 | * NotificationCompat.Action object. Does not include the PendingIntent so 42 | * that it may be generated each time the notification is built. Necessary to 43 | * compensate for missing functionality in the support library. 44 | */ 45 | public class Action { 46 | 47 | private static final String TAG = "Action"; 48 | 49 | // Key name for bundled extras 50 | public static final String EXTRA_ID = "NOTIFICATION_ACTION_ID"; 51 | 52 | private Context context; 53 | 54 | private JSONObject actionOptionsJSON; 55 | 56 | /** 57 | * Structure to encapsulate a named action that can be shown as part of 58 | * this notification. 59 | * 60 | * @param context The application context. 61 | * @param actionOptionsJSON The action options. 62 | */ 63 | public Action(Context context, JSONObject actionOptionsJSON) { 64 | this.context = context; 65 | this.actionOptionsJSON = actionOptionsJSON; 66 | } 67 | 68 | /** 69 | * Gets the ID for the action. 70 | */ 71 | public String getId() { 72 | return actionOptionsJSON.optString("id", getTitle()); 73 | } 74 | 75 | public String getType() { 76 | return actionOptionsJSON.optString("type", "button"); 77 | } 78 | 79 | /** 80 | * Gets the Title for the action. 81 | */ 82 | public String getTitle() { 83 | return actionOptionsJSON.optString("title", "unknown"); 84 | } 85 | 86 | /** 87 | * Gets the icon for the action. Since Android 7 (Nougat) icons for actions 88 | * are not shown anymore. They would only be used on Android Wear. 89 | * See: https://android-developers.googleblog.com/2016/06/notifications-in-android-n.html 90 | */ 91 | public int getIcon() { 92 | String iconPath = actionOptionsJSON.optString("icon"); 93 | 94 | // Get icon from the app resources or system resources 95 | int resId = new AssetUtil(context).getResourceId(iconPath, AssetUtil.RESOURCE_TYPE_DRAWABLE); 96 | 97 | // Fallback, nothing found 98 | if (resId == 0) resId = android.R.drawable.screen_background_dark; 99 | 100 | return resId; 101 | } 102 | 103 | /** 104 | * If the app shpould be launched when the action is clicked. 105 | * Default is false. 106 | */ 107 | public boolean isLaunch() { 108 | return actionOptionsJSON.optBoolean("launch", false); 109 | } 110 | 111 | /** 112 | * Returns true if the action is of type input. 113 | */ 114 | public boolean isInput() { 115 | return getType().equals("input"); 116 | } 117 | 118 | /** 119 | * Gets the input config in case of the action is of type input. 120 | */ 121 | public RemoteInput buildRemoteInput() { 122 | return new RemoteInput.Builder(getId()) 123 | .setLabel(actionOptionsJSON.optString("emptyText")) 124 | // Specifies whether the user can provide arbitrary text values 125 | // The default is true. If you specify false, you must either provide a non-null and 126 | // non-empty array to setChoices, or enable a data result in setAllowDataType. 127 | // Otherwise an IllegalArgumentException is thrown 128 | .setAllowFreeFormInput(actionOptionsJSON.optBoolean("editable", true)) 129 | .setChoices(getChoices()) 130 | .build(); 131 | } 132 | 133 | /** 134 | * List of possible choices for input actions. 135 | */ 136 | private String[] getChoices() { 137 | JSONArray opts = actionOptionsJSON.optJSONArray("choices"); 138 | 139 | if (opts == null) 140 | return null; 141 | 142 | String[] choices = new String[opts.length()]; 143 | 144 | for (int i = 0; i < choices.length; i++) { 145 | choices[i] = opts.optString(i); 146 | } 147 | 148 | return choices; 149 | } 150 | 151 | public void handleClick(Intent intent, Notification notification) { 152 | Log.d(TAG, "Handle action click, options=" + actionOptionsJSON); 153 | 154 | // Fire action click event to JS 155 | LocalNotification.fireEvent( 156 | getId(), 157 | notification, 158 | // Get input data for action, if it is an input action 159 | getRemoteInputData(intent)); 160 | 161 | // Clear notification from statusbar if it should not be ongoing 162 | // This will also remove the notification from the SharedPreferences 163 | // if it is the last one 164 | if (!notification.getOptions().isAndroidOngoing()) { 165 | // A clear does not work on notifications with input fields: 166 | // https://stackoverflow.com/questions/54219914/cancel-notification-with-remoteinput-not-working 167 | // Google recommends after a reply to update the notificiation without the input action: 168 | // https://developer.android.com/develop/ui/views/notifications/build-notification#retrieve-user-reply 169 | // We update the notification without actions and cancel it after a timeout 170 | if (isInput()) { 171 | try { 172 | notification.update(new JSONObject("{\"actions\": null}")); 173 | // Clear the notification after a delay, because the update needs a little bit time to complete 174 | // To be compatible with Android 7, this is done with a Handler 175 | // Since Android 8, this could be handled with the property androidTimeoutAfter 176 | new android.os.Handler(Looper.getMainLooper()).postDelayed( 177 | new Runnable() { 178 | public void run() { 179 | notification.clear(); 180 | } 181 | }, 182 | 500); 183 | } catch (JSONException e) { 184 | Log.e(TAG, "Failed to update notification", e); 185 | } 186 | } else { 187 | notification.clear(); 188 | } 189 | } 190 | 191 | // Launch the app if required 192 | if (isLaunch()) LocalNotification.launchApp(context); 193 | } 194 | 195 | /** 196 | * Gets the input data for an action, if available. 197 | * @param intent The received intent. 198 | * @param actionId The action where to look for. 199 | */ 200 | private JSONObject getRemoteInputData(Intent intent) { 201 | Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); 202 | if (remoteInput == null) return null; 203 | 204 | try { 205 | JSONObject data = new JSONObject(); 206 | data.put("text", remoteInput.getCharSequence(getId()).toString()); 207 | return data; 208 | } catch (JSONException jsonException) { 209 | Log.e(TAG, "Failed to build remote input JSON", jsonException); 210 | return null; 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /src/android/trigger/TriggerHandlerEvery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | package de.appplant.cordova.plugin.localnotification.trigger; 23 | 24 | import android.util.Log; 25 | import org.json.JSONObject; 26 | import java.util.Calendar; 27 | import java.util.Date; 28 | 29 | import de.appplant.cordova.plugin.localnotification.Options; 30 | import de.appplant.cordova.plugin.localnotification.OptionsTrigger; 31 | 32 | public class TriggerHandlerEvery extends TriggerHandler { 33 | 34 | public static final String TAG = "TriggerHandlerEvery"; 35 | 36 | /** Is null if trigger.every is a JSONObject */ 37 | private String triggerEveryString; 38 | 39 | /** Is null if trigger.every is a String */ 40 | private JSONObject triggerEveryJSONObject; 41 | 42 | /** 43 | * Example: 44 | * trigger: { every: 'day', count: 5 } 45 | * trigger every: { minute: 10, hour: 9, day: 27, month: 10 } 46 | */ 47 | public TriggerHandlerEvery(Options options) { 48 | super(options); 49 | // trigger.every can be String or JSONObject, only one of them 50 | // will be set 51 | this.triggerEveryString = optionsTrigger.getEveryAsString(); 52 | this.triggerEveryJSONObject = optionsTrigger.getEveryAsJSONObject(); 53 | 54 | // Change base date if firstAt or after is set 55 | if (optionsTrigger.has("firstAt")) { 56 | this.baseDate = new Date(optionsTrigger.getFirstAt()); 57 | 58 | } else if (optionsTrigger.has("after")) { 59 | this.baseDate = new Date(optionsTrigger.getAfter()); 60 | } 61 | } 62 | 63 | public boolean isLastOccurrence() { 64 | // Check if trigger.count is exceeded if it is set 65 | return optionsTrigger.has("count") && occurrence >= optionsTrigger.getCount(); 66 | } 67 | 68 | /** 69 | * Calculates the next trigger. 70 | * @param baseCalendar The base calendar from where to calculate the next trigger. 71 | */ 72 | public Date calculateNextTrigger(Calendar baseCalendar) { 73 | // All occurrences are done 74 | if (isLastOccurrence()) return null; 75 | 76 | Calendar nextCalendar = (Calendar) baseCalendar.clone(); 77 | 78 | // trigger: { every: 'day', count: 5 } 79 | if (triggerEveryString != null) { 80 | try { 81 | addInterval(nextCalendar, triggerEveryString, 1); 82 | } catch (IllegalArgumentException exception) { 83 | Log.e(TAG, "Error calculating next trigger, trigger unit is wrong: " + exception.getMessage()); 84 | return null; 85 | } 86 | 87 | // trigger every: { minute: 10, hour: 9, day: 27, month: 10 } 88 | } else if (triggerEveryJSONObject != null) { 89 | // Set calendar to trigger.every values like minute: 20, hour: 9, etc. 90 | // Returns the next higher Calendar field for calculating the next trigger 91 | // If 0 is returned the options are empty or wrong 92 | int nextTriggerCalendarFieldToIncrease = setEveryValues(nextCalendar); 93 | 94 | // Nothing should be increased, options are empty or wrong 95 | if (nextTriggerCalendarFieldToIncrease == 0) return null; 96 | 97 | // If the next trigger is in the past or equal, increase the calendar 98 | // The trigger could be set by trigger.every to the past. 99 | // For e.g., if the current time is 9:30 and every: {minute: 10} is set, the trigger 100 | // would be set to 9:10. To get a next trigger, the hour have to be increased by 1 to 10:10. 101 | if (nextCalendar.compareTo(baseCalendar) <= 0) { 102 | nextCalendar.add(nextTriggerCalendarFieldToIncrease, 1); 103 | // Correct trigger after incrementing it 104 | // Example: If weekday was set to monday and a year was added, 105 | // the weekday could be changed to another day, set to monday again 106 | setEveryValues(nextCalendar); 107 | } 108 | } 109 | 110 | // Check if the trigger is within the before option 111 | if (!isWithinTriggerbefore(nextCalendar)) return null; 112 | 113 | return nextCalendar.getTime(); 114 | } 115 | 116 | /** 117 | * Set trigger.every values like { month: 10, day: 27, ...} in the given calendar. 118 | * @param calendar 119 | * @return The next higher {@link Calendar} field that has to be increase from the highest trigger.every option 120 | * For e.g. returns {@link Calendar.HOUR} when maximum minute is set, 121 | * or {@link Calendar.DAY_OF_YEAR} when maximum hour is set. 122 | * Returns 0 if no or wrong trigger.every values are set. 123 | */ 124 | private int setEveryValues(Calendar calendar) { 125 | int nextTriggerCalendarFieldToIncrease = 0; 126 | 127 | // Set second to 0 128 | calendar.set(Calendar.SECOND, 0); 129 | 130 | // Set minute from options in next calendar 131 | if (triggerEveryJSONObject.has("minute")) { 132 | calendar.set(Calendar.MINUTE, triggerEveryJSONObject.optInt("minute")); 133 | // One hour has to be added for the next trigger 134 | nextTriggerCalendarFieldToIncrease = Calendar.HOUR; 135 | } 136 | 137 | // Set hour from options in next calendar 138 | if (triggerEveryJSONObject.has("hour")) { 139 | calendar.set(Calendar.HOUR_OF_DAY, triggerEveryJSONObject.optInt("hour")); 140 | resetTimeIfNotSetByTrigger(calendar); 141 | // One day has to be added for the next trigger 142 | nextTriggerCalendarFieldToIncrease = Calendar.DAY_OF_YEAR; 143 | } 144 | 145 | // Set day from options in next calendar 146 | if (triggerEveryJSONObject.has("day")) { 147 | calendar.set(Calendar.DAY_OF_MONTH, triggerEveryJSONObject.optInt("day")); 148 | resetTimeIfNotSetByTrigger(calendar); 149 | // One month has to be added for the next trigger 150 | nextTriggerCalendarFieldToIncrease = Calendar.MONTH; 151 | } 152 | 153 | // Set weekday (day of week) from options in next calendar (1 = Monday, 7 = Sunday) 154 | if (triggerEveryJSONObject.has("weekday")) { 155 | // Calendar.MONDAY is 2, so we have to add 1 to the weekday 156 | calendar.set(Calendar.DAY_OF_WEEK, 1 + triggerEveryJSONObject.optInt("weekday")); 157 | 158 | resetTimeIfNotSetByTrigger(calendar); 159 | 160 | // One week has to be added for the next trigger 161 | nextTriggerCalendarFieldToIncrease = Calendar.WEEK_OF_YEAR; 162 | } 163 | 164 | // Set weekOfMonth from options in next calendar 165 | if (triggerEveryJSONObject.has("weekOfMonth")) { 166 | 167 | int weekOfMonth = triggerEveryJSONObject.optInt("weekOfMonth"); 168 | calendar.set(Calendar.WEEK_OF_MONTH, weekOfMonth); 169 | 170 | // Reset hour/minute 171 | resetTimeIfNotSetByTrigger(calendar); 172 | resetWeekdayIfNotSetByTrigger(calendar); 173 | 174 | // If the week of month is the first week, set day of month to 1 175 | if (weekOfMonth == 1) { 176 | calendar.set(Calendar.DAY_OF_MONTH, 1); 177 | // Correct weekday if it is set, but prevent jumping to the last month 178 | setWeekdayIfInFuture(calendar); 179 | } 180 | 181 | // One month has to be added for the next trigger 182 | nextTriggerCalendarFieldToIncrease = Calendar.MONTH; 183 | } 184 | 185 | // Set week of year from options in next calendar 186 | if (triggerEveryJSONObject.has("week")) { 187 | int weekOfYear = triggerEveryJSONObject.optInt("week"); 188 | calendar.set(Calendar.WEEK_OF_YEAR, weekOfYear); 189 | 190 | resetTimeIfNotSetByTrigger(calendar); 191 | resetWeekdayIfNotSetByTrigger(calendar); 192 | 193 | // If the week of year is the first week, set day of year to 1 194 | if (weekOfYear == 1) { 195 | calendar.set(Calendar.DAY_OF_YEAR, 1); 196 | // Correct weekday if it is set, but prevent jumping to the last year 197 | setWeekdayIfInFuture(calendar); 198 | } 199 | 200 | nextTriggerCalendarFieldToIncrease = Calendar.YEAR; 201 | } 202 | 203 | // Set month from options in next calendar 204 | if (triggerEveryJSONObject.has("month")) { 205 | // The first month is 0 for Calendar 206 | calendar.set(Calendar.MONTH, triggerEveryJSONObject.optInt("month") - 1); 207 | 208 | resetTimeIfNotSetByTrigger(calendar); 209 | resetDayIfNotSetByTrigger(calendar); 210 | 211 | // One year has to be added for the next trigger 212 | nextTriggerCalendarFieldToIncrease = Calendar.YEAR; 213 | } 214 | 215 | return nextTriggerCalendarFieldToIncrease; 216 | } 217 | 218 | /** 219 | * Set minute/hour to 0 if not set by trigger 220 | **/ 221 | private void resetTimeIfNotSetByTrigger(Calendar calendar) { 222 | // Reset minute if not set 223 | if (!triggerEveryJSONObject.has("minute")) calendar.set(Calendar.MINUTE, 0); 224 | // Reset hour if not set 225 | if (!triggerEveryJSONObject.has("hour")) calendar.set(Calendar.HOUR_OF_DAY, 0); 226 | } 227 | 228 | /** 229 | * Set day to 1 if not set by trigger 230 | * @param calendar 231 | */ 232 | private void resetDayIfNotSetByTrigger(Calendar calendar) { 233 | if (triggerEveryJSONObject.has("day") || triggerEveryJSONObject.has("weekday")) return; 234 | calendar.set(Calendar.DAY_OF_MONTH, 1); 235 | } 236 | 237 | private void resetWeekdayIfNotSetByTrigger(Calendar calendar) { 238 | if (triggerEveryJSONObject.has("weekday")) return; 239 | calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); 240 | } 241 | 242 | /** 243 | * Sets a configured weekday only if it is in the future. This should prevent the calendar 244 | * jump to the last month or year, if the date was set to the first of a month or year. 245 | */ 246 | private void setWeekdayIfInFuture(Calendar calendar) { 247 | // Not set by options 248 | if (!triggerEveryJSONObject.has("weekday")) return; 249 | 250 | // Only set if in future. For weekday 1 is Monday and for Calendar it is 2, 251 | // so we have to consider it 252 | if (calendar.get(Calendar.DAY_OF_WEEK) < 1 + triggerEveryJSONObject.optInt("weekday")) { 253 | calendar.set(Calendar.DAY_OF_WEEK, 1 + triggerEveryJSONObject.optInt("weekday")); 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /src/ios/UNUserNotificationCenter+APPLocalNotification.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "UNUserNotificationCenter+APPLocalNotification.h" 23 | #import "UNNotificationRequest+APPLocalNotification.h" 24 | 25 | @import UserNotifications; 26 | 27 | NSString * const kAPPGeneralCategory = @"GENERAL"; 28 | 29 | @implementation UNUserNotificationCenter (APPLocalNotification) 30 | 31 | #pragma mark - 32 | #pragma mark NotificationCategory 33 | 34 | /** 35 | * Register general notification category to listen for dismiss actions. 36 | */ 37 | - (void) registerGeneralNotificationCategory 38 | { 39 | UNNotificationCategory* category = [UNNotificationCategory categoryWithIdentifier:kAPPGeneralCategory 40 | actions:@[] 41 | intentIdentifiers:@[] 42 | options:UNNotificationCategoryOptionCustomDismissAction]; 43 | 44 | [self setNotificationCategories:[NSSet setWithObject:category]]; 45 | } 46 | 47 | /** 48 | * Add the specified category to the list of categories. 49 | * @param addCategory The category to add. 50 | */ 51 | - (void) addActionGroup:(UNNotificationCategory*)addCategory 52 | { 53 | if (!addCategory) return; 54 | 55 | [self getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { 56 | NSMutableSet* mutableCategories = [NSMutableSet setWithSet:categories]; 57 | 58 | // Remove category first, if it already exists 59 | for (UNNotificationCategory* category in mutableCategories) 60 | { 61 | if ([addCategory.identifier isEqualToString:category.identifier]) { 62 | [mutableCategories removeObject:category]; 63 | break; 64 | } 65 | } 66 | 67 | NSLog(@"Adding action category: %@", addCategory.identifier); 68 | [mutableCategories addObject:addCategory]; 69 | [self setNotificationCategories:mutableCategories]; 70 | }]; 71 | } 72 | 73 | /** 74 | * Remove if the specified category does exist. 75 | * @param removeCategoryIdentifier The category id to remove. 76 | */ 77 | - (void) removeActionGroup:(NSString*)removeCategoryIdentifier 78 | { 79 | [self getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { 80 | NSMutableSet* mutableCategories = [NSMutableSet setWithSet:categories]; 81 | 82 | for (UNNotificationCategory* category in mutableCategories) 83 | { 84 | if ([category.identifier isEqualToString:removeCategoryIdentifier]) { 85 | [mutableCategories removeObject:category]; 86 | break; 87 | } 88 | } 89 | 90 | [self setNotificationCategories:mutableCategories]; 91 | }]; 92 | } 93 | 94 | /** 95 | * Check if the specified category does exist. 96 | * @param findCategoryIdentifier The category id to check for. 97 | * @return [ BOOL ] 98 | */ 99 | - (BOOL) hasActionGroup:(NSString*)findCategoryIdentifier 100 | { 101 | dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 102 | __block BOOL found = NO; 103 | 104 | [self getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { 105 | for (UNNotificationCategory* category in categories) 106 | { 107 | if ([category.identifier isEqualToString:findCategoryIdentifier]) { 108 | found = YES; 109 | dispatch_semaphore_signal(semaphore); 110 | break; 111 | } 112 | } 113 | }]; 114 | 115 | dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 116 | 117 | return found; 118 | } 119 | 120 | #pragma mark - 121 | #pragma mark LocalNotifications 122 | 123 | /** 124 | * List of all delivered or still pending notifications. 125 | * @return [ NSArray* ] 126 | */ 127 | - (NSArray*) getNotifications 128 | { 129 | NSMutableArray* notifications = [[NSMutableArray alloc] init]; 130 | [notifications addObjectsFromArray:[self getPendingNotifications]]; 131 | [notifications addObjectsFromArray:[self getDeliveredNotifications]]; 132 | return notifications; 133 | } 134 | 135 | /** 136 | * List of all triggered notifications. 137 | * @return [ NSArray* ] 138 | */ 139 | - (NSArray*) getDeliveredNotifications 140 | { 141 | NSMutableArray* notifications = [[NSMutableArray alloc] init]; 142 | dispatch_semaphore_t sema = dispatch_semaphore_create(0); 143 | 144 | [self getDeliveredNotificationsWithCompletionHandler:^(NSArray *delivered) { 145 | for (UNNotification* notification in delivered) 146 | { 147 | [notifications addObject:notification.request]; 148 | } 149 | dispatch_semaphore_signal(sema); 150 | }]; 151 | 152 | dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 153 | 154 | return notifications; 155 | } 156 | 157 | /** 158 | * List of all pending notifications. 159 | * @return [ NSArray* ] 160 | */ 161 | - (NSArray*) getPendingNotifications 162 | { 163 | NSMutableArray* notificationsRequests = [[NSMutableArray alloc] init]; 164 | dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 165 | 166 | [self getPendingNotificationRequestsWithCompletionHandler:^(NSArray *requests) { 167 | [notificationsRequests addObjectsFromArray:requests]; 168 | dispatch_semaphore_signal(semaphore); 169 | }]; 170 | 171 | dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 172 | 173 | return notificationsRequests; 174 | } 175 | 176 | /** 177 | * List of all notifications from given type. 178 | * @param type Notification life cycle type. 179 | * @return [ NSArray* ] 180 | */ 181 | - (NSArray*) getNotificationsByType:(APPNotificationType)type 182 | { 183 | switch (type) { 184 | case NotifcationTypeScheduled: 185 | return [self getPendingNotifications]; 186 | 187 | case NotifcationTypeTriggered: 188 | return [self getDeliveredNotifications]; 189 | 190 | default: 191 | return [self getNotifications]; 192 | } 193 | } 194 | 195 | /** 196 | * List of all local notifications IDs. 197 | * @return [ NSArray* ] 198 | */ 199 | - (NSArray*) getNotificationIds 200 | { 201 | NSMutableArray* ids = [[NSMutableArray alloc] init]; 202 | 203 | for (UNNotificationRequest* notification in [self getNotifications]) 204 | { 205 | [ids addObject:notification.options.id]; 206 | } 207 | 208 | return ids; 209 | } 210 | 211 | /** 212 | * List of all notifications IDs from given type. 213 | * @param type Notification life cycle type. 214 | * @return [ NSArray* ] 215 | */ 216 | - (NSArray*) getNotificationIdsByType:(APPNotificationType)type 217 | { 218 | NSMutableArray* ids = [[NSMutableArray alloc] init]; 219 | 220 | for (UNNotificationRequest* notification in [self getNotificationsByType:type]) 221 | { 222 | [ids addObject:notification.options.id]; 223 | } 224 | 225 | return ids; 226 | } 227 | 228 | /** 229 | * Find notification by ID. 230 | * @param findNotificationId Notification ID 231 | * @return [ UNNotificationRequest* ] 232 | */ 233 | - (UNNotificationRequest*) getNotificationWithId:(NSNumber*)findNotificationId 234 | { 235 | for (UNNotificationRequest* notification in [self getNotifications]) 236 | { 237 | NSString* notificationId = [NSString stringWithFormat:@"%@", notification.options.id]; 238 | 239 | if ([notificationId isEqualToString:[findNotificationId stringValue]]) { 240 | return notification; 241 | } 242 | } 243 | 244 | return NULL; 245 | } 246 | 247 | /** 248 | * Find notification type by ID. 249 | * @param notificationId The ID of the notification. 250 | * @return [ APPNotificationType ] 251 | */ 252 | - (APPNotificationType) getTypeOfNotificationWithId:(NSNumber*)notificationId 253 | { 254 | // Check if triggered 255 | if ([[self getNotificationIdsByType:NotifcationTypeTriggered] containsObject:notificationId]) return NotifcationTypeTriggered; 256 | 257 | // Check if scheduled 258 | if ([[self getNotificationIdsByType:NotifcationTypeScheduled] containsObject:notificationId]) return NotifcationTypeScheduled; 259 | 260 | return NotifcationTypeUnknown; 261 | } 262 | 263 | /** 264 | * List of properties from all notifications. 265 | * @return [ NSArray* ] 266 | */ 267 | - (NSArray*) getNotificationOptions 268 | { 269 | return [self getNotificationOptionsByType:NotifcationTypeAll]; 270 | } 271 | 272 | /** 273 | * List of properties from all notifications of given type. 274 | * @param type Notification life cycle type. 275 | * @return [ NSArray* ] 276 | */ 277 | - (NSArray*) getNotificationOptionsByType:(APPNotificationType)type 278 | { 279 | NSMutableArray* options = [[NSMutableArray alloc] init]; 280 | 281 | for (UNNotificationRequest* notification in [self getNotificationsByType:type]) 282 | { 283 | [options addObject:notification.options.userInfo]; 284 | } 285 | 286 | return options; 287 | } 288 | 289 | /** 290 | * List of properties from given local notifications. 291 | * @param notificationsIds The ids of the notifications to find. 292 | * @return [ NSArray* ] 293 | */ 294 | - (NSArray*) getNotificationOptionsById:(NSArray*)notificationsIds 295 | { 296 | NSMutableArray* options = [[NSMutableArray alloc] init]; 297 | 298 | for (UNNotificationRequest* notification in [self getNotifications]) 299 | { 300 | if ([notificationsIds containsObject:notification.options.id]) { 301 | [options addObject:notification.options.userInfo]; 302 | } 303 | } 304 | 305 | return options; 306 | } 307 | 308 | /** 309 | * Clear all notfications. 310 | */ 311 | - (void) clearNotifications 312 | { 313 | [self removeAllDeliveredNotifications]; 314 | } 315 | 316 | /** 317 | * Clear Specified notfication. 318 | */ 319 | - (void) clearNotification:(UNNotificationRequest*)notificationRequest 320 | { 321 | [self removeDeliveredNotificationsWithIdentifiers:@[notificationRequest.identifier]]; 322 | } 323 | 324 | /** 325 | * Cancel all notfications. 326 | */ 327 | - (void) cancelNotifications 328 | { 329 | [self removeAllPendingNotificationRequests]; 330 | [self removeAllDeliveredNotifications]; 331 | } 332 | 333 | /** 334 | * Cancel specified notfication. 335 | * @param notificationRequest The notification object. 336 | */ 337 | - (void) cancelNotification:(UNNotificationRequest*)notificationRequest 338 | { 339 | NSArray* ids = @[notificationRequest.identifier]; 340 | [self removeDeliveredNotificationsWithIdentifiers:ids]; 341 | [self removePendingNotificationRequestsWithIdentifiers:ids]; 342 | } 343 | 344 | @end 345 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013 appPlant GmbH 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 29 | 30 | LocalNotification 31 | 32 | Schedules and queries for local notifications 33 | 34 | https://github.com/katzer/cordova-plugin-local-notifications.git 35 | 36 | notification, local notification, user notification 37 | 38 | Apache 2.0 39 | 40 | Sebastián Katzer and Manuel Beck 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 116 | 121 | 124 | 125 | 126 | 137 | 142 | 143 | 144 | 147 | 148 | 149 | 152 | 153 | 156 | 157 | 160 | 161 | 162 | 165 | 166 | 167 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 192 | 193 | 196 | 197 | 200 | 201 | 204 | 205 | 208 | 209 | 212 | 213 | 216 | 217 | 220 | 221 | 224 | 225 | 228 | 229 | 232 | 233 | 236 | 237 | 240 | 241 | 244 | 245 | 248 | 249 | 252 | 253 | 256 | 257 | 260 | 261 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /src/android/Manager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * Copyright (c) Manuel Beck 2024 6 | * 7 | * This file contains Original Code and/or Modifications of Original Code 8 | * as defined in and that are subject to the Apache License 9 | * Version 2.0 (the 'License'). You may not use this file except in 10 | * compliance with the License. Please obtain a copy of the License at 11 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 12 | * file. 13 | * 14 | * The Original Code and all software distributed under the License are 15 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 16 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 17 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 19 | * Please see the License for the specific language governing rights and 20 | * limitations under the License. 21 | */ 22 | 23 | // codebeat:disable[TOO_MANY_FUNCTIONS] 24 | 25 | package de.appplant.cordova.plugin.localnotification; 26 | 27 | import android.annotation.SuppressLint; 28 | import android.app.AlarmManager; 29 | import android.app.NotificationChannel; 30 | import androidx.core.app.NotificationManagerCompat; 31 | import android.content.Context; 32 | import android.content.Intent; 33 | import android.content.SharedPreferences; 34 | import android.service.notification.StatusBarNotification; 35 | import android.media.AudioAttributes; 36 | import android.net.Uri; 37 | import android.util.Log; 38 | import android.os.PowerManager; 39 | 40 | import org.json.JSONException; 41 | import org.json.JSONObject; 42 | 43 | import java.util.ArrayList; 44 | import java.util.Calendar; 45 | import java.util.List; 46 | import java.util.Random; 47 | import java.util.Set; 48 | 49 | import static android.os.Build.VERSION.SDK_INT; 50 | import static android.os.Build.VERSION_CODES.O; 51 | import static android.os.Build.VERSION_CODES.P; 52 | import static android.os.Build.VERSION_CODES.S; 53 | import static de.appplant.cordova.plugin.localnotification.Notification.Type.TRIGGERED; 54 | import de.appplant.cordova.plugin.localnotification.util.AssetUtil; 55 | 56 | /** 57 | * Central way to access all or single local notifications set by specific 58 | * state like triggered or scheduled. Offers shortcut ways to schedule, 59 | * cancel or clear local notifications. 60 | */ 61 | public final class Manager { 62 | // Key for shared preferences 63 | static final String PREF_KEY_ID = "NOTIFICATION_ID"; 64 | 65 | public static final String TAG = "Manager"; 66 | 67 | private Context context; 68 | 69 | private static final Random randomGenerator = new Random(); 70 | 71 | public Manager(Context context) { 72 | this.context = context; 73 | } 74 | 75 | /** 76 | * Check if the setting to schedule exact alarms is enabled. 77 | */ 78 | public static boolean canScheduleExactAlarms(Context context) { 79 | // Supported since Android 12 80 | if (SDK_INT < S) return true; 81 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 82 | return alarmManager.canScheduleExactAlarms(); 83 | } 84 | 85 | public static NotificationChannel getChannel(Context context, Options options) { 86 | // Channels are only supported since Android 8 87 | if (SDK_INT < O) return null; 88 | return NotificationManagerCompat.from(context).getNotificationChannel(options.getAndroidChannelId()); 89 | } 90 | 91 | /** 92 | * Create Notification channel with options 93 | * @param options Set of channel options. 94 | * 95 | */ 96 | public static void createChannel(Context context, Options options) { 97 | // Channels are only supported since Android 8 98 | if (SDK_INT < O) return; 99 | 100 | // Check if channel exists 101 | NotificationChannel channel = getChannel(context, options); 102 | 103 | // Channel already created 104 | if (channel != null) return; 105 | 106 | Log.d(TAG, "Create channel" + 107 | ", id=" + options.getAndroidChannelId() + 108 | ", name=" + options.getAndroidChannelName() + 109 | ", options=" + options); 110 | 111 | // Create new channel 112 | channel = new NotificationChannel( 113 | options.getAndroidChannelId(), 114 | options.getAndroidChannelName(), options.getAndroidChannelImportance()); 115 | 116 | channel.setDescription(options.getAndroidChannelDescription()); 117 | channel.enableVibration(options.isAndroidChannelEnableVibration()); 118 | channel.enableLights(options.getAndroidChannelEnableLights()); 119 | 120 | Uri soundUri = options.getSoundUri(); 121 | Log.d(TAG, "sound uri: " + soundUri); 122 | 123 | // Grant permission to the system to play the sound, needed only in Android 8 124 | if (soundUri != Uri.EMPTY && SDK_INT < P) { 125 | grantUriPermission(context, soundUri); 126 | } 127 | 128 | // If options.getSoundUri() is Uri.EMPTY, an empty sound will be set, which means no sound 129 | channel.setSound(soundUri, new AudioAttributes.Builder().setUsage(options.getSoundUsage()).build()); 130 | 131 | NotificationManagerCompat.from(context).createNotificationChannel(channel); 132 | } 133 | 134 | /** 135 | * Deletes a notification channel by an id. If you create a new channel with this same id, 136 | * the deleted channel will be un-deleted with all of the same settings it had before it was deleted. 137 | * @param channelId Like "my_channel_01" 138 | */ 139 | public void deleteChannel(String channelId) { 140 | // Channels are supported since Android 8 141 | if (SDK_INT < O) return; 142 | 143 | Log.d(TAG, "Delete channel, id=" + channelId); 144 | 145 | // Cancel all notifications regarding this channel 146 | for (Notification notification : new Manager(context).getNotificationsFromSharedPreferences()) { 147 | if (notification.getOptions().getAndroidChannelId().equals(channelId)) { 148 | notification.cancel(); 149 | } 150 | } 151 | 152 | NotificationManagerCompat.from(context).deleteNotificationChannel(channelId); 153 | } 154 | 155 | /** 156 | * Update local notification specified by ID. 157 | * @param notificationId The ID of the notification. 158 | * @param updates JSON object with notification options. 159 | */ 160 | public Notification update(int notificationId, JSONObject updates) { 161 | Notification notification = Notification.getFromSharedPreferences(context, notificationId); 162 | if (notification == null) return null; 163 | 164 | notification.update(updates); 165 | 166 | return notification; 167 | } 168 | 169 | /** 170 | * Clear all local notifications. 171 | */ 172 | public void clearAll() { 173 | for (Notification notification : getByType(TRIGGERED)) { 174 | notification.clear(); 175 | } 176 | 177 | NotificationManagerCompat.from(context).cancelAll(); 178 | } 179 | 180 | /** 181 | * Cancel all local notifications. 182 | */ 183 | public void cancelAll() { 184 | for (Notification notification : getNotificationsFromSharedPreferences()) { 185 | notification.cancel(); 186 | } 187 | 188 | NotificationManagerCompat.from(context).cancelAll(); 189 | } 190 | 191 | /** 192 | * Get saved notification ids 193 | */ 194 | public List getNotificationIds() { 195 | List notificationIds = new ArrayList(); 196 | 197 | // Options are stored by the notification id in the shared preferences 198 | for (String key : getSharedPreferences().getAll().keySet()) { 199 | // Skip keys with underscore for e.g. _occurrence 200 | if (key.contains("_")) continue; 201 | 202 | try { 203 | notificationIds.add(Integer.parseInt(key)); 204 | } catch (NumberFormatException exception) { 205 | exception.printStackTrace(); 206 | } 207 | } 208 | 209 | return notificationIds; 210 | } 211 | 212 | /** 213 | * Get saved notification ids for a given type. 214 | * @param type The notification life cycle type 215 | */ 216 | public List getNotificationIdsByType(Notification.Type type) { 217 | // Returns triggered and scheduled notifications 218 | if (type == Notification.Type.ALL) return getNotificationIds(); 219 | 220 | List activeIds = new ArrayList(); 221 | 222 | for (StatusBarNotification statusBarNotification : getActiveNotifications()) { 223 | activeIds.add(statusBarNotification.getId()); 224 | } 225 | 226 | if (type == TRIGGERED) return activeIds; 227 | 228 | // Return scheduled notifications 229 | List notificationIds = getNotificationIds(); 230 | // Remove triggered notifications 231 | notificationIds.removeAll(activeIds); 232 | 233 | return notificationIds; 234 | } 235 | 236 | /** 237 | * List of all local notification. 238 | */ 239 | public List getNotificationsFromSharedPreferences() { 240 | return getNotificationsFromSharedPreferences(getNotificationIds()); 241 | } 242 | 243 | /** 244 | * List of local notifications with matching ID. 245 | */ 246 | public List getNotificationsFromSharedPreferences(List notificationIds) { 247 | List notifications = new ArrayList(); 248 | 249 | for (int notificationId : notificationIds) { 250 | Notification notification = Notification.getFromSharedPreferences(context, notificationId); 251 | if (notification != null) notifications.add(notification); 252 | } 253 | 254 | return notifications; 255 | } 256 | 257 | /** 258 | * List of local notifications from given type. 259 | * 260 | * @param type The notification life cycle type 261 | */ 262 | public List getByType(Notification.Type type) { 263 | return type == Notification.Type.ALL ? getNotificationsFromSharedPreferences() : getNotificationsFromSharedPreferences(getNotificationIdsByType(type)); 264 | } 265 | 266 | /** 267 | * Returns the active status bar notification with the specified notificationId. 268 | * If there is no active status bar notification, null will be returned. 269 | * @param notificationId 270 | */ 271 | StatusBarNotification getActiveNotification(int notificationId) { 272 | for (StatusBarNotification statusBarNotification : getActiveNotifications()) { 273 | if (statusBarNotification.getId() == notificationId) { 274 | return statusBarNotification; 275 | } 276 | } 277 | 278 | return null; 279 | } 280 | 281 | public SharedPreferences getSharedPreferences() { 282 | return getSharedPreferences(context); 283 | } 284 | 285 | /** 286 | * Shared private preferences for the application. 287 | */ 288 | public static SharedPreferences getSharedPreferences(Context context) { 289 | return context.getSharedPreferences(PREF_KEY_ID, Context.MODE_PRIVATE); 290 | } 291 | 292 | /** 293 | * Get all active status bar notifications. 294 | */ 295 | public List getActiveNotifications() { 296 | return NotificationManagerCompat.from(context).getActiveNotifications(); 297 | } 298 | 299 | /** 300 | * Wake up the screen and returns a WakeLock, which have to be release, after the work is done. 301 | * @return WakeLock, which have to be release, after the work is done. 302 | */ 303 | public static PowerManager.WakeLock wakeUpScreen(Context context) { 304 | PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 305 | PowerManager.WakeLock wakeLook = powerManager.newWakeLock( 306 | PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "LocalNotification"); 307 | wakeLook.acquire(); 308 | return wakeLook; 309 | } 310 | 311 | /** 312 | * In Android 7 and 8, the app will crash if an external process has no 313 | * permission to access content:// Uris, which are used for shared files in [App path]/files/shared_files. 314 | * This was fixed in Android 9. 315 | * See: https://stackoverflow.com/questions/39359465/android-7-0-notification-sound-from-file-provider-uri-not-playing 316 | */ 317 | public static void grantUriPermission(Context context, Uri uri) { 318 | context.grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); 319 | } 320 | 321 | /** 322 | * Gets a random request code between 1 and Integer.MAX_VALUE. 323 | */ 324 | public static int getRandomRequestCode() { 325 | return randomGenerator.nextInt(Integer.MAX_VALUE) + 1; 326 | } 327 | } -------------------------------------------------------------------------------- /src/android/util/AssetUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * Copyright (c) Manuel Beck 2024 6 | * 7 | * This file contains Original Code and/or Modifications of Original Code 8 | * as defined in and that are subject to the Apache License 9 | * Version 2.0 (the 'License'). You may not use this file except in 10 | * compliance with the License. Please obtain a copy of the License at 11 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 12 | * file. 13 | * 14 | * The Original Code and all software distributed under the License are 15 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 16 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 17 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 19 | * Please see the License for the specific language governing rights and 20 | * limitations under the License. 21 | */ 22 | 23 | package de.appplant.cordova.plugin.localnotification.util; 24 | 25 | import android.content.ContentResolver; 26 | import android.content.Context; 27 | import android.content.res.Resources; 28 | import androidx.appcompat.content.res.AppCompatResources; 29 | 30 | import android.graphics.Bitmap; 31 | import android.graphics.Canvas; 32 | import android.graphics.Color; 33 | import android.graphics.BitmapFactory; 34 | import android.graphics.PorterDuff; 35 | import android.graphics.PorterDuffXfermode; 36 | import android.graphics.Rect; 37 | import android.graphics.RectF; 38 | import android.graphics.Paint; 39 | import android.graphics.drawable.BitmapDrawable; 40 | import android.graphics.drawable.Drawable; 41 | import android.graphics.drawable.VectorDrawable; 42 | import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; 43 | 44 | import android.net.Uri; 45 | import android.util.Log; 46 | 47 | import java.io.File; 48 | import java.io.FileNotFoundException; 49 | import java.io.FileOutputStream; 50 | import java.io.IOException; 51 | import java.io.InputStream; 52 | 53 | /** 54 | * Util class to map unified asset URIs to native URIs. See {@link AssetUtil#getUri(String, int)}. 55 | */ 56 | public final class AssetUtil { 57 | 58 | public static final String TAG = "AssetUtil"; 59 | 60 | /** 61 | * Needed for access the resources and app directory. 62 | */ 63 | private final Context context; 64 | 65 | public static final int RESOURCE_TYPE_DRAWABLE = 0; 66 | public static final int RESOURCE_TYPE_RAW = 1; 67 | 68 | public AssetUtil(Context context) { 69 | this.context = context; 70 | } 71 | 72 | /** 73 | * The Uri for a path. 74 | * @param path The path to get the Uri for. 75 | * @param resourceType Only needed, if the path is a res:// path. Represents the type of the resource, 76 | * can be {@link AssetUtil#RESOURCE_TYPE_DRAWABLE} or {@link AssetUtil#RESOURCE_TYPE_RAW}. 77 | * @return The Uri for the path or {@link Uri.EMPTY} if the path is empty, does not exists or is not recognizeable. 78 | */ 79 | public Uri getUri(String path, int resourceType) { 80 | if (path == null || path.isEmpty()) return Uri.EMPTY; 81 | 82 | // Resource file from res directory 83 | if (path.startsWith("res:")) return getUriForResource(path, resourceType); 84 | 85 | // File from www folder 86 | if (path.startsWith("www") || path.startsWith("file://")) return getSharedUriForAssetFile(path); 87 | 88 | // Shared file in the shared_files directory 89 | if (path.startsWith("shared://")) { 90 | // Create content:// Uri 91 | return getSharedUri(new File(getSharedDirectory(), path.replace("shared://", ""))); 92 | } 93 | 94 | // Path not recognizeable 95 | Log.e(TAG, "Path not recognizeable: " + path); 96 | return Uri.EMPTY; 97 | } 98 | 99 | /** 100 | * Gets the Uri for a resource in the res directory. 101 | * @param resourcePath Path like res://mySound, res://myImage.png, etc. 102 | * @param resourceType Can be {@link AssetUtil#RESOURCE_TYPE_DRAWABLE} or {@link AssetUtil#RESOURCE_TYPE_RAW} 103 | * @return {@link Uri.EMPTY} if a resource could not be found. 104 | */ 105 | public Uri getUriForResource(String resourcePath, int resourceType) { 106 | // Get from app resources 107 | Resources resources = context.getResources(); 108 | int resourceId = getResourceId(resources, resourcePath, resourceType); 109 | 110 | // Get from system resources 111 | if (resourceId == 0) { 112 | resources = Resources.getSystem(); 113 | resourceId = getResourceId(resources, resourcePath, resourceType); 114 | } 115 | 116 | if (resourceId == 0) { 117 | Log.w(TAG, "Resource not found: " + resourcePath); 118 | return Uri.EMPTY; 119 | } 120 | 121 | // Will be something like: 122 | // App Resource: android.resource://com.example.app/raw/mySound 123 | return new Uri.Builder() 124 | // Scheme: android.resource 125 | .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 126 | // Authority: com.example.app (for the App) 127 | .authority(resources.getResourcePackageName(resourceId)) 128 | // Resource directory: raw 129 | .appendPath(resources.getResourceTypeName(resourceId)) 130 | // Resource name: mySound 131 | .appendPath(resources.getResourceEntryName(resourceId)) 132 | .build(); 133 | } 134 | 135 | /** 136 | * Get the resource Id for a given resourceType. Searches in the App resources first, then in the system resources. 137 | * @param resourceName Can also be a resource path like "res://mySound", "res://myImage.png", etc. 138 | * @param resourceType Can be {@link AssetUtil#RESOURCE_TYPE_DRAWABLE} or {@link AssetUtil#RESOURCE_TYPE_RAW} 139 | * @return The resource ID or 0 if not found. 140 | */ 141 | public int getResourceId(String resourceName, int resourceType) { 142 | // Get resource from App 143 | int resourceId = getResourceId(context.getResources(), resourceName, resourceType); 144 | 145 | // Get resource from system, if not found 146 | if (resourceId == 0) return getResourceId(Resources.getSystem(), resourceName, resourceType); 147 | 148 | return resourceId; 149 | } 150 | 151 | /** 152 | * Get the resource Id for a given resourceType. 153 | * @param resources The resources where to look for, can be {@link Context#getResources()} or {@link Resources#getSystem()} 154 | * @param resourceName Can also be a resource path like "res://mySound", "res://myImage.png", etc. 155 | * @param resourceType Can be {@link AssetUtil#RESOURCE_TYPE_DRAWABLE} or {@link AssetUtil#RESOURCE_TYPE_RAW} 156 | * @return The resource ID or 0 if not found. 157 | */ 158 | public int getResourceId(Resources resources, String resourceName, int resourceType) { 159 | if (resourceType == RESOURCE_TYPE_DRAWABLE) { 160 | // Try first in drawable 161 | int resourceId = getResourceId(resources, resourceName, "drawable"); 162 | 163 | // Try in mipmap if not found 164 | if (resourceId == 0) { 165 | resourceId = getResourceId(resources, resourceName, "mipmap"); 166 | } 167 | 168 | return resourceId; 169 | 170 | // Get sound, video, etc. 171 | } else if (resourceType == RESOURCE_TYPE_RAW) { 172 | return getResourceId(resources, resourceName, "raw"); 173 | } 174 | 175 | // Resource type unknown 176 | Log.e(TAG, "Unknown resource type: " + resourceType); 177 | return 0; 178 | } 179 | 180 | /** 181 | * Get the resource Id. Searches in a given resource directory and resources. 182 | * @param resources The resources where to look for, can be {@link Context#getResources()} or {@link Resources#getSystem()} 183 | * @param resourceName Can also be a resource path like "res://mySound", "res://myImage.png", etc. 184 | * @param resourceDirectory The directory of the resource, for e.g. "mipmap", "drawable", "raw", etc. 185 | * @return The resource ID or 0 if not found. 186 | */ 187 | public int getResourceId(Resources resources, String resourceName, String resourceDirectory) { 188 | return resources.getIdentifier(getResourceName(resourceName), resourceDirectory, getPackageName(resources)); 189 | } 190 | 191 | /** 192 | * Gets the resource name from the path. 193 | * @param resourcePath Resource path as string. 194 | */ 195 | public static String getResourceName(String resourcePath) { 196 | String resourceName = resourcePath; 197 | 198 | // Get the filename without the path 199 | if (resourceName.contains("/")) { 200 | resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1); 201 | } 202 | 203 | // Remove file extension 204 | if (resourceName.contains(".")) { 205 | resourceName = resourceName.substring(0, resourceName.lastIndexOf('.')); 206 | } 207 | 208 | return resourceName; 209 | } 210 | 211 | /** 212 | * Package name specified by the resource bundle. 213 | * @return "android" if system resources are used, otherwise the package name of the app. 214 | */ 215 | private String getPackageName(Resources resources) { 216 | return resources == Resources.getSystem() ? "android" : context.getPackageName(); 217 | } 218 | 219 | /** 220 | * Shared Uri for an asset file. 221 | * Copies the asset file to the shared directory [App path]/files/shared_files, to make it accessible 222 | * through a content:// Uri. 223 | * @param assetPath Path like www/myFile.png or file://myFile.png 224 | * @return content:// Uri pointing to the shared asset file in [App path]/files/shared_files. 225 | * E.g. content://com.example.app.localnotifications.provider/shared_files/www/myAssetFile.png 226 | */ 227 | private Uri getSharedUriForAssetFile(String assetPath) { 228 | // Change file:// to www folder 229 | assetPath = assetPath.replaceFirst("file://", "www/"); 230 | 231 | // Create all directories specified by the asset path 232 | File sharedDirectory = new File( 233 | getSharedDirectory(), 234 | // www/my/subfolder 235 | assetPath.substring(0, assetPath.lastIndexOf('/'))); 236 | 237 | // Create sub directories for the shared directory 238 | sharedDirectory.mkdirs(); 239 | 240 | // Get the asset file to copy to the shared directory 241 | String assetFilename = assetPath.substring(assetPath.lastIndexOf('/') + 1); 242 | File sharedAssetFile = new File(sharedDirectory, assetFilename); 243 | 244 | try { 245 | copyFile(context.getAssets().open(assetPath), new FileOutputStream(sharedAssetFile)); 246 | } catch (Exception exception) { 247 | Log.e(TAG, "File not found: " + assetPath, exception); 248 | return Uri.EMPTY; 249 | } 250 | 251 | return getSharedUri(sharedAssetFile); 252 | } 253 | 254 | /** 255 | * Get the content:// Uri for a shared file. 256 | * @param sharedFile The file to get the Uri from 257 | * @return E.g. content://com.example.app.localnotifications.provider/shared_files/mySharedFile.png 258 | * or Uri.EMPTY if the file is outside the paths supported by the provider. 259 | */ 260 | private Uri getSharedUri(File sharedFile) { 261 | try { 262 | return PluginFileProvider.getUriForFile(context, context.getPackageName() + ".localnotifications.provider", sharedFile); 263 | 264 | // When the given sharedFile is outside the paths supported by the provider. 265 | } catch (IllegalArgumentException exception) { 266 | Log.e(TAG, "sharedFile is outside the paths supported by the provider: " + sharedFile.getAbsolutePath(), exception); 267 | return Uri.EMPTY; 268 | } 269 | } 270 | 271 | /** 272 | * Get the shared directory for the app, defined by the file provider paths. 273 | */ 274 | public File getSharedDirectory() { 275 | return new File(context.getFilesDir(), "shared_files"); 276 | } 277 | 278 | /** 279 | * Get the bitmap for a resource path, which can be e.g. a res://, www or shared:// path. 280 | * @return The bitmap or null if the resource could not be found, or an {@link IOException} occurred. 281 | */ 282 | public Bitmap getBitmap(String resourcePath) { 283 | // Check if uri exists 284 | Uri resourceUri = getUri(resourcePath, AssetUtil.RESOURCE_TYPE_DRAWABLE); 285 | if (resourceUri == Uri.EMPTY) return null; 286 | 287 | // Get bitmap from app resources 288 | if (resourcePath.startsWith("res://")) { 289 | return getBitmapFromDrawable(getResourceId(resourcePath, AssetUtil.RESOURCE_TYPE_DRAWABLE)); 290 | 291 | // Get bitmap from file 292 | } else { 293 | try { 294 | return getBitmapFromUri(resourceUri); 295 | } catch (IOException exception){ 296 | Log.e(TAG, "Could not get bitmap" + resourcePath, exception); 297 | return null; 298 | } 299 | } 300 | } 301 | /** 302 | * Convert Uri to Bitmap. 303 | */ 304 | public Bitmap getBitmapFromUri(Uri uri) throws IOException { 305 | return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri)); 306 | } 307 | 308 | /** 309 | * Get a bitmap from a drawable resource, which can be a bitmap or vector drawable. 310 | * @param drawableId 311 | * @return The bitmap or null if the drawable type is unsupported. 312 | */ 313 | public Bitmap getBitmapFromDrawable(int drawableId) { 314 | Drawable drawable = AppCompatResources.getDrawable(context, drawableId); 315 | 316 | if (drawable instanceof BitmapDrawable) { 317 | return ((BitmapDrawable) drawable).getBitmap(); 318 | 319 | } else if (drawable instanceof VectorDrawableCompat || drawable instanceof VectorDrawable) { 320 | Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 321 | Canvas canvas = new Canvas(bitmap); 322 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 323 | drawable.draw(canvas); 324 | return bitmap; 325 | } 326 | 327 | Log.e(TAG, "Unsupported drawable type: " + drawable.getClass().getName()); 328 | return null; 329 | } 330 | 331 | /** 332 | * Convert a bitmap to a circular bitmap. 333 | * This code has been extracted from the Phonegap Plugin Push plugin: 334 | * https://github.com/phonegap/phonegap-plugin-push 335 | * 336 | * @param bitmap Bitmap to convert. 337 | * @return Circular bitmap. 338 | */ 339 | public static Bitmap getCircleBitmap(Bitmap bitmap) { 340 | if (bitmap == null) return null; 341 | 342 | final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); 343 | final Canvas canvas = new Canvas(output); 344 | final int color = Color.RED; 345 | final Paint paint = new Paint(); 346 | final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 347 | final RectF rectF = new RectF(rect); 348 | 349 | paint.setAntiAlias(true); 350 | canvas.drawARGB(0, 0, 0, 0); 351 | paint.setColor(color); 352 | float cx = bitmap.getWidth() / 2; 353 | float cy = bitmap.getHeight() / 2; 354 | float radius = cx < cy ? cx : cy; 355 | canvas.drawCircle(cx, cy, radius, paint); 356 | 357 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 358 | canvas.drawBitmap(bitmap, rect, rect, paint); 359 | 360 | bitmap.recycle(); 361 | 362 | return output; 363 | } 364 | 365 | /** 366 | * Copy content from input stream into output stream. 367 | * 368 | * @param in The input stream. 369 | * @param out The output stream. 370 | */ 371 | public static void copyFile(InputStream in, FileOutputStream out) { 372 | byte[] buffer = new byte[1024]; 373 | int read; 374 | 375 | try { 376 | while ((read = in.read(buffer)) != -1) { 377 | out.write(buffer, 0, read); 378 | } 379 | out.flush(); 380 | out.close(); 381 | } catch (Exception e) { 382 | e.printStackTrace(); 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/ios/APPNotificationOptions.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | #import "APPNotificationOptions.h" 23 | #import "UNUserNotificationCenter+APPLocalNotification.h" 24 | 25 | @import CoreLocation; 26 | @import UserNotifications; 27 | 28 | // Maps these crap where Sunday is the 1st day of the week 29 | static NSInteger WEEKDAYS[8] = { 0, 2, 3, 4, 5, 6, 7, 1 }; 30 | 31 | @interface APPNotificationOptions () 32 | 33 | // The dictionary which contains all notification properties 34 | @property(nonatomic, retain) NSDictionary* dict; 35 | 36 | @end 37 | 38 | @implementation APPNotificationOptions : NSObject 39 | 40 | @synthesize dict; 41 | 42 | #pragma mark - 43 | #pragma mark Initialization 44 | 45 | /** 46 | * Initialize by using the given property values. 47 | * @param dictionary A key-value property map. 48 | * @return [ APPNotificationOptions ] 49 | */ 50 | - (id) initWithDict:(NSDictionary*)dictionary 51 | { 52 | self = [self init]; 53 | self.dict = dictionary; 54 | return self; 55 | } 56 | 57 | #pragma mark - 58 | #pragma mark Properties 59 | 60 | /** 61 | * The ID for the notification. 62 | * 63 | * @return [ NSNumber* ] 64 | */ 65 | - (NSNumber*) id 66 | { 67 | NSInteger id = [dict[@"id"] integerValue]; 68 | 69 | return [NSNumber numberWithInteger:id]; 70 | } 71 | 72 | /** 73 | * The ID for the notification. 74 | * 75 | * @return [ NSString* ] 76 | */ 77 | - (NSString*) identifier 78 | { 79 | return [NSString stringWithFormat:@"%@", self.id]; 80 | } 81 | 82 | /** 83 | * The title for the notification. 84 | * 85 | * @return [ NSString* ] 86 | */ 87 | - (NSString*) title 88 | { 89 | return dict[@"title"]; 90 | } 91 | 92 | /** 93 | * The subtitle for the notification. 94 | * 95 | * @return [ NSString* ] 96 | */ 97 | - (NSString*) subtitle 98 | { 99 | NSArray *parts = [self.title componentsSeparatedByString:@"\n"]; 100 | 101 | return parts.count < 2 ? @"" : [parts objectAtIndex:1]; 102 | } 103 | 104 | /** 105 | * The text for the notification. 106 | * 107 | * @return [ NSString* ] 108 | */ 109 | - (NSString*) text 110 | { 111 | return dict[@"text"]; 112 | } 113 | 114 | /** 115 | * Don't show a notification, make no sound, no vibration, when app is in foreground 116 | * 117 | * @return [ BOOL ] 118 | */ 119 | - (BOOL) silent 120 | { 121 | return [dict[@"silent"] boolValue]; 122 | } 123 | 124 | /** 125 | * Show notification in foreground. 126 | * 127 | * @return [ BOOL ] 128 | */ 129 | - (BOOL) iOSForeground 130 | { 131 | return [dict[@"iOSForeground"] boolValue]; 132 | } 133 | 134 | /** 135 | * The badge number for the notification. 136 | * 0 removes the badge, -1 don't changes the badge 137 | * 138 | * @return [ int ] 139 | */ 140 | - (int) badgeNumber 141 | { 142 | return [dict[@"badgeNumber"] intValue]; 143 | } 144 | 145 | /** 146 | * The category of the notification. 147 | * 148 | * @return [ NSString* ] 149 | */ 150 | - (NSString*) actionGroupId 151 | { 152 | id actions = dict[@"actions"]; 153 | 154 | return ([actions isKindOfClass:NSString.class]) ? actions : kAPPGeneralCategory; 155 | } 156 | 157 | /** 158 | * The sound file for the notification. 159 | * 160 | * @return [ UNNotificationSound* ] 161 | */ 162 | - (UNNotificationSound*) sound 163 | { 164 | NSString* soundPath = dict[@"sound"]; 165 | 166 | if (soundPath == NULL || [soundPath length] == 0 ) return NULL; 167 | 168 | if ([soundPath isEqualToString:@"default"]) { 169 | return [UNNotificationSound defaultSound]; 170 | } 171 | 172 | // Change file:// to www/ for assets 173 | if ([soundPath hasPrefix:@"file://"]) { 174 | soundPath = [self soundPathForAsset:soundPath]; 175 | 176 | // Gets the file name from the path 177 | } else if ([soundPath hasPrefix:@"res:"]) { 178 | soundPath = [self soundNameForResource:soundPath]; 179 | } 180 | 181 | return [UNNotificationSound soundNamed:soundPath]; 182 | } 183 | 184 | 185 | /** 186 | * Additional content to attach. 187 | * 188 | * @return [ UNNotificationSound* ] 189 | */ 190 | - (NSArray *) attachments 191 | { 192 | NSArray* attachmentsPaths = dict[@"attachments"]; 193 | NSMutableArray* attachments = [[NSMutableArray alloc] init]; 194 | 195 | if (!attachmentsPaths) return attachments; 196 | 197 | for (NSString* path in attachmentsPaths) { 198 | UNNotificationAttachment* attachment = [UNNotificationAttachment attachmentWithIdentifier:path 199 | URL:[self urlForAttachmentPath:path] 200 | options:NULL 201 | error:NULL]; 202 | 203 | if (attachment) { 204 | [attachments addObject:attachment]; 205 | } 206 | } 207 | 208 | return attachments; 209 | } 210 | 211 | #pragma mark - 212 | #pragma mark Public 213 | 214 | /** 215 | * Specify how and when to trigger the notification. 216 | * 217 | * @return [ UNNotificationTrigger* ] 218 | */ 219 | - (UNNotificationTrigger*) trigger 220 | { 221 | NSString* type = [self valueForTriggerOption:@"type"]; 222 | 223 | if ([type isEqualToString:@"location"]) return [self triggerWithRegion]; 224 | if (![type isEqualToString:@"calendar"]) NSLog(@"Unknown type: %@", type); 225 | if ([self isRepeating]) return [self repeatingTrigger]; 226 | 227 | return [self nonRepeatingTrigger]; 228 | } 229 | 230 | /** 231 | * The notification's user info dict. 232 | * 233 | * @return [ NSDictionary* ] 234 | */ 235 | - (NSDictionary*) userInfo 236 | { 237 | if (dict[@"updatedAt"]) { 238 | NSMutableDictionary* data = [dict mutableCopy]; 239 | 240 | [data removeObjectForKey:@"updatedAt"]; 241 | 242 | return data; 243 | } 244 | 245 | return dict; 246 | } 247 | 248 | #pragma mark - 249 | #pragma mark Private 250 | 251 | - (id) valueForTriggerOption:(NSString*)key 252 | { 253 | return dict[@"trigger"][key]; 254 | } 255 | 256 | /** 257 | * The date when to fire the notification. 258 | * 259 | * @return [ NSDate* ] 260 | */ 261 | - (NSDate*) triggerDate 262 | { 263 | double timestamp = [[self valueForTriggerOption:@"at"] doubleValue]; 264 | 265 | return [NSDate dateWithTimeIntervalSince1970:(timestamp / 1000)]; 266 | } 267 | 268 | /** 269 | * If the notification shall be repeating. 270 | * 271 | * @return [ BOOL ] 272 | */ 273 | - (BOOL) isRepeating 274 | { 275 | id every = [self valueForTriggerOption:@"every"]; 276 | 277 | if ([every isKindOfClass:NSString.class]) 278 | return ((NSString*) every).length > 0; 279 | 280 | if ([every isKindOfClass:NSDictionary.class]) 281 | return ((NSDictionary*) every).count > 0; 282 | 283 | return every > 0; 284 | } 285 | 286 | /** 287 | * Non repeating trigger. 288 | * 289 | * @return [ UNTimeIntervalNotificationTrigger* ] 290 | */ 291 | - (UNNotificationTrigger*) nonRepeatingTrigger 292 | { 293 | if ([self valueForTriggerOption:@"at"]) { 294 | return [self triggerWithDateMatchingComponents:NO]; 295 | } 296 | 297 | return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[self timeInterval] repeats:NO]; 298 | } 299 | 300 | /** 301 | * Repeating trigger. 302 | * 303 | * @return [ UNNotificationTrigger* ] 304 | */ 305 | - (UNNotificationTrigger*) repeatingTrigger 306 | { 307 | id every = [self valueForTriggerOption:@"every"]; 308 | 309 | if ([every isKindOfClass:NSString.class]) 310 | return [self triggerWithDateMatchingComponents:YES]; 311 | 312 | if ([every isKindOfClass:NSDictionary.class]) 313 | return [self triggerWithCustomDateMatchingComponents]; 314 | 315 | return [self triggerWithTimeInterval]; 316 | } 317 | 318 | /** 319 | * A trigger based on a calendar time defined by the user. 320 | * 321 | * @return [ UNTimeIntervalNotificationTrigger* ] 322 | */ 323 | - (UNTimeIntervalNotificationTrigger*) triggerWithTimeInterval 324 | { 325 | double seconds = [self convertTicksToSeconds:[[self valueForTriggerOption:@"every"] doubleValue] 326 | unit:[self valueForTriggerOption:@"unit"]]; 327 | 328 | if (seconds < 60) { 329 | NSLog(@"time interval must be at least 60 sec if repeating"); 330 | seconds = 60; 331 | } 332 | 333 | return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:seconds 334 | repeats:YES]; 335 | } 336 | 337 | /** 338 | * A repeating trigger based on a calendar time intervals defined by the plugin. 339 | * 340 | * @return [ UNCalendarNotificationTrigger* ] 341 | */ 342 | - (UNCalendarNotificationTrigger*) triggerWithDateMatchingComponents:(BOOL)repeats 343 | { 344 | NSDateComponents *date = [[self calendarWithMondayAsFirstDay] components:[self repeatInterval] 345 | fromDate:[self triggerDate]]; 346 | 347 | date.timeZone = [NSTimeZone defaultTimeZone]; 348 | 349 | return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:date 350 | repeats:repeats]; 351 | } 352 | 353 | /** 354 | * A repeating trigger based on a calendar time intervals defined by the user. 355 | * 356 | * @return [ UNCalendarNotificationTrigger* ] 357 | */ 358 | - (UNCalendarNotificationTrigger*) triggerWithCustomDateMatchingComponents 359 | { 360 | NSDateComponents *date = [self customDateComponents]; 361 | date.calendar = [self calendarWithMondayAsFirstDay]; 362 | date.timeZone = [NSTimeZone defaultTimeZone]; 363 | 364 | return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:date 365 | repeats:YES]; 366 | } 367 | 368 | /** 369 | * A repeating trigger based on a location region. 370 | * 371 | * @return [ UNLocationNotificationTrigger* ] 372 | */ 373 | - (UNLocationNotificationTrigger*) triggerWithRegion 374 | { 375 | NSArray* center = [self valueForTriggerOption:@"center"]; 376 | double radius = [[self valueForTriggerOption:@"radius"] doubleValue]; 377 | BOOL single = [[self valueForTriggerOption:@"single"] boolValue]; 378 | 379 | CLLocationCoordinate2D coord = 380 | CLLocationCoordinate2DMake([center[0] doubleValue], [center[1] doubleValue]); 381 | 382 | CLCircularRegion* region = 383 | [[CLCircularRegion alloc] initWithCenter:coord 384 | radius:radius 385 | identifier:self.identifier]; 386 | 387 | region.notifyOnEntry = [[self valueForTriggerOption:@"notifyOnEntry"] boolValue]; 388 | region.notifyOnExit = [[self valueForTriggerOption:@"notifyOnExit"] boolValue]; 389 | 390 | return [UNLocationNotificationTrigger triggerWithRegion:region 391 | repeats:!single]; 392 | } 393 | 394 | /** 395 | * The time interval between the next fire date and now. 396 | * 397 | * @return [ double ] 398 | */ 399 | - (double) timeInterval 400 | { 401 | double ticks = [[self valueForTriggerOption:@"in"] doubleValue]; 402 | NSString* unit = [self valueForTriggerOption:@"unit"]; 403 | double seconds = [self convertTicksToSeconds:ticks unit:unit]; 404 | 405 | return MAX(0.01f, seconds); 406 | } 407 | 408 | /** 409 | * The repeat interval for the notification. 410 | * 411 | * @return [ NSCalendarUnit ] 412 | */ 413 | - (NSCalendarUnit) repeatInterval 414 | { 415 | NSString* interval = [self valueForTriggerOption:@"every"]; 416 | NSCalendarUnit units = NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond; 417 | 418 | if ([interval isEqualToString:@"minute"]) 419 | return NSCalendarUnitSecond; 420 | 421 | if ([interval isEqualToString:@"hour"]) 422 | return NSCalendarUnitMinute|NSCalendarUnitSecond; 423 | 424 | if ([interval isEqualToString:@"day"]) 425 | return NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond; 426 | 427 | if ([interval isEqualToString:@"week"]) 428 | return NSCalendarUnitWeekday|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond; 429 | 430 | if ([interval isEqualToString:@"month"]) 431 | return NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond; 432 | 433 | if ([interval isEqualToString:@"year"]) 434 | return NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond; 435 | 436 | return units; 437 | } 438 | 439 | /** 440 | * The repeat interval for the notification. 441 | * 442 | * @return [ NSDateComponents* ] 443 | */ 444 | - (NSDateComponents*) customDateComponents 445 | { 446 | NSDateComponents* date = [[NSDateComponents alloc] init]; 447 | NSDictionary* every = [self valueForTriggerOption:@"every"]; 448 | 449 | date.second = 0; 450 | 451 | for (NSString* key in every) { 452 | long value = [[every valueForKey:key] longValue]; 453 | 454 | if ([key isEqualToString:@"minute"]) { 455 | date.minute = value; 456 | } else 457 | if ([key isEqualToString:@"hour"]) { 458 | date.hour = value; 459 | } else 460 | if ([key isEqualToString:@"day"]) { 461 | date.day = value; 462 | } else 463 | if ([key isEqualToString:@"weekday"]) { 464 | date.weekday = WEEKDAYS[value]; 465 | } else 466 | if ([key isEqualToString:@"weekdayOrdinal"]) { 467 | date.weekdayOrdinal = value; 468 | } else 469 | if ([key isEqualToString:@"week"]) { 470 | date.weekOfYear = value; 471 | } else 472 | if ([key isEqualToString:@"weekOfMonth"]) { 473 | date.weekOfMonth = value; 474 | } else 475 | if ([key isEqualToString:@"month"]) { 476 | date.month = value; 477 | } else 478 | if ([key isEqualToString:@"quarter"]) { 479 | date.quarter = value; 480 | } else 481 | if ([key isEqualToString:@"year"]) { 482 | date.year = value; 483 | } 484 | } 485 | 486 | return date; 487 | } 488 | 489 | /** 490 | * Converts file:// to www/ for assets. 491 | * @param path A relative assets file path. 492 | * @return [ NSString* ] 493 | */ 494 | - (NSString*) soundPathForAsset:(NSString*)path 495 | { 496 | return [path stringByReplacingOccurrencesOfString:@"file://" withString:@"www/"]; 497 | } 498 | 499 | /** 500 | * Convert a ressource path to an valid sound name attribute. 501 | * @param path A relative ressource file path. 502 | * @return [ NSString* ] 503 | */ 504 | - (NSString*) soundNameForResource:(NSString*)path 505 | { 506 | return [path pathComponents].lastObject; 507 | } 508 | 509 | /** 510 | * URL for the specified attachment path. 511 | * @param path Absolute/relative path or a base64 data. 512 | * @return [ NSURL* ] 513 | */ 514 | - (NSURL*) urlForAttachmentPath:(NSString*)path 515 | { 516 | if ([path hasPrefix:@"file:///"]) return [self urlForFile:path]; 517 | if ([path hasPrefix:@"res:"]) return [self urlForResource:path]; 518 | if ([path hasPrefix:@"www"] || [path hasPrefix:@"file://"]) return [self urlForAsset:path]; 519 | if ([path hasPrefix:@"base64:"]) return [self urlFromBase64:path]; 520 | 521 | if (![[NSFileManager defaultManager] fileExistsAtPath:path]) NSLog(@"File not found: %@", path); 522 | 523 | return [NSURL fileURLWithPath:path]; 524 | } 525 | 526 | /** 527 | * URL to an absolute file path. 528 | * @return [ NSURL* ] 529 | */ 530 | - (NSURL*) urlForFile:(NSString*)absoluteFilePath 531 | { 532 | absoluteFilePath = [absoluteFilePath stringByReplacingOccurrencesOfString:@"file://" withString:@""]; 533 | 534 | if (![[NSFileManager defaultManager] fileExistsAtPath:absoluteFilePath]) { 535 | NSLog(@"File not found: %@", absoluteFilePath); 536 | } 537 | 538 | return [NSURL fileURLWithPath:absoluteFilePath]; 539 | } 540 | 541 | /** 542 | * URL to a resource file. 543 | * @param path A relative file path. 544 | * @return [ NSURL* ] 545 | */ 546 | - (NSURL*) urlForResource:(NSString*)path 547 | { 548 | if ([path isEqualToString:@"res://icon"]) path = @"res://AppIcon60x60@3x.png"; 549 | 550 | NSString* absPath = [path stringByReplacingOccurrencesOfString:@"res:/" 551 | withString:@""]; 552 | 553 | absPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:absPath]; 554 | 555 | if (![[NSFileManager defaultManager] fileExistsAtPath:absPath]) NSLog(@"File not found: %@", absPath); 556 | 557 | return [NSURL fileURLWithPath:absPath]; 558 | } 559 | 560 | /** 561 | * URL to an asset file. 562 | * @param path A relative www file path. 563 | * @return [ NSURL* ] 564 | */ 565 | - (NSURL*) urlForAsset:(NSString*)path 566 | { 567 | NSString *absoluteAssetPath = [NSString stringWithFormat:@"%@/%@", 568 | [[NSBundle mainBundle] bundlePath], 569 | [path stringByReplacingOccurrencesOfString:@"file://" 570 | withString:@"www/"]]; 571 | 572 | if (![[NSFileManager defaultManager] fileExistsAtPath:absoluteAssetPath]) { 573 | NSLog(@"File not found: %@", absoluteAssetPath); 574 | } 575 | 576 | return [NSURL fileURLWithPath:absoluteAssetPath]; 577 | } 578 | 579 | /** 580 | * URL for a base64 encoded string. 581 | * @param base64String Base64 encoded string. 582 | * @return [ NSURL* ] 583 | */ 584 | - (NSURL*) urlFromBase64:(NSString*)base64String 585 | { 586 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^base64:[^/]+.." 587 | options:NSRegularExpressionCaseInsensitive 588 | error:Nil]; 589 | 590 | NSString *dataString = [regex stringByReplacingMatchesInString:base64String 591 | options:0 592 | range:NSMakeRange(0, [base64String length]) 593 | withTemplate:@""]; 594 | 595 | NSData* data = [[NSData alloc] initWithBase64EncodedString:dataString 596 | options:0]; 597 | 598 | 599 | return [self urlForData:data withFileName:[self basenameFromAttachmentPath:base64String]]; 600 | } 601 | 602 | /** 603 | * Extract the attachments basename. 604 | * @param path The file path or base64 data. 605 | * @return [ NSString* ] 606 | */ 607 | - (NSString*) basenameFromAttachmentPath:(NSString*)path 608 | { 609 | if ([path hasPrefix:@"base64:"]) { 610 | NSString* pathWithoutPrefix = [path stringByReplacingOccurrencesOfString:@"base64:" 611 | withString:@""]; 612 | 613 | return [pathWithoutPrefix substringToIndex:[pathWithoutPrefix rangeOfString:@"//"].location]; 614 | } 615 | 616 | return path; 617 | } 618 | 619 | /** 620 | * Write the data into a temp file. 621 | * @param data The data to save to file. 622 | * @param filename The name of the file. 623 | * @return [ NSURL* ] 624 | */ 625 | - (NSURL*) urlForData:(NSData*)data withFileName:(NSString*) filename 626 | { 627 | [[NSFileManager defaultManager] createDirectoryAtPath:NSTemporaryDirectory() 628 | withIntermediateDirectories:YES 629 | attributes:NULL 630 | error:NULL]; 631 | 632 | NSString* absPath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; 633 | 634 | if (![[NSFileManager defaultManager] fileExistsAtPath:absPath]) NSLog(@"File not found: %@", absPath); 635 | 636 | NSURL* url = [NSURL fileURLWithPath:absPath]; 637 | [data writeToURL:url atomically:NO]; 638 | 639 | return url; 640 | } 641 | 642 | /** 643 | * Convert the amount of ticks into seconds. 644 | * @param ticks The amount of ticks. 645 | * @param unit The unit of the ticks (minute, hour, day, ...) 646 | * @return [ double ] Amount of ticks in seconds. 647 | */ 648 | - (double) convertTicksToSeconds:(double)ticks unit:(NSString*)unit 649 | { 650 | if ([unit isEqualToString:@"second"]) { 651 | return ticks; 652 | } else if ([unit isEqualToString:@"minute"]) { 653 | return ticks * 60; 654 | } else if ([unit isEqualToString:@"hour"]) { 655 | return ticks * 60 * 60; 656 | } else if ([unit isEqualToString:@"day"]) { 657 | return ticks * 60 * 60 * 24; 658 | } else if ([unit isEqualToString:@"week"]) { 659 | return ticks * 60 * 60 * 24 * 7; 660 | } else if ([unit isEqualToString:@"month"]) { 661 | return ticks * 60 * 60 * 24 * 30.438; 662 | } else if ([unit isEqualToString:@"quarter"]) { 663 | return ticks * 60 * 60 * 24 * 91.313; 664 | } else if ([unit isEqualToString:@"year"]) { 665 | return ticks * 60 * 60 * 24 * 365; 666 | } 667 | 668 | return 0; 669 | } 670 | 671 | /** 672 | * Instance if a calendar where the monday is the first day of the week. 673 | * @return [ NSCalendar* ] 674 | */ 675 | - (NSCalendar*) calendarWithMondayAsFirstDay 676 | { 677 | NSCalendar* calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierISO8601]; 678 | calendar.firstWeekday = 2; 679 | calendar.minimumDaysInFirstWeek = 1; 680 | return calendar; 681 | } 682 | 683 | /** 684 | * String representation of this Object 685 | */ 686 | - (NSString *)description { 687 | return [NSString stringWithFormat: @"%@", dict]; 688 | } 689 | @end 690 | -------------------------------------------------------------------------------- /src/ios/APPLocalNotification.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache 2.0 License 3 | * 4 | * Copyright (c) Sebastian Katzer 2017 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apache License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://opensource.org/licenses/Apache-2.0/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | */ 21 | 22 | // codebeat:disable[TOO_MANY_FUNCTIONS] 23 | 24 | #import "APPLocalNotification.h" 25 | #import "APPNotificationContent.h" 26 | #import "APPNotificationOptions.h" 27 | #import "APPNotificationCategory.h" 28 | #import "UNUserNotificationCenter+APPLocalNotification.h" 29 | #import "UNNotificationRequest+APPLocalNotification.h" 30 | 31 | @interface APPLocalNotification () 32 | 33 | @property (strong, nonatomic) UNUserNotificationCenter* center; 34 | @property (NS_NONATOMIC_IOSONLY, nullable, weak) id delegate; 35 | @property (readwrite, assign) BOOL deviceready; 36 | @property (readwrite, assign) BOOL isActive; 37 | @property (readonly, nonatomic, retain) NSArray* launchDetails; 38 | @property (readonly, nonatomic, retain) NSMutableArray* eventQueue; 39 | 40 | @end 41 | 42 | @implementation APPLocalNotification 43 | 44 | @synthesize deviceready, isActive, eventQueue; 45 | 46 | #pragma mark - 47 | #pragma mark Life Cycle 48 | 49 | /** 50 | * Registers obervers after plugin was initialized. 51 | */ 52 | - (void) pluginInitialize 53 | { 54 | NSLog(@"LocalNotification: pluginInitialize"); 55 | eventQueue = [[NSMutableArray alloc] init]; 56 | _center = [UNUserNotificationCenter currentNotificationCenter]; 57 | _delegate = _center.delegate; 58 | 59 | _center.delegate = self; 60 | [_center registerGeneralNotificationCategory]; 61 | 62 | [self monitorAppStateChanges]; 63 | } 64 | 65 | /** 66 | * Monitor changes of the app state and update the _isActive flag. 67 | */ 68 | - (void) monitorAppStateChanges 69 | { 70 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 71 | 72 | [center addObserverForName:UIApplicationDidBecomeActiveNotification 73 | object:NULL queue:[NSOperationQueue mainQueue] 74 | usingBlock:^(NSNotification *e) { self->isActive = YES; }]; 75 | 76 | [center addObserverForName:UIApplicationDidEnterBackgroundNotification 77 | object:NULL queue:[NSOperationQueue mainQueue] 78 | usingBlock:^(NSNotification *e) { self->isActive = NO; }]; 79 | } 80 | 81 | #pragma mark - 82 | #pragma mark Interface 83 | 84 | 85 | /** 86 | * Set launchDetails object. 87 | */ 88 | - (void) launch:(CDVInvokedUrlCommand*)command 89 | { 90 | if (!_launchDetails) return; 91 | 92 | [self.commandDelegate evalJs:[NSString 93 | stringWithFormat:@"cordova.plugins.notification.local.launchDetails = {id:%@, action:'%@'}", 94 | _launchDetails[0], _launchDetails[1]]]; 95 | 96 | _launchDetails = NULL; 97 | } 98 | 99 | /** 100 | * Execute all queued events. 101 | */ 102 | - (void) ready:(CDVInvokedUrlCommand*)command 103 | { 104 | deviceready = YES; 105 | 106 | [self.commandDelegate runInBackground:^{ 107 | for (NSString* js in self->eventQueue) { 108 | [self.commandDelegate evalJs:js]; 109 | } 110 | [self->eventQueue removeAllObjects]; 111 | }]; 112 | } 113 | 114 | /** 115 | * Schedule notifications. 116 | */ 117 | - (void) schedule:(CDVInvokedUrlCommand*)command 118 | { 119 | [self.commandDelegate runInBackground:^{ 120 | for (NSDictionary* options in command.arguments) { 121 | [self scheduleNotification:[[APPNotificationContent alloc] initWithOptions:options]]; 122 | } 123 | 124 | [self hasPermission:command]; 125 | }]; 126 | } 127 | 128 | /** 129 | * Update notifications. 130 | */ 131 | - (void) update:(CDVInvokedUrlCommand*)command 132 | { 133 | NSArray* notifications = command.arguments; 134 | 135 | [self.commandDelegate runInBackground:^{ 136 | for (NSDictionary* options in notifications) { 137 | NSNumber* id = [options objectForKey:@"id"]; 138 | UNNotificationRequest* notification; 139 | 140 | notification = [self->_center getNotificationWithId:id]; 141 | 142 | if (!notification) 143 | continue; 144 | 145 | [self updateNotification:[notification copy] 146 | withOptions:options]; 147 | 148 | [self fireEvent:@"update" notification:notification]; 149 | } 150 | 151 | [self hasPermission:command]; 152 | }]; 153 | } 154 | 155 | /** 156 | * Clear notifications by id. 157 | * @param command Contains the IDs of the notifications to clear. 158 | */ 159 | - (void) clear:(CDVInvokedUrlCommand*)command 160 | { 161 | [self.commandDelegate runInBackground:^{ 162 | for (NSNumber* id in command.arguments) { 163 | UNNotificationRequest* notification = [self->_center getNotificationWithId:id]; 164 | if (!notification) continue; 165 | [self->_center clearNotification:notification]; 166 | [self fireEvent:@"clear" notification:notification]; 167 | } 168 | 169 | [self execCallback:command]; 170 | }]; 171 | } 172 | 173 | /** 174 | * Clear all local notifications. 175 | */ 176 | - (void) clearAll:(CDVInvokedUrlCommand*)command 177 | { 178 | [self.commandDelegate runInBackground:^{ 179 | [self->_center clearNotifications]; 180 | [self clearApplicationIconBadgeNumber]; 181 | [self fireEvent:@"clearall"]; 182 | [self execCallback:command]; 183 | }]; 184 | } 185 | 186 | /** 187 | * Cancel notifications by id. 188 | * @param command Contains the IDs of the notifications to clear. 189 | */ 190 | - (void) cancel:(CDVInvokedUrlCommand*)command 191 | { 192 | [self.commandDelegate runInBackground:^{ 193 | for (NSNumber* id in command.arguments) { 194 | UNNotificationRequest* notification = [self->_center getNotificationWithId:id]; 195 | if (!notification) continue; 196 | [self->_center cancelNotification:notification]; 197 | [self fireEvent:@"cancel" notification:notification]; 198 | } 199 | 200 | [self execCallback:command]; 201 | }]; 202 | } 203 | 204 | /** 205 | * Cancel all local notifications. 206 | */ 207 | - (void) cancelAll:(CDVInvokedUrlCommand*)command 208 | { 209 | [self.commandDelegate runInBackground:^{ 210 | [self->_center cancelNotifications]; 211 | [self clearApplicationIconBadgeNumber]; 212 | [self fireEvent:@"cancelall"]; 213 | [self execCallback:command]; 214 | }]; 215 | } 216 | 217 | /** 218 | * Get type of notification. 219 | * @param command Contains the type to check. 220 | */ 221 | - (void) type:(CDVInvokedUrlCommand*)command 222 | { 223 | [self.commandDelegate runInBackground:^{ 224 | NSString* type; 225 | 226 | switch ([self->_center getTypeOfNotificationWithId:[command argumentAtIndex:0]]) { 227 | case NotifcationTypeScheduled: 228 | type = @"scheduled"; 229 | break; 230 | case NotifcationTypeTriggered: 231 | type = @"triggered"; 232 | break; 233 | default: 234 | type = @"unknown"; 235 | } 236 | 237 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 238 | messageAsString:type] 239 | callbackId:command.callbackId]; 240 | }]; 241 | } 242 | 243 | /** 244 | * List of notification IDs by type. 245 | */ 246 | - (void) ids:(CDVInvokedUrlCommand*)command 247 | { 248 | [self.commandDelegate runInBackground:^{ 249 | APPNotificationType type = NotifcationTypeUnknown; 250 | 251 | switch ([command.arguments[0] intValue]) { 252 | case 0: 253 | type = NotifcationTypeAll; 254 | break; 255 | case 1: 256 | type = NotifcationTypeScheduled; 257 | break; 258 | case 2: 259 | type = NotifcationTypeTriggered; 260 | break; 261 | } 262 | 263 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 264 | messageAsArray:[self->_center getNotificationIdsByType:type]] 265 | callbackId:command.callbackId]; 266 | }]; 267 | } 268 | 269 | /** 270 | * Notification by id. 271 | * @param command Contains the id of the notification to return. 272 | */ 273 | - (void) notification:(CDVInvokedUrlCommand*)command 274 | { 275 | [self.commandDelegate runInBackground:^{ 276 | // command.arguments is a list of ids 277 | NSArray* notifications = [self->_center getNotificationOptionsById:command.arguments]; 278 | 279 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 280 | messageAsDictionary:[notifications firstObject]] 281 | callbackId:command.callbackId]; 282 | }]; 283 | } 284 | 285 | /** 286 | * Get notifications by type or ids. 287 | * @param command Contains the ids of the notifications to return. 288 | */ 289 | - (void) notifications:(CDVInvokedUrlCommand*)command 290 | { 291 | [self.commandDelegate runInBackground:^{ 292 | APPNotificationType type = NotifcationTypeUnknown; 293 | NSArray* notifications; 294 | 295 | switch ([command.arguments[0] intValue]) { 296 | case 0: 297 | type = NotifcationTypeAll; 298 | break; 299 | case 1: 300 | type = NotifcationTypeScheduled; 301 | break; 302 | case 2: 303 | type = NotifcationTypeTriggered; 304 | break; 305 | 306 | // Get notifications by ids 307 | case 3: 308 | notifications = [self->_center getNotificationOptionsById:command.arguments[1]]; 309 | break; 310 | } 311 | 312 | // Get notifications by type 313 | if (notifications == nil) { 314 | notifications = [self->_center getNotificationOptionsByType:type]; 315 | } 316 | 317 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 318 | messageAsArray:notifications] 319 | callbackId:command.callbackId]; 320 | }]; 321 | } 322 | 323 | /** 324 | * Check for permission to show notifications. 325 | */ 326 | - (void) hasPermission:(CDVInvokedUrlCommand*)command 327 | { 328 | [_center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) { 329 | BOOL authorized = settings.authorizationStatus == UNAuthorizationStatusAuthorized; 330 | BOOL enabled = settings.notificationCenterSetting == UNNotificationSettingEnabled; 331 | [self execCallback:command arg:authorized && enabled]; 332 | }]; 333 | } 334 | 335 | /** 336 | * Request for permission to show notifcations. 337 | */ 338 | - (void) requestPermission:(CDVInvokedUrlCommand*)command 339 | { 340 | [_center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) 341 | completionHandler:^(BOOL granted, NSError* e) { 342 | [self hasPermission:command]; 343 | } 344 | ]; 345 | } 346 | 347 | /** 348 | * Register, removes or checks for an action group. 349 | */ 350 | - (void) actions:(CDVInvokedUrlCommand *)command 351 | { 352 | [self.commandDelegate runInBackground:^{ 353 | NSString* actionGroupId = [command argumentAtIndex:1]; 354 | 355 | // The first agrument defines, which method was called 356 | switch ([command.arguments[0] intValue]) { 357 | // addActions was called 358 | case 0: { 359 | NSArray* actions = [command argumentAtIndex:2]; 360 | [self->_center addActionGroup:[APPNotificationCategory parse:actions 361 | withId:actionGroupId]]; 362 | [self execCallback:command]; 363 | break; 364 | } 365 | // removeActions was called 366 | case 1: 367 | [self->_center removeActionGroup:actionGroupId]; 368 | [self execCallback:command]; 369 | break; 370 | // hasActions was called 371 | case 2: 372 | [self execCallback:command arg:[self->_center hasActionGroup:actionGroupId]]; 373 | break; 374 | } 375 | }]; 376 | } 377 | 378 | /** 379 | * Open native settings to enable notifications. 380 | * In iOS it's not possible to open the notification settings, only the app settings. 381 | */ 382 | - (void) openNotificationSettings:(CDVInvokedUrlCommand*)command 383 | { 384 | @try { 385 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] 386 | options:@{} 387 | completionHandler:^(BOOL success) { 388 | if (success) { 389 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] 390 | callbackId:command.callbackId]; 391 | } else { 392 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR] 393 | callbackId:command.callbackId]; 394 | } 395 | }]; 396 | } 397 | @catch (NSException *exception) { 398 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR 399 | messageAsString:exception.reason] 400 | callbackId:command.callbackId]; 401 | } 402 | } 403 | 404 | /** 405 | * Clear the badge number on the app icon. Called from JavaScript. 406 | */ 407 | - (void) clearBadge:(CDVInvokedUrlCommand*)command 408 | { 409 | [self clearApplicationIconBadgeNumber]; 410 | [self execCallback:command]; 411 | } 412 | 413 | #pragma mark - 414 | #pragma mark Private 415 | 416 | /** 417 | * Schedule the local notification. 418 | */ 419 | - (void) scheduleNotification:(APPNotificationContent*)appNotificationContent 420 | { 421 | __weak APPLocalNotification* weakSelf = self; 422 | UNNotificationRequest* request = appNotificationContent.request; 423 | NSString* event = [request wasUpdated] ? @"update" : @"add"; 424 | 425 | NSLog(@"Schedule notification, event=%@, trigger=%@, options=%@", event, request.trigger, appNotificationContent.options); 426 | 427 | [_center addNotificationRequest:request 428 | withCompletionHandler:^(NSError* e) { 429 | __strong APPLocalNotification* strongSelf = weakSelf; 430 | [strongSelf fireEvent:event notification:request]; 431 | } 432 | ]; 433 | } 434 | 435 | /** 436 | * Update the local notification. 437 | * @param notification The notification to update. 438 | * @param newOptions The options to update. 439 | */ 440 | - (void) updateNotification:(UNNotificationRequest*)notification 441 | withOptions:(NSDictionary*)newOptions 442 | { 443 | NSMutableDictionary* options = [notification.content.userInfo mutableCopy]; 444 | 445 | [options addEntriesFromDictionary:newOptions]; 446 | [options setObject:[NSDate date] forKey:@"updatedAt"]; 447 | 448 | [self scheduleNotification:[[APPNotificationContent alloc] initWithOptions:options]]; 449 | } 450 | 451 | #pragma mark - 452 | #pragma mark UNUserNotificationCenterDelegate 453 | 454 | /** 455 | * The method will be called on the delegate only if the application is in the foreground. 456 | */ 457 | - (void) userNotificationCenter:(UNUserNotificationCenter *)center 458 | willPresentNotification:(UNNotification *)notification 459 | withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler 460 | { 461 | [_delegate userNotificationCenter:center 462 | willPresentNotification:notification 463 | withCompletionHandler:completionHandler]; 464 | 465 | if ([notification.request.trigger isKindOfClass:UNPushNotificationTrigger.class]) return; 466 | 467 | APPNotificationOptions* notificationOptions = notification.request.options; 468 | NSLog(@"Handle notification while app is in foreground: %@", notificationOptions); 469 | 470 | if (![notification.request wasUpdated]) { 471 | [self fireEvent:@"trigger" notification:notification.request]; 472 | } 473 | 474 | // Init to None, if notification should be silent 475 | UNNotificationPresentationOptions presentationOptions = UNNotificationPresentationOptionNone; 476 | 477 | // Notification should not be silent 478 | if (!notificationOptions.silent) { 479 | // Default is badge, sound and notification center like in Android 480 | presentationOptions = UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; 481 | 482 | // Display notification always in Notification Center like in Android 483 | // Only works since iOS 14, UNNotificationPresentationOptionAlert was splitted in 484 | // UNNotificationPresentationOptionList and UNNotificationPresentationOptionBanner 485 | if (@available(iOS 14, *)) { 486 | presentationOptions |= UNNotificationPresentationOptionList; 487 | } 488 | 489 | // Show banner only when option iOSForeground is true, or app is in background 490 | if (notificationOptions.iOSForeground || !isActive) { 491 | // UNNotificationPresentationOptionBanner available since iOS 14 492 | if (@available(iOS 14, *)) { 493 | presentationOptions |= UNNotificationPresentationOptionBanner; 494 | 495 | // iOS older then 14: Show notification as banner and in notification center 496 | } else { 497 | #pragma clang diagnostic push 498 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 499 | presentationOptions |= UNNotificationPresentationOptionAlert; 500 | #pragma clang diagnostic pop 501 | } 502 | } 503 | } 504 | 505 | completionHandler(presentationOptions); 506 | } 507 | 508 | /** 509 | * Called to let your app know which action was selected by the user for a given 510 | * notification. 511 | */ 512 | - (void) userNotificationCenter:(UNUserNotificationCenter *)center 513 | didReceiveNotificationResponse:(UNNotificationResponse *)response 514 | withCompletionHandler:(void (^)(void))handler 515 | { 516 | UNNotificationRequest* toast = response.notification.request; 517 | 518 | [_delegate userNotificationCenter:center 519 | didReceiveNotificationResponse:response 520 | withCompletionHandler:handler]; 521 | 522 | handler(); 523 | 524 | if ([toast.trigger isKindOfClass:UNPushNotificationTrigger.class]) return; 525 | 526 | NSString* action = response.actionIdentifier; 527 | NSString* event = action; 528 | 529 | if ([action isEqualToString:UNNotificationDefaultActionIdentifier]) { 530 | event = @"click"; 531 | } else 532 | if ([action isEqualToString:UNNotificationDismissActionIdentifier]) { 533 | event = @"clear"; 534 | } 535 | 536 | if (!deviceready && [event isEqualToString:@"click"]) { 537 | _launchDetails = @[toast.options.id, event]; 538 | } 539 | 540 | if (![event isEqualToString:@"clear"]) { 541 | [self fireEvent:@"clear" notification:toast]; 542 | } 543 | 544 | NSMutableDictionary* data = [[NSMutableDictionary alloc] init]; 545 | 546 | if ([response isKindOfClass:UNTextInputNotificationResponse.class]) { 547 | [data setObject:((UNTextInputNotificationResponse*) response).userText 548 | forKey:@"text"]; 549 | } 550 | 551 | [self fireEvent:event notification:toast data:data]; 552 | } 553 | 554 | #pragma mark - 555 | #pragma mark Helper 556 | 557 | /** 558 | * Removes the badge number from the app icon. 559 | */ 560 | - (void) clearApplicationIconBadgeNumber 561 | { 562 | NSLog(@"LocalNotification: clear application badge"); 563 | dispatch_async(dispatch_get_main_queue(), ^{ 564 | [UIApplication sharedApplication].applicationIconBadgeNumber = 0; 565 | }); 566 | } 567 | 568 | /** 569 | * Invokes the callback without any parameter. 570 | */ 571 | - (void) execCallback:(CDVInvokedUrlCommand*)command 572 | { 573 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] 574 | callbackId:command.callbackId]; 575 | } 576 | 577 | /** 578 | * Invokes the callback with a single boolean parameter. 579 | */ 580 | - (void) execCallback:(CDVInvokedUrlCommand*)command arg:(BOOL)arg 581 | { 582 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 583 | messageAsBool:arg] 584 | callbackId:command.callbackId]; 585 | } 586 | 587 | /** 588 | * Fire general event. 589 | * @param event The name of the event to fire. 590 | */ 591 | - (void) fireEvent:(NSString*)event 592 | { 593 | [self fireEvent:event notification:NULL data:[[NSMutableDictionary alloc] init]]; 594 | } 595 | 596 | /** 597 | * Fire event for about a local notification. 598 | * @param event The name of the event to fire. 599 | * @param notificationRequest The UNNotificationRequest 600 | */ 601 | - (void) fireEvent:(NSString*)event 602 | notification:(UNNotificationRequest*)notificationRequest 603 | { 604 | [self fireEvent:event notification:notificationRequest data:[[NSMutableDictionary alloc] init]]; 605 | } 606 | 607 | /** 608 | * Fire event for about a local notification. 609 | * @param event The name of the event to fire. 610 | * @param notificationRequest The UNNotificationRequest 611 | * @param data Event object with additional data. 612 | */ 613 | - (void) fireEvent:(NSString*)event 614 | notification:(UNNotificationRequest*)notificationRequest 615 | data:(NSMutableDictionary*)data 616 | { 617 | [data setObject:event forKey:@"event"]; 618 | [data setObject:@(isActive) forKey:@"foreground"]; 619 | [data setObject:@(!deviceready) forKey:@"queued"]; 620 | 621 | if (notificationRequest) { 622 | [data setObject:notificationRequest.options.id forKey:@"notification"]; 623 | } 624 | 625 | NSString *params; 626 | NSString *dataAsJSON = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:data 627 | options:0 628 | error:NULL] 629 | encoding:NSUTF8StringEncoding]; 630 | 631 | if (notificationRequest) { 632 | params = [NSString stringWithFormat:@"%@,%@", [notificationRequest encodeToJSON], dataAsJSON]; 633 | } else { 634 | params = [NSString stringWithFormat:@"%@", dataAsJSON]; 635 | } 636 | 637 | NSString *js = [NSString stringWithFormat:@"cordova.plugins.notification.local.fireEvent('%@', %@)", event, params]; 638 | 639 | NSLog(@"%@", js); 640 | 641 | if (deviceready) { 642 | [self.commandDelegate evalJs:js]; 643 | } else { 644 | [self.eventQueue addObject:js]; 645 | } 646 | } 647 | 648 | @end 649 | 650 | // codebeat:enable[TOO_MANY_FUNCTIONS] 651 | --------------------------------------------------------------------------------