├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------