Sebastián Katzer пре 8 година
родитељ
комит
e50f6f2fec
100 измењених фајлова са 9300 додато и 4077 уклоњено
  1. 4 0
      .gitignore
  2. 3 0
      CHANGELOG.md
  3. 1 1
      LICENSE
  4. 408 80
      README.md
  5. BIN
      images/android-actions.png
  6. BIN
      images/android-chat.png
  7. BIN
      images/android-inbox.png
  8. BIN
      images/android-progress.png
  9. BIN
      images/android-reply-2.png
  10. BIN
      images/android-reply.png
  11. BIN
      images/android-stack.png
  12. BIN
      images/ios-actions.png
  13. BIN
      images/ios-basic.png
  14. BIN
      images/ios-permission.png
  15. BIN
      images/logo.png
  16. BIN
      images/windows-actions.png
  17. 9 5
      package.json
  18. 97 76
      plugin.xml
  19. 19 9
      src/android/ClearReceiver.java
  20. 0 69
      src/android/ClickActivity.java
  21. 107 0
      src/android/ClickReceiver.java
  22. 271 308
      src/android/LocalNotification.java
  23. 21 18
      src/android/TriggerReceiver.java
  24. 28 0
      src/android/build/localnotification.gradle
  25. 0 122
      src/android/notification/AbstractTriggerReceiver.java
  26. 265 73
      src/android/notification/Builder.java
  27. 0 61
      src/android/notification/ClickActivity.java
  28. 193 211
      src/android/notification/Manager.java
  29. 185 150
      src/android/notification/Notification.java
  30. 446 172
      src/android/notification/Options.java
  31. 231 0
      src/android/notification/Request.java
  32. 0 59
      src/android/notification/TriggerReceiver.java
  33. 137 0
      src/android/notification/action/Action.java
  34. 139 0
      src/android/notification/action/ActionGroup.java
  35. 15 26
      src/android/notification/receiver/AbstractClearReceiver.java
  36. 29 36
      src/android/notification/receiver/AbstractClickReceiver.java
  37. 0 0
      src/android/notification/receiver/AbstractRestoreReceiver.java
  38. 81 0
      src/android/notification/receiver/AbstractTriggerReceiver.java
  39. 70 0
      src/android/notification/trigger/DateTrigger.java
  40. 105 0
      src/android/notification/trigger/IntervalTrigger.java
  41. 208 0
      src/android/notification/trigger/MatchTrigger.java
  42. 26 0
      src/android/notification/util/AssetProvider.java
  43. 73 138
      src/android/notification/util/AssetUtil.java
  44. 24 0
      src/android/xml/localnotification_provider_paths.xml
  45. 37 41
      src/ios/APPLocalNotification.h
  46. 332 358
      src/ios/APPLocalNotification.m
  47. 0 249
      src/ios/APPLocalNotificationOptions.m
  48. 9 18
      src/ios/APPNotificationContent.h
  49. 152 0
      src/ios/APPNotificationContent.m
  50. 42 0
      src/ios/APPNotificationOptions.h
  51. 821 0
      src/ios/APPNotificationOptions.m
  52. 0 63
      src/ios/UIApplication+APPLocalNotification.h
  53. 0 333
      src/ios/UIApplication+APPLocalNotification.m
  54. 0 247
      src/ios/UILocalNotification+APPLocalNotification.m
  55. 6 28
      src/ios/UNNotificationRequest+APPLocalNotification.h
  56. 102 0
      src/ios/UNNotificationRequest+APPLocalNotification.m
  57. 70 0
      src/ios/UNUserNotificationCenter+APPLocalNotification.h
  58. 322 0
      src/ios/UNUserNotificationCenter+APPLocalNotification.m
  59. 0 437
      src/windows/LocalNotificationCore.js
  60. 431 202
      src/windows/LocalNotificationProxy.js
  61. BIN
      src/windows/LocalNotificationProxy/.vs/LocalNotificationProxy/v15/.suo
  62. 40 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy.sln
  63. 199 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Builder.cs
  64. 41 44
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Button.cs
  65. 88 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Every.cs
  66. 41 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/IAction.cs
  67. 56 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Input.cs
  68. 432 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Manager.cs
  69. 308 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Notification.cs
  70. 218 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Options.cs
  71. 51 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/ProgressBar.cs
  72. 357 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Trigger.cs
  73. 181 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotificationProxy.cs
  74. 151 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotificationProxy.csproj
  75. 29 0
      src/windows/LocalNotificationProxy/LocalNotificationProxy/Properties/AssemblyInfo.cs
  76. 0 443
      src/windows/LocalNotificationUtil.js
  77. 46 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveGroup.cs
  78. 49 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveHelper.cs
  79. 89 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveImage.cs
  80. 72 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveImageEnums.cs
  81. 108 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveProgressBar.cs
  82. 43 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveProgressBarBindableProperty.cs
  83. 81 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveProgressBarValue.cs
  84. 85 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveSubgroup.cs
  85. 43 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveSubgroupEnums.cs
  86. 143 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveText.cs
  87. 28 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveTextBindableProperty.cs
  88. 173 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveTextEnums.cs
  89. 45 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BaseImageHelper.cs
  90. 28 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BaseTextHelper.cs
  91. 98 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BindableValues/BindableProgressBarValue.cs
  92. 78 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BindableValues/BindableString.cs
  93. 37 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveGroup.cs
  94. 67 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveImage.cs
  95. 32 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveImageEnums.cs
  96. 30 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveProgressBar.cs
  97. 67 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveSubgroup.cs
  98. 103 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveText.cs
  99. 22 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/IAdaptiveChild.cs
  100. 22 0
      src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/IAdaptiveSubgroupChild.cs

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+/src/windows.old
+/src/windows/**/.vs
+/src/windows/**/bin
+/src/windows/**/obj

+ 3 - 0
CHANGELOG.md

@@ -3,6 +3,9 @@ ChangeLog
 
 Please also read the [Upgrade Guide](https://github.com/katzer/cordova-plugin-local-notifications/wiki/Upgrade-Guide) for more information.
 
+#### Version 0.8.5 (22.05.2017)
+- iOS 10
+
 #### Version 0.8.4 (04.01.2016)
 - Bug fixes
  - SyntaxError: missing ) after argument list

+ 1 - 1
LICENSE

@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2013-2015 appPlant UG, Inc.
+   Copyright 2013 appPlant GmbH
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.

+ 408 - 80
README.md

@@ -1,119 +1,452 @@
 
-[![npm version](https://badge.fury.io/js/de.appplant.cordova.plugin.local-notification.svg)](http://badge.fury.io/js/de.appplant.cordova.plugin.local-notification)
-[![PayPayl donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L3HKQCD9UA35A "Donate once-off to this project using Paypal")
+<p align="left"><b><a href="https://github.com/katzer/cordova-plugin-local-notifications/tree/example">SAMPLE APP</a> :point_right:</b></p>
 
-## Important notice
-1. Its been a while that the plugin has received major updates and bug fixes. My (free)time is limited and I have multiple projects to maintain. Once I've updated my background-mode plugin the local-notification plugin will get an official release supporting ios10. The ios10 branch will be merged into the master as well.
+<br>
 
-2. For the future I am looking for other opportunities to support and enhance the plugin. That might be a crowdfunding campain or adding other core maintainers. Everyone with an serious interest is welcome to contact me!
+<p align="center">
+    <img src="images/logo.png">
+</p>
 
-3. I am aware of the growing number of open issues and pull. Therefore, there's a new ticket template that can be found [here][ticket_template]. For every new issue please add an filled template. That will help rwillett and me to faster response to your issue. If you ignore this template, we'll ignore your issue.
+<p align="center">
+    <a href="https://www.npmjs.com/package/de.appplant.cordova.plugin.local-notification">
+        <img src="https://badge.fury.io/js/de.appplant.cordova.plugin.local-notification.svg" alt="npm version" />
+    </a>
+    <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L3HKQCD9UA35A "Donate once-off to this project using Paypal"">
+        <img src="https://img.shields.io/badge/paypal-donate-yellow.svg" alt="PayPayl donate button" />
+    </a>
+</p>
 
-__UPDATE (22.05.2017)__ I've released v0.8.5 with contains the code from ios10 branch as it is. I will start working on a new major version while dropping support for older SDKs. I will try to include all you PRs.
+<br>
 
-Thanks for using my plugin and for your support!</br>
-Sebastián Katzer
+> A notification is a message you display to the user outside of your app's normal UI. When you tell the system to issue a notification, it first appears as an icon in the notification area. To see the details of the notification, the user opens the notification drawer. Both the notification area and the notification drawer are system-controlled areas that the user can view at any time.
 
-Cordova Local-Notification Plugin
-=================================
+<br>
 
-The essential purpose of local notifications is to enable an application to inform its users that it has something for them — for example, a message or an upcoming appointment — when the application isn’t running in the foreground.<br>
-They are scheduled by an application and delivered on the same device.
+<img width="60%" align="right" hspace="19" vspace="12" src="https://storage.googleapis.com/material-design/publish/material_v_12/assets/0BwJzNNZmsTcKZy1YYTV3VWQzVUE/notifications-behavior-03-drawer.png"></img>
+<img width="60%" align="right" hspace="19" vspace="12" src="https://storage.googleapis.com/material-design/publish/material_v_12/assets/0Bzhp5Z4wHba3S1JWc3NkTVpjVk0/notifications-guidelines-03-optin.png"></img>
 
-<img width="35%" align="right" hspace="19" vspace="12" src="https://raw.githubusercontent.com/katzer/cordova-plugin-local-notifications/example/images/android.png"></img>
+### Notification components
 
-### How they appear to the user
-Users see notifications in the following ways:
-- Displaying an alert or banner
-- Badging the app’s icon
-- Playing a sound
+- Header area
+- Content area
+- Action area
 
+### How notifications may be noticed
 
-### Examples of Notification Usage
-Local notifications are ideally suited for applications with time-based behaviors, such as calendar and to-do list applications. Applications that run in the background for the limited period allowed by iOS might also find local notifications useful.<br>
-For example, applications that depend on servers for messages or data can poll their servers for incoming items while running in the background; if a message is ready to view or an update is ready to download, they can then present a local notification immediately to inform their users.
+- Showing a status bar icon
+- Appearing on the lock screen
+- Playing a sound or vibrating
+- Peeking onto the current screen
+- Blinking the device's LED
 
+### Supported platforms
 
-## Supported Platforms
-The current 0.8 branch supports the following platforms:
-- __iOS__ _(>= 8)_<br>
-- __Android__ _(SDK >=7)_
-- __Windows 8.1__ _(added with v0.8.2)_
-- __Windows Phone 8.1__ _(added with v0.8.2)_
-- __Windows 10__ _(added with v0.8.3)_
+- Android
+- iOS
+- Windows
 
-Find out more information [here][wiki_platforms] in our wiki.
+<br>
+<br>
 
+## Important Notice
 
-## Installation
-The plugin is installable from source and available on Cordova Plugin Registry and PhoneGap Build.
+The _x_ branch has been merged into _master_ (30.10.2017)
 
-Find out more information [here][wiki_installation] in our wiki.
+See the _0.8_ branch if you cannot upgrade. Further development for `v0.9-beta` will happen here. The `0.9-dev` and `ios10` branches are obsolate and will be removed soon.
 
+Known issues 
+- Support for Android Orio is limited yet
+- v0.9 and v0.8 aren't compatible with each other (Wont fix)
 
-## I want to get a quick overview
-All wiki pages contain samples, but for a quick overview, the sample section may be the fastest way.
+Please report bugs or missing features!
 
-Find out more information [here][wiki_samples] in our wiki.
 
+## Basics
 
-## I want to get a deep overview
-The plugin supports scheduling local notifications in various ways with a single interface. It also allows you to update, clear or cancel them. There are different interfaces to query for local notifications and a complete set of events to hook into the life cycle of local notifications.
+The plugin creates the object `cordova.plugins.notification.local` and is accessible after *deviceready* has been fired.
 
-Find out more about how to schedule single, multiple, delayed or repeating local notifications [here][wiki_schedule].<br>
-Information about events like _click_ or _trigger_ can be found [here][wiki_events].
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'My first notification',
+    text: 'Thats pretty easy...'
+});
+```
 
-To get a deep overview we recommend to read about all the topics in our [wiki][wiki] and try out the [Kitchen Sink App][wiki_kitchensink]
+<p align="center">
+    <img src="images/ios-basic.png">
+</p>
 
+The plugin allows to schedule multiple notifications at once.
 
-## I want to see the plugin in action
-The plugin offers a kitchen sink sample app. Check out the Cordova project and run the app directly from your command line or preferred IDE.
+```js
+cordova.plugins.notification.local.schedule([
+    { id: 1, title: 'My first notification' },
+    { id: 2, title: 'My first notification' }
+]);
+```
 
-Find out more information [here][wiki_kitchensink] in our wiki.
+## Properties
 
+A notification does have a set of configurable properties. Not all of them are supported across all platforms.
 
-## What's new
-We are proud to announce our newest release version 0.8.x. Besides the hard work at the office and at the weekends it contains a lot of goodies, new features and easy to use APIs.
+| Property      | Property      | Property      | Property      | Property      | Property      | Property      | Property      |
+| :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ |
+| id            | data          | actionGroupId | summary       | led           | showWhen      | channel       | actions       |
+| text          | icon          | attachments   | smallIcon     | color         | defaults      | launch        | groupSummary  |
+| title         | silent        | progressBar   | sticky        | vibrate       | priority      | mediaSession  | 
+| sound         | trigger       | group         | autoClear     | lockscreen    | number        | badge         |
 
-Find out more information [here][wiki_changelog] in our wiki.
+For their default values see:
 
+```js
+cordova.plugins.notification.local.getDefaults();
+```
+
+To change some default values:
+
+```js
+cordova.plugins.notification.local.setDefaults({
+    led: { color: '#FF00FF', on: 500, off: 500 },
+    vibrate: false
+});
+```
 
-## Sample
-The sample demonstrates how to schedule a local notification which repeats every week. The listener will be called when the user has clicked on the local notification.
+## Actions
 
-```javascript
-var date = new Date();
+The plugin knows two types of actions: _button_ and _input_.
 
+```js
 cordova.plugins.notification.local.schedule({
-    id: 1,
-    title: "Message Title",
-    message: "Message Text",
-    firstAt: date, // firstAt and at properties must be an IETF-compliant RFC 2822 timestamp
-    every: "week", // this also could be minutes i.e. 25 (int)
-    sound: "file://sounds/reminder.mp3",
-    icon: "http://icons.com/?cal_id=1",
-    data: { meetingId:"123#fg8" }
+    title: 'The big survey',
+    text: 'Are you a fan of RB Leipzig?',
+    attachments: ['file://img/rb-leipzig.jpg'],
+    actions: [
+        { id: 'yes', title: 'Yes' },
+        { id: 'no',  title: 'No' }
+    ]
 });
+```
+
+<p align="center">
+    <img width="31%" src="images/android-actions.png">
+    &nbsp;&nbsp;&nbsp;&nbsp;
+    <img width="31%" src="images/ios-actions.png">
+    &nbsp;&nbsp;&nbsp;&nbsp;
+    <img width="31%" src="images/windows-actions.png">
+</p>
 
-cordova.plugins.notification.local.on("click", function (notification) {
-    joinMeeting(notification.data.meetingId);
+### Input
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Justin Rhyss',
+    text: 'Do you want to go see a movie tonight?',
+    actions: [{
+        id: 'reply',
+        type: 'input',
+        title: 'Reply',
+        emptyText: 'Type message',
+    }, ... ]
 });
 ```
 
-Find out more information [here][wiki_samples] in our wiki.
+<p align="center">
+    <img src="images/android-reply.png">
+</p>
 
+It is recommended to pre-define action groups rather then specifying them with each new notification of the same type.
 
-## I would like to propose new features
-We appreciate any feature proposal and support for their development. Please describe them [here][feature_proposal_issue].
 
-Find out more information [here][wiki_next] in our wiki.
+```js
+cordova.plugins.notification.local.addActionGroup('yes-no', [
+    { id: 'yes', title: 'Yes' },
+    { id: 'no',  title: 'No'  }
+]);
+```
 
-## Supporting
-Your support is needed. If you use the plugin please send us a drop through the donation button.
+Once you have defined an action group, you can reference it when scheduling notifications: 
 
-Thank you!
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Justin Rhyss',
+    text: 'Do you want to go see a movie tonight?',
+    actionGroupId: 'yes-no'
+});
+```
 
-[![PayPayl donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L3HKQCD9UA35A "Donate once-off to this project using Paypal")
+### Properties
+
+Actions do have a set of configurable properties. Not all of them are supported across all platforms.
+
+| Property     | Type         | Android | iOS | Windows |
+| :----------- | :----------- | :------ | :-- | :------ |
+| id           | button+input | x       | x   | x       |
+| title        | button+input | x       | x   | x       |
+| launch       | button+input | x       | x   | x       |
+| ui           | button+input |         | x   |         |
+| needsAuth    | button+input |         | x   |         |
+| icon         | button+input | x       |     |         |
+| emptyText    | input        | x       | x   | x       |
+| submitTitle  | input        |         | x   |         |
+| editable     | input        | x       |     |         |
+| choices      | input        | x       |     |         |
+| defaultValue | input        |         |     | x       |
+
+
+## Triggers
+
+Notifications may trigger immediately or depend on calendar or location.
+
+To trigger at a fix date:
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Design team meeting',
+    text: '3:00 - 4:00 PM',
+    trigger: { at: new Date(2017, 10, 27, 15) }
+});
+```
+
+Or relative from now:
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Design team meeting',
+    trigger: { in: 1, unit: 'hour' }
+});
+```
+
+### Repeating
+
+Repeat relative from now:
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Design team meeting',
+    trigger: { every: 'day', count: 5 }
+});
+```
+
+Or trigger every time the date matches:
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Happy Birthday!!!',
+    trigger: { every: { month: 10, day: 27, hour: 9, minute: 0 } }
+});
+```
+
+### Location based
+
+To trigger when the user enters a region:
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Welcome to our office',
+    trigger: {
+        type: 'location',
+        center: [x, y],
+        radius: 15,
+        notifyOnEntry: true
+    }
+});
+```
+
+## Progress
+
+Notifications can include an animated progress indicator that shows users the status of an ongoing operation.
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Sync in progress',
+    text: 'Copied 2 of 10 files',
+    progressBar: { value: 20 }
+});
+```
+
+<p align="center">
+    <img src="images/android-progress.png">
+</p>
+
+
+## Patterns
+
+Split the text by line breaks if the message comes from a single person and just to long to show in a single line.
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'The Big Meeting',
+    text: '4:15 - 5:15 PM\nBig Conference Room',
+    smallIcon: 'res://calendar',
+    icon: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzfXKe6Yfjr6rCtR6cMPJB8CqMAYWECDtDqH-eMnerHHuXv9egrw'
+});
+```
+
+<p align="center">
+    <img src="images/android-inbox.png">
+</p>
+
+### Summarizing
+
+Instead of displaying multiple notifications, you can create one notification that summarizes them all.
+
+```js
+cordova.plugins.notification.local.schedule({
+    id: 15,
+    title: 'Chat with Irish',
+    icon: 'http://climberindonesia.com/assets/icon/ionicons-2.0.1/png/512/android-chat.png',
+    text: [
+        { message: 'I miss you' },
+        { person: 'Irish', message: 'I miss you more!' },
+        { message: 'I always miss you more by 10%' }
+    ]
+});
+```
+
+<p align="center">
+    <img src="images/android-chat.png">
+</p>
+
+To add a new message to the existing chat:
+
+```js
+cordova.plugins.notification.local.update({
+    id: 15,
+    text: [{ person: 'Irish', message: 'Bye bye' }]
+});
+```
+
+### Grouping
+
+Your app can present multiple notifications as a single group:
+
+- A parent notification displays a summary of its child notifications.
+- The child notifications are presented without duplicate header information.
+
+```js
+cordova.plugins.notification.local.schedule([
+    { id: 0, title: 'Design team meeting', ... },
+    { id: 1, summary: 'me@gmail.com', group: 'email', groupSummary: true },
+    { id: 2, title: 'Please take all my money', ... group: 'email' },
+    { id: 3, title: 'A question regarding this plugin', ... group: 'email' },
+    { id: 4, title: 'Wellcome back home', ... group: 'email' }
+]);
+```
+
+<p align="center">
+    <img src="images/android-stack.png">
+</p>
+
+
+## Permissions
+
+Each platform may require the user to grant permissions first before the app is allowed to schedule notifications.
+
+```js
+cordova.plugins.notification.local.hasPermission(function (granted) { ... });
+```
+
+If requesting via plug-in, a system dialog does pop up for the first time. Later its only possible to tweak the settings through the system settings.
+
+```js
+cordova.plugins.notification.local.requestPermission(function (granted) { ... });
+```
+
+<p align="center">
+    <img src="images/ios-permission.png">
+</p>
+
+Checking the permissions is done automatically, however it's possible to skip that.
+
+```js
+cordova.plugins.notification.local.schedule(toast, callback, scope, { skipPermission: true });
+```
+
+
+## Events
+
+The following events are supported: `add`, `trigger`, `click`, `clear`, `cancel`, `update`, `clearall` and `cancelall`.
+
+```js
+cordova.plugins.notification.local.on(event, callback, scope);
+```
+
+To unsubscribe from events:
+
+```js
+cordova.plugins.notification.local.un(event, callback, scope);
+```
+
+### Custom
+
+The plugin also fires events specified by actions.
+
+```js
+cordova.plugins.notification.local.schedule({
+    title: 'Do you want to go see a movie tonight?',
+    actions: [{ id: 'yes', title: 'Yes' }]
+});
+```
+
+The name of the event is the id of the action.
+
+```js
+cordova.plugins.notification.local.on('yes', function (notification, eopts) { ... });
+```
+
+### Fire manually
+
+Not an official interface, however its possible to manually fire events.
+
+```js
+cordova.plugins.notification.local.core.fireEvent(event, args);
+```
+
+
+## Launch Details
+
+Check the `launchDetails` to find out if the app was launched by clicking on a notification.
+
+```js
+document.addEventListener('deviceready', function () {
+    console.log(cordova.plugins.notification.local.launchDetails);
+}, false);
+```
+
+
+## Methods
+
+All methods work asynchron and accept callback methods.
+See the sample app for how to use them.
+
+| Method   | Method            | Method          | Method         | Method      |
+| :------- | :---------------- | :-------------- | :------------- | :---------- |
+| schedule | cancelAll         | isTriggered     | get            | getDefaults |
+| update   | hasPermission     | getType         | getAll         | setDefaults |
+| clear    | requestPermission | getIds          | getScheduled   | on          |
+| clearAll | isPresent         | getScheduledIds | getTriggered   | un          |
+| cancel   | isScheduled       | getTriggeredIds | addActionGroup |
+
+
+## Installation
+
+The plugin can be installed via [Cordova-CLI][CLI] and is publicly available on [NPM][npm].
+
+Execute from the projects root folder:
+
+    $ cordova plugin add cordova-plugin-local-notification
+
+Or install a specific version:
+
+    $ cordova plugin add cordova-plugin-local-notification@VERSION
+
+Or install the latest head version:
+
+    $ cordova plugin add https://github.com/katzer/cordova-plugin-local-notification.git
+
+Or install from local source:
+
+    $ cordova plugin add cordova-plugin-local-notification --nofetch --searchpath <path>
 
 
 ## Contributing
@@ -129,19 +462,14 @@ Thank you!
 
 This software is released under the [Apache 2.0 License][apache2_license].
 
-© 2013-2016 appPlant UG, Inc. All rights reserved
+Made with :yum: from Leipzig
+
+© 2013 [appPlant GmbH][appplant]
 
 
 [ticket_template]: https://github.com/katzer/cordova-plugin-local-notifications/issues/1188
 [cordova]: https://cordova.apache.org
-[wiki]: https://github.com/katzer/cordova-plugin-local-notifications/wiki
-[wiki_platforms]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/02.-Platforms
-[wiki_installation]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/03.-Installation
-[wiki_kitchensink]: https://github.com/katzer/cordova-plugin-local-notifications/tree/example
-[wiki_schedule]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/04.-Scheduling
-[wiki_events]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/09.-Events
-[wiki_samples]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/11.-Samples
-[wiki_changelog]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/Upgrade-Guide
-[wiki_next]: https://github.com/katzer/cordova-plugin-local-notifications/wiki/Feature-Requests
-[feature_proposal_issue]: https://github.com/katzer/cordova-plugin-local-notifications/issues/451
+[CLI]: http://cordova.apache.org/docs/en/edge/guide_cli_index.md.html#The%20Command-line%20Interface
+[npm]: https://www.npmjs.com/package/cordova-plugin-local-notification
 [apache2_license]: http://opensource.org/licenses/Apache-2.0
+[appplant]: http://appplant.de

BIN
images/android-actions.png


BIN
images/android-chat.png


BIN
images/android-inbox.png


BIN
images/android-progress.png


BIN
images/android-reply-2.png


BIN
images/android-reply.png


BIN
images/android-stack.png


BIN
images/ios-actions.png


BIN
images/ios-basic.png


BIN
images/ios-permission.png



BIN
images/windows-actions.png


+ 9 - 5
package.json

@@ -1,23 +1,23 @@
 {
   "name": "cordova-plugin-local-notifications",
-  "version": "0.8.4",
-  "description": "Schedule, trigger and query local notifications on iOS and Android",
+  "version": "0.0.9",
+  "description": "Schedules and queries for local notifications",
   "cordova": {
-    "id": "de.appplant.cordova.plugin.local-notification",
+    "id": "cordova-plugin-local-notifications",
     "platforms": [
       "ios",
-      "android",
       "windows"
     ]
   },
   "repository": {
     "type": "git",
-    "url": "git+https://github.com/katzer/cordova-plugin-local-notifications.git"
+    "url": "git+https://github.com/katzer/cordova-plugin-local-notifications.git#x"
   },
   "keywords": [
     "appplant",
     "notification",
     "local notification",
+    "user notification",
     "ecosystem:cordova",
     "cordova-ios",
     "cordova-android",
@@ -31,6 +31,10 @@
     {
       "name": "cordova-windows",
       "version": ">=4.2.0"
+    },
+    {
+      "name": "apple-ios",
+      "version": ">=10.0.0"
     }
   ],
   "author": "Sebastián Katzer",

+ 97 - 76
plugin.xml

@@ -1,9 +1,30 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
+<!--
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+-->
+
 <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
         xmlns:android="http://schemas.android.com/apk/res/android"
-        id="de.appplant.cordova.plugin.local-notification"
-        version="0.8.4">
+        id="cordova-plugin-local-notifications"
+        version="0.9.0">
 
     <name>LocalNotification</name>
 
@@ -11,7 +32,7 @@
 
     <repo>https://github.com/katzer/cordova-plugin-local-notifications.git</repo>
 
-    <keywords>appplant, notification, local notification</keywords>
+    <keywords>appplant, notification, local notification, user notification</keywords>
 
     <license>Apache 2.0</license>
 
@@ -19,28 +40,20 @@
 
     <!-- cordova -->
     <engines>
-        <engine name="cordova" version=">=3.6.0" />
-        <!-- TODO next version: Probably I meant cordova-lib -->
-        <!-- <engine name="cordova-plugman" version=">=4.3.0" /> -->
-        <engine name="cordova-windows" version=">=4.2.0" />
+        <engine name="cordova"         version=">=3.6.0"  />
+        <engine name="cordova-plugman" version=">=4.3.0"  />
+        <engine name="cordova-windows" version=">=4.2.0"  />
+        <engine name="cordova-android" version=">=6.0.0"  />
+        <engine name="apple-ios"       version=">=10.0.0" />
     </engines>
 
     <!-- dependencies -->
     <dependency id="cordova-plugin-device" />
-    <dependency id="cordova-plugin-app-event" />
-
-    <!-- info -->
-    <info>
-        Your support is needed. If you use the local-notification plugin please support us in order to ensure further development.
-        https://github.com/katzer/cordova-plugin-local-notifications#supporting
-
-        Thank you!
-    </info>
+    <dependency id="cordova-plugin-badge" version=">=0.8.5" />
 
     <!-- js -->
     <js-module src="www/local-notification.js" name="LocalNotification">
         <clobbers target="cordova.plugins.notification.local" />
-        <clobbers target="plugin.notification.local" />
     </js-module>
 
     <js-module src="www/local-notification-core.js" name="LocalNotification.Core">
@@ -55,7 +68,6 @@
 
     <!-- ios -->
     <platform name="ios">
-
         <config-file target="config.xml" parent="/*">
             <feature name="LocalNotification">
                 <param name="ios-package" value="APPLocalNotification" onload="true" />
@@ -63,24 +75,29 @@
             </feature>
         </config-file>
 
+        <framework src="UserNotifications.framework" />
+        <framework src="CoreLocation.framework" />
+
         <header-file src="src/ios/APPLocalNotification.h" />
         <source-file src="src/ios/APPLocalNotification.m" />
 
-        <header-file src="src/ios/APPLocalNotificationOptions.h" />
-        <source-file src="src/ios/APPLocalNotificationOptions.m" />
+        <header-file src="src/ios/APPNotificationContent.h" />
+        <source-file src="src/ios/APPNotificationContent.m" />
 
-        <header-file src="src/ios/UIApplication+APPLocalNotification.h" />
-        <source-file src="src/ios/UIApplication+APPLocalNotification.m" />
+        <header-file src="src/ios/APPNotificationOptions.h" />
+        <source-file src="src/ios/APPNotificationOptions.m" />
 
-        <header-file src="src/ios/UILocalNotification+APPLocalNotification.h" />
-        <source-file src="src/ios/UILocalNotification+APPLocalNotification.m" />
+        <header-file src="src/ios/UNUserNotificationCenter+APPLocalNotification.h" />
+        <source-file src="src/ios/UNUserNotificationCenter+APPLocalNotification.m" />
 
+        <header-file src="src/ios/UNNotificationRequest+APPLocalNotification.h" />
+        <source-file src="src/ios/UNNotificationRequest+APPLocalNotification.m" />
     </platform>
 
     <!-- android -->
     <platform name="android">
-
-        <framework src="com.android.support:support-v4:25.+" value="gradle" />
+        <framework src="com.android.support:support-v4:26.+" value="gradle" />
+        <framework src="src/android/build/localnotification.gradle" custom="true" type="gradleReference"/>
 
         <config-file target="res/xml/config.xml" parent="/*">
             <feature name="LocalNotification">
@@ -89,6 +106,15 @@
         </config-file>
 
         <config-file target="AndroidManifest.xml" parent="/manifest/application">
+            <provider
+                android:name="de.appplant.cordova.plugin.notification.util.AssetProvider"
+                android:authorities="${applicationId}.provider"
+                android:exported="false"
+                android:grantUriPermissions="true" >
+                <meta-data
+                    android:name="android.support.FILE_PROVIDER_PATHS"
+                    android:resource="@xml/localnotification_provider_paths"/>
+            </provider>
 
             <receiver
                 android:name="de.appplant.cordova.plugin.localnotification.TriggerReceiver"
@@ -99,38 +125,28 @@
                 android:exported="false" />
 
             <activity
-                android:name="de.appplant.cordova.plugin.localnotification.ClickActivity"
+                android:name="de.appplant.cordova.plugin.localnotification.ClickReceiver"
                 android:launchMode="singleInstance"
                 android:theme="@android:style/Theme.Translucent"
                 android:exported="false" />
-
-            <receiver
-                android:name="de.appplant.cordova.plugin.notification.TriggerReceiver"
-                android:exported="false" />
-
-            <receiver
-                android:name="de.appplant.cordova.plugin.notification.ClearReceiver"
-                android:exported="false" />
-
+<!--
             <receiver android:name="de.appplant.cordova.plugin.localnotification.RestoreReceiver" android:exported="false" >
                 <intent-filter>
                     <action android:name="android.intent.action.BOOT_COMPLETED" />
                 </intent-filter>
             </receiver>
-
-            <activity
-                android:name="de.appplant.cordova.plugin.notification.ClickActivity"
-                android:launchMode="singleInstance"
-                android:theme="@android:style/Theme.Translucent"
-                android:exported="false" />
-
+ -->
         </config-file>
 
         <config-file target="AndroidManifest.xml" parent="/manifest">
-            <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+            <!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
             <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
         </config-file>
 
+        <source-file
+            src="src/android/xml/localnotification_provider_paths.xml"
+            target-dir="res/xml" />
+
         <source-file
             src="src/android/LocalNotification.java"
             target-dir="src/de/appplant/cordova/plugin/localnotification" />
@@ -140,47 +156,63 @@
             target-dir="src/de/appplant/cordova/plugin/localnotification" />
 
         <source-file
-            src="src/android/ClickActivity.java"
+            src="src/android/ClickReceiver.java"
             target-dir="src/de/appplant/cordova/plugin/localnotification" />
 
         <source-file
             src="src/android/ClearReceiver.java"
             target-dir="src/de/appplant/cordova/plugin/localnotification" />
-
+<!--
         <source-file
             src="src/android/RestoreReceiver.java"
             target-dir="src/de/appplant/cordova/plugin/localnotification" />
+ -->
+        <source-file
+            src="src/android/notification/action/Action.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/action" />
 
         <source-file
-            src="src/android/notification/AbstractClearReceiver.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/action/ActionGroup.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/action" />
 
         <source-file
-            src="src/android/notification/AbstractClickActivity.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/receiver/AbstractClearReceiver.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/receiver" />
 
         <source-file
-            src="src/android/notification/AbstractRestoreReceiver.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/receiver/AbstractClickReceiver.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/receiver" />
+<!--
+        <source-file
+            src="src/android/notification/receiver/AbstractRestoreReceiver.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/receiver" />
+ -->
+        <source-file
+            src="src/android/notification/receiver/AbstractTriggerReceiver.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/receiver" />
 
         <source-file
-            src="src/android/notification/AbstractTriggerReceiver.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/trigger/DateTrigger.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/trigger" />
 
         <source-file
-            src="src/android/notification/AssetUtil.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/trigger/IntervalTrigger.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/trigger" />
 
         <source-file
-            src="src/android/notification/Builder.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/trigger/MatchTrigger.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/trigger" />
 
         <source-file
-            src="src/android/notification/ClearReceiver.java"
-            target-dir="src/de/appplant/cordova/plugin/notification" />
+            src="src/android/notification/util/AssetProvider.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/util" />
+
+        <source-file
+            src="src/android/notification/util/AssetUtil.java"
+            target-dir="src/de/appplant/cordova/plugin/notification/util" />
 
         <source-file
-            src="src/android/notification/ClickActivity.java"
+            src="src/android/notification/Builder.java"
             target-dir="src/de/appplant/cordova/plugin/notification" />
 
         <source-file
@@ -196,30 +228,19 @@
             target-dir="src/de/appplant/cordova/plugin/notification" />
 
         <source-file
-            src="src/android/notification/TriggerReceiver.java"
+            src="src/android/notification/Request.java"
             target-dir="src/de/appplant/cordova/plugin/notification" />
-
     </platform>
 
     <!-- windows -->
     <platform name="windows">
-
-        <config-file target="config.xml" parent="/*" >
-            <preference name="WindowsToastCapable" value="true" />
-        </config-file>
+        <framework src="src/windows/lib.UW/ARM/LocalNotificationProxy.winmd" target-dir="x86" arch="x86" custom="true"/>
+        <framework src="src/windows/lib.UW/ARM/LocalNotificationProxy.winmd" target-dir="x64" arch="x64" custom="true"/>
+        <framework src="src/windows/lib.UW/ARM/LocalNotificationProxy.winmd" target-dir="ARM" arch="ARM" custom="true"/>
 
         <js-module src="src/windows/LocalNotificationProxy.js" name="LocalNotification.Proxy" >
             <merges target="" />
         </js-module>
-
-        <js-module src="src/windows/LocalNotificationCore.js" name="LocalNotification.Proxy.Core" >
-            <merges target="" />
-        </js-module>
-
-        <js-module src="src/windows/LocalNotificationUtil.js" name="LocalNotification.Proxy.Util" >
-            <merges target="" />
-        </js-module>
-
     </platform>
 
 </plugin>

+ 19 - 9
src/android/ClearReceiver.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,13 +17,16 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.localnotification;
 
+import android.os.Bundle;
+
 import de.appplant.cordova.plugin.notification.Notification;
+import de.appplant.cordova.plugin.notification.receiver.AbstractClearReceiver;
+
+import static de.appplant.cordova.plugin.notification.Request.EXTRA_LAST;
 
 
 /**
@@ -31,17 +34,24 @@ import de.appplant.cordova.plugin.notification.Notification;
  * notification manually. It un-persists the cleared notification from the
  * shared preferences.
  */
-public class ClearReceiver extends de.appplant.cordova.plugin.notification.ClearReceiver {
+public class ClearReceiver extends AbstractClearReceiver {
 
     /**
      * Called when a local notification was cleared from outside of the app.
      *
-     * @param notification
-     *      Wrapper around the local notification
+     * @param notification Wrapper around the local notification.
+     * @param bundle       The bundled extras.
      */
     @Override
-    public void onClear (Notification notification) {
-        super.onClear(notification);
+    public void onClear (Notification notification, Bundle bundle) {
+        boolean isLast = bundle.getBoolean(EXTRA_LAST, false);
+
+        if (isLast) {
+            notification.cancel();
+        } else {
+            notification.clear();
+        }
+
         LocalNotification.fireEvent("clear", notification);
     }
 

+ 0 - 69
src/android/ClickActivity.java

@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-package de.appplant.cordova.plugin.localnotification;
-
-import de.appplant.cordova.plugin.notification.Builder;
-import de.appplant.cordova.plugin.notification.Notification;
-import de.appplant.cordova.plugin.notification.TriggerReceiver;
-
-/**
- * The receiver activity is triggered when a notification is clicked by a user.
- * The activity calls the background callback and brings the launch intent
- * up to foreground.
- */
-public class ClickActivity extends de.appplant.cordova.plugin.notification.ClickActivity {
-
-    /**
-     * Called when local notification was clicked by the user.
-     *
-     * @param notification
-     *      Wrapper around the local notification
-     */
-    @Override
-    public void onClick(Notification notification) {
-        LocalNotification.fireEvent("click", notification);
-
-        super.onClick(notification);
-
-        if (notification.getOptions().isOngoing())
-            return;
-
-        String event = notification.isRepeating() ? "clear" : "cancel";
-        LocalNotification.fireEvent(event, notification);
-    }
-
-    /**
-     * Build notification specified by options.
-     *
-     * @param builder
-     *      Notification builder
-     */
-    @Override
-    public Notification buildNotification (Builder builder) {
-        return builder
-                .setTriggerReceiver(TriggerReceiver.class)
-                .build();
-    }
-
-}

+ 107 - 0
src/android/ClickReceiver.java

@@ -0,0 +1,107 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.localnotification;
+
+import android.os.Bundle;
+import android.support.v4.app.RemoteInput;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import de.appplant.cordova.plugin.notification.Notification;
+import de.appplant.cordova.plugin.notification.receiver.AbstractClickReceiver;
+
+import static de.appplant.cordova.plugin.notification.Options.EXTRA_LAUNCH;
+import static de.appplant.cordova.plugin.notification.Request.EXTRA_LAST;
+
+/**
+ * The receiver activity is triggered when a notification is clicked by a user.
+ * The activity calls the background callback and brings the launch intent
+ * up to foreground.
+ */
+public class ClickReceiver extends AbstractClickReceiver {
+
+    /**
+     * Called when local notification was clicked by the user.
+     *
+     * @param notification Wrapper around the local notification.
+     * @param bundle       The bundled extras.
+     */
+    @Override
+    public void onClick(Notification notification, Bundle bundle) {
+        String action    = getAction();
+        JSONObject data  = new JSONObject();
+
+        setTextInput(action, data);
+        launchAppIf();
+
+        LocalNotification.fireEvent(action, notification, data);
+
+        if (notification.getOptions().isSticky())
+            return;
+
+        if (isLast()) {
+            notification.cancel();
+        } else {
+            notification.clear();
+        }
+    }
+
+    /**
+     * Set the text if any remote input is given.
+     *
+     * @param action The action where to look for.
+     * @param data   The object to extend.
+     */
+    private void setTextInput(String action, JSONObject data) {
+        Bundle input = RemoteInput.getResultsFromIntent(getIntent());
+
+        if (input == null)
+            return;
+
+        try {
+            data.put("text", input.getString(action));
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Launch app if requested by user.
+     */
+    private void launchAppIf() {
+        boolean doLaunch = getIntent().getBooleanExtra(EXTRA_LAUNCH, true);
+
+        if (!doLaunch)
+            return;
+
+        launchApp();
+    }
+
+    /**
+     * If the notification was the last scheduled one by request.
+     */
+    private boolean isLast() {
+        return getIntent().getBooleanExtra(EXTRA_LAST, false);
+    }
+
+}

+ 271 - 308
src/android/LocalNotification.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,13 +17,13 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.localnotification;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
+import android.util.Pair;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaInterface;
@@ -34,12 +34,17 @@ import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
 import de.appplant.cordova.plugin.notification.Manager;
 import de.appplant.cordova.plugin.notification.Notification;
+import de.appplant.cordova.plugin.notification.Options;
+import de.appplant.cordova.plugin.notification.Request;
+import de.appplant.cordova.plugin.notification.action.ActionGroup;
+
+import static de.appplant.cordova.plugin.notification.Notification.Type.SCHEDULED;
+import static de.appplant.cordova.plugin.notification.Notification.Type.TRIGGERED;
 
 /**
  * This plugin utilizes the Android AlarmManager in combination with local
@@ -56,17 +61,18 @@ public class LocalNotification extends CordovaPlugin {
     private static Boolean deviceready = false;
 
     // To inform the user about the state of the app in callbacks
-    protected static Boolean isInBackground = true;
+    private static Boolean isInBackground = true;
 
     // Queues all events before deviceready
     private static ArrayList<String> eventQueue = new ArrayList<String>();
 
+    // Launch details
+    private static Pair<Integer, String> launchDetails;
+
     /**
      * Called after plugin construction and fields have been initialized.
      * Prefer to use pluginInitialize instead since there is no value in
      * having parameters on the initialize() function.
-     *
-     * pluginInitialize is not available for cordova 3.0-3.5 !
      */
     @Override
     public void initialize (CordovaInterface cordova, CordovaWebView webView) {
@@ -76,8 +82,7 @@ public class LocalNotification extends CordovaPlugin {
     /**
      * Called when the system is about to start resuming a previous activity.
      *
-     * @param multitasking
-     *      Flag indicating if multitasking is turned on for app
+     * @param multitasking Flag indicating if multitasking is turned on for app.
      */
     @Override
     public void onPause(boolean multitasking) {
@@ -103,7 +108,7 @@ public class LocalNotification extends CordovaPlugin {
      */
     @Override
     public void onDestroy() {
-        deviceready = false;
+        deviceready    = false;
         isInBackground = true;
     }
 
@@ -117,88 +122,84 @@ public class LocalNotification extends CordovaPlugin {
      * To run on the UI thread, use:
      *     cordova.getActivity().runOnUiThread(runnable);
      *
-     * @param action
-     *      The action to execute.
-     * @param args
-     *      The exec() arguments in JSON form.
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     * @return
-     *      Whether the action was valid.
+     * @param action  The action to execute.
+     * @param args    The exec() arguments in JSON form.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
+     *
+     * @return Whether the action was valid.
      */
     @Override
     public boolean execute (final String action, final JSONArray args,
                             final CallbackContext command) throws JSONException {
 
-        Notification.setDefaultTriggerReceiver(TriggerReceiver.class);
+        if (action.equals("launch")) {
+            launch(command);
+            return true;
+        }
 
         cordova.getThreadPool().execute(new Runnable() {
             public void run() {
-                if (action.equals("hasPermission")) {
-                    hasPermission(command);
-                }
-                else if (action.equals("schedule")) {
-                    schedule(args);
+                if (action.equals("ready")) {
+                    deviceready();
+                } else
+                if (action.equalsIgnoreCase("check")) {
+                    check(command);
+                } else
+                if (action.equalsIgnoreCase("request")) {
+                    request(command);
+                } else
+                if (action.equalsIgnoreCase("actions")) {
+                    actions(args.optJSONObject(0));
                     command.success();
-                }
-                else if (action.equals("update")) {
-                    update(args);
+                } else
+                if (action.equalsIgnoreCase("schedule")) {
+                    schedule(args);
                     command.success();
-                }
-                else if (action.equals("cancel")) {
+                } else
+                // if (action.equals("update")) {
+                //     update(args);
+                //     command.success();
+                // } else
+                if (action.equals("cancel")) {
                     cancel(args);
                     command.success();
-                }
-                else if (action.equals("cancelAll")) {
+                } else
+                if (action.equals("cancelAll")) {
                     cancelAll();
                     command.success();
-                }
-                else if (action.equals("clear")) {
+                } else
+                if (action.equals("clear")) {
                     clear(args);
                     command.success();
-                }
-                else if (action.equals("clearAll")) {
+                } else
+                if (action.equals("clearAll")) {
                     clearAll();
                     command.success();
-                }
-                else if (action.equals("isPresent")) {
-                    isPresent(args.optInt(0), command);
-                }
-                else if (action.equals("isScheduled")) {
-                    isScheduled(args.optInt(0), command);
-                }
-                else if (action.equals("isTriggered")) {
-                    isTriggered(args.optInt(0), command);
-                }
-                else if (action.equals("getAllIds")) {
-                    getAllIds(command);
-                }
-                else if (action.equals("getScheduledIds")) {
-                    getScheduledIds(command);
-                }
-                else if (action.equals("getTriggeredIds")) {
-                    getTriggeredIds(command);
-                }
-                else if (action.equals("getSingle")) {
-                    getSingle(args, command);
-                }
-                else if (action.equals("getSingleScheduled")) {
-                    getSingleScheduled(args, command);
-                }
-                else if (action.equals("getSingleTriggered")) {
-                    getSingleTriggered(args, command);
-                }
-                else if (action.equals("getAll")) {
-                    getAll(args, command);
-                }
-                else if (action.equals("getScheduled")) {
-                    getScheduled(args, command);
-                }
-                else if (action.equals("getTriggered")) {
-                    getTriggered(args, command);
-                }
-                else if (action.equals("deviceready")) {
-                    deviceready();
+                } else
+                if (action.equals("type")) {
+                    type(args.optInt(0), command);
+                } else
+                if (action.equals("ids")) {
+                    ids(command);
+                } else
+                if (action.equals("scheduledIds")) {
+                    scheduledIds(command);
+                } else
+                if (action.equals("triggeredIds")) {
+                    triggeredIds(command);
+                } else
+                if (action.equals("notification")) {
+                    notification(args.optInt(0), command);
+                } else
+                if (action.equals("notifications")) {
+                    notifications(args, command);
+                } else
+                if (action.equals("scheduledNotifications")) {
+                    scheduledNotifications(command);
+                } else
+                if (action.equals("triggeredNotifications")) {
+                    triggeredNotifications(command);
                 }
             }
         });
@@ -206,67 +207,121 @@ public class LocalNotification extends CordovaPlugin {
         return true;
     }
 
+    /**
+     * Set launchDetails object.
+     *
+     * @param command The callback context used when calling back into
+     *                JavaScript.
+     */
+    @SuppressLint("DefaultLocale")
+    private void launch(CallbackContext command) {
+        if (launchDetails == null)
+            return;
+
+        JSONObject details = new JSONObject();
+
+        try {
+            details.put("id", launchDetails.first);
+            details.put("action", launchDetails.second);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        command.success(details);
+
+        launchDetails = null;
+    }
+
     /**
      * Ask if user has enabled permission for local notifications.
      *
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void hasPermission (CallbackContext command) {
-        PluginResult result = new PluginResult(PluginResult.Status.OK, getNotificationMgr().hasPermission());
+    private void check (CallbackContext command) {
+        boolean allowed     = getNotMgr().hasPermission();
+        PluginResult result = new PluginResult(PluginResult.Status.OK, allowed);
+
         command.sendPluginResult(result);
     }
 
     /**
-     * Schedule multiple local notifications.
+     * Request permission for local notifications.
      *
-     * @param notifications
-     *      Properties for each local notification
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void schedule (JSONArray notifications) {
-        for (int i = 0; i < notifications.length(); i++) {
-            JSONObject options = notifications.optJSONObject(i);
+    private void request (CallbackContext command) {
+        check(command);
+    }
 
-            Notification notification =
-                    getNotificationMgr().schedule(options, TriggerReceiver.class);
+    /**
+     * Register action group.
+     *
+     * @param args The action group spec.
+     */
+    private void actions (JSONObject args) {
+        ActionGroup group = ActionGroup.parse(cordova.getActivity(), args);
 
-            fireEvent("schedule", notification);
+        if (group != null) {
+            ActionGroup.register(group);
         }
     }
 
     /**
-     * Update multiple local notifications.
+     * Schedule multiple local notifications.
      *
-     * @param updates
-     *      Notification properties including their IDs
+     * @param notifications The notifications to schedule.
      */
-    private void update (JSONArray updates) {
-        for (int i = 0; i < updates.length(); i++) {
-            JSONObject update = updates.optJSONObject(i);
-            int id = update.optInt("id", 0);
+    private void schedule (JSONArray notifications) {
+        Manager mgr = getNotMgr();
 
-            Notification notification =
-                    getNotificationMgr().update(id, update, TriggerReceiver.class);
+        for (int i = 0; i < notifications.length(); i++) {
+            JSONObject dict = notifications.optJSONObject(i);
+            Options options = new Options(dict);
+            Request request = new Request(options);
 
-            if (notification == null)
-                continue;
+            Notification notification =
+                    mgr.schedule(request, TriggerReceiver.class);
 
-            fireEvent("update", notification);
+            if (notification != null) {
+                fireEvent("add", notification);
+            }
         }
     }
 
+    // /**
+    //  * Update multiple local notifications.
+    //  *
+    //  * @param updates
+    //  *      Notification properties including their IDs
+    //  */
+    // private void update (JSONArray updates) {
+    //     for (int i = 0; i < updates.length(); i++) {
+    //         JSONObject update = updates.optJSONObject(i);
+    //         int id = update.optInt("id", 0);
+
+    //         Notification notification =
+    //                 getNotMgr().update(id, update, TriggerReceiver.class);
+
+    //         if (notification == null)
+    //             continue;
+
+    //         fireEvent("update", notification);
+    //     }
+    // }
+
     /**
      * Cancel multiple local notifications.
      *
-     * @param ids
-     *      Set of local notification IDs
+     * @param ids Set of local notification IDs
      */
     private void cancel (JSONArray ids) {
         for (int i = 0; i < ids.length(); i++) {
             int id = ids.optInt(i, 0);
 
             Notification notification =
-                    getNotificationMgr().cancel(id);
+                    getNotMgr().cancel(id);
 
             if (notification == null)
                 continue;
@@ -279,22 +334,21 @@ public class LocalNotification extends CordovaPlugin {
      * Cancel all scheduled notifications.
      */
     private void cancelAll() {
-        getNotificationMgr().cancelAll();
+        getNotMgr().cancelAll();
         fireEvent("cancelall");
     }
 
     /**
      * Clear multiple local notifications without canceling them.
      *
-     * @param ids
-     *      Set of local notification IDs
+     * @param ids Set of local notification IDs
      */
     private void clear(JSONArray ids){
         for (int i = 0; i < ids.length(); i++) {
             int id = ids.optInt(i, 0);
 
             Notification notification =
-                    getNotificationMgr().clear(id);
+                    getNotMgr().clear(id);
 
             if (notification == null)
                 continue;
@@ -307,224 +361,126 @@ public class LocalNotification extends CordovaPlugin {
      * Clear all triggered notifications without canceling them.
      */
     private void clearAll() {
-        getNotificationMgr().clearAll();
+        getNotMgr().clearAll();
         fireEvent("clearall");
     }
 
     /**
-     * If a notification with an ID is present.
+     * Get the type of the notification (unknown, scheduled, triggered).
      *
-     * @param id
-     *      Notification ID
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param id      The ID of the notification to check.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void isPresent (int id, CallbackContext command) {
-        boolean exist = getNotificationMgr().exist(id);
+    private void type (int id, CallbackContext command) {
+        Notification toast = getNotMgr().get(id);
 
-        PluginResult result = new PluginResult(
-                PluginResult.Status.OK, exist);
-
-        command.sendPluginResult(result);
-    }
-
-    /**
-     * If a notification with an ID is scheduled.
-     *
-     * @param id
-     *      Notification ID
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     */
-    private void isScheduled (int id, CallbackContext command) {
-        boolean exist = getNotificationMgr().exist(
-                id, Notification.Type.SCHEDULED);
-
-        PluginResult result = new PluginResult(
-                PluginResult.Status.OK, exist);
-
-        command.sendPluginResult(result);
-    }
-
-    /**
-     * If a notification with an ID is triggered.
-     *
-     * @param id
-     *      Notification ID
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     */
-    private void isTriggered (int id, CallbackContext command) {
-        boolean exist = getNotificationMgr().exist(
-                id, Notification.Type.TRIGGERED);
-
-        PluginResult result = new PluginResult(
-                PluginResult.Status.OK, exist);
+        if (toast == null) {
+            command.success("unknown");
+            return;
+        }
 
-        command.sendPluginResult(result);
+        switch (toast.getType()) {
+            case SCHEDULED:
+                command.success("scheduled");
+                break;
+            case TRIGGERED:
+                command.success("triggered");
+                break;
+            default:
+                command.success("unknown");
+                break;
+        }
     }
 
     /**
      * Set of IDs from all existent notifications.
      *
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getAllIds (CallbackContext command) {
-        List<Integer> ids = getNotificationMgr().getIds();
-
+    private void ids (CallbackContext command) {
+        List<Integer> ids = getNotMgr().getIds();
         command.success(new JSONArray(ids));
     }
 
     /**
      * Set of IDs from all scheduled notifications.
      *
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getScheduledIds (CallbackContext command) {
-        List<Integer> ids = getNotificationMgr().getIdsByType(
-                Notification.Type.SCHEDULED);
-
+    private void scheduledIds (CallbackContext command) {
+        List<Integer> ids = getNotMgr().getIdsByType(SCHEDULED);
         command.success(new JSONArray(ids));
     }
 
     /**
      * Set of IDs from all triggered notifications.
      *
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getTriggeredIds (CallbackContext command) {
-        List<Integer> ids = getNotificationMgr().getIdsByType(
-                Notification.Type.TRIGGERED);
-
+    private void triggeredIds (CallbackContext command) {
+        List<Integer> ids = getNotMgr().getIdsByType(TRIGGERED);
         command.success(new JSONArray(ids));
     }
 
     /**
      * Options from local notification.
      *
-     * @param ids
-     *      Set of local notification IDs
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     */
-    private void getSingle (JSONArray ids, CallbackContext command) {
-        getOptions(ids.optString(0), Notification.Type.ALL, command);
-    }
-
-    /**
-     * Options from scheduled notification.
-     *
-     * @param ids
-     *      Set of local notification IDs
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param id      The ID of the notification.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getSingleScheduled (JSONArray ids, CallbackContext command) {
-        getOptions(ids.optString(0), Notification.Type.SCHEDULED, command);
-    }
+    private void notification (int id, CallbackContext command) {
+        Options options = getNotMgr().getOptions(id);
 
-    /**
-     * Options from triggered notification.
-     *
-     * @param ids
-     *      Set of local notification IDs
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     */
-    private void getSingleTriggered (JSONArray ids, CallbackContext command) {
-        getOptions(ids.optString(0), Notification.Type.TRIGGERED, command);
+        if (options != null) {
+            command.success(options.getDict());
+        } else {
+            command.success();
+        }
     }
 
     /**
      * Set of options from local notification.
      *
-     * @param ids
-     *      Set of local notification IDs
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param ids     Set of local notification IDs.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getAll (JSONArray ids, CallbackContext command) {
-        getOptions(ids, Notification.Type.ALL, command);
-    }
+    private void notifications (JSONArray ids, CallbackContext command) {
+        List<JSONObject> options;
 
-    /**
-     * Set of options from scheduled notifications.
-     *
-     * @param ids
-     *      Set of local notification IDs
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     */
-    private void getScheduled (JSONArray ids, CallbackContext command) {
-        getOptions(ids, Notification.Type.SCHEDULED, command);
-    }
+        if (ids.length() == 0) {
+            options = getNotMgr().getOptions();
+        } else {
+            options = getNotMgr().getOptionsById(toList(ids));
+        }
 
-    /**
-     * Set of options from triggered notifications.
-     *
-     * @param ids
-     *      Set of local notification IDs
-     * @param command
-     *      The callback context used when calling back into JavaScript.
-     */
-    private void getTriggered (JSONArray ids, CallbackContext command) {
-        getOptions(ids, Notification.Type.TRIGGERED, command);
+        command.success(new JSONArray(options));
     }
 
     /**
-     * Options from local notification.
+     * Set of options from scheduled notifications.
      *
-     * @param id
-     *      Set of local notification IDs
-     * @param type
-     *      The local notification life cycle type
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getOptions (String id, Notification.Type type,
-                             CallbackContext command) {
-
-        JSONArray ids = new JSONArray().put(id);
-        PluginResult result;
-
-        List<JSONObject> options =
-                getNotificationMgr().getOptionsBy(type, toList(ids));
-
-        if (options.isEmpty()) {
-            // Status.NO_RESULT led to no callback invocation :(
-            // Status.OK        led to no NPE and crash
-            result = new PluginResult(PluginResult.Status.NO_RESULT);
-        } else {
-            result = new PluginResult(PluginResult.Status.OK, options.get(0));
-        }
-
-        command.sendPluginResult(result);
+    private void scheduledNotifications (CallbackContext command) {
+        List<JSONObject> options = getNotMgr().getOptionsByType(SCHEDULED);
+        command.success(new JSONArray(options));
     }
 
     /**
-     * Set of options from local notifications.
+     * Set of options from triggered notifications.
      *
-     * @param ids
-     *      Set of local notification IDs
-     * @param type
-     *      The local notification life cycle type
-     * @param command
-     *      The callback context used when calling back into JavaScript.
+     * @param command The callback context used when calling back into
+     *                JavaScript.
      */
-    private void getOptions (JSONArray ids, Notification.Type type,
-                             CallbackContext command) {
-
-        List<JSONObject> options;
-
-        if (ids.length() == 0) {
-            options = getNotificationMgr().getOptionsByType(type);
-        } else {
-            options = getNotificationMgr().getOptionsBy(type, toList(ids));
-        }
-
+    private void triggeredNotifications (CallbackContext command) {
+        List<JSONObject> options = getNotMgr().getOptionsByType(TRIGGERED);
         command.success(new JSONArray(options));
     }
 
@@ -545,40 +501,64 @@ public class LocalNotification extends CordovaPlugin {
     /**
      * Fire given event on JS side. Does inform all event listeners.
      *
-     * @param event
-     *      The event name
+     * @param event The event name.
      */
     private void fireEvent (String event) {
-        fireEvent(event, null);
+        fireEvent(event, null, new JSONObject());
     }
 
     /**
      * Fire given event on JS side. Does inform all event listeners.
      *
-     * @param event
-     *      The event name
-     * @param notification
-     *      Optional local notification to pass the id and properties.
+     * @param event        The event name.
+     * @param notification Optional notification to pass with.
      */
     static void fireEvent (String event, Notification notification) {
-        String state = getApplicationState();
-        String params = "\"" + state + "\"";
+        fireEvent(event, notification, new JSONObject());
+    }
+
+    /**
+     * Fire given event on JS side. Does inform all event listeners.
+     *
+     * @param event The event name.
+     * @param toast Optional notification to pass with.
+     * @param data  Event object with additional data.
+     */
+    static void fireEvent (String event, Notification toast, JSONObject data) {
+        String params, js;
+
+        try {
+            data.put("event", event);
+            data.put("foreground", !isInBackground);
+            data.put("queued", !deviceready);
+
+            if (toast != null) {
+                data.put("notification", toast.getId());
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
 
-        if (notification != null) {
-            params = notification.toString() + "," + params;
+        if (toast != null) {
+            params = toast.toString() + "," + data.toString();
+        } else {
+            params = data.toString();
         }
 
-        String js = "cordova.plugins.notification.local.core.fireEvent(" +
+        js = "cordova.plugins.notification.local.core.fireEvent(" +
                 "\"" + event + "\"," + params + ")";
 
+        if (launchDetails == null && !deviceready && toast != null) {
+            launchDetails = new Pair<Integer, String>(toast.getId(), event);
+        }
+
         sendJavascript(js);
     }
 
     /**
      * Use this instead of deprecated sendJavascript
      *
-     * @param js
-     *       JS code snippet as string
+     * @param js JS code snippet as string.
      */
     private static synchronized void sendJavascript(final String js) {
 
@@ -586,28 +566,21 @@ public class LocalNotification extends CordovaPlugin {
             eventQueue.add(js);
             return;
         }
-        Runnable jsLoader = new Runnable() {
+
+        ((Activity)(webView.getContext())).runOnUiThread(new Runnable() {
             public void run() {
                 webView.loadUrl("javascript:" + js);
             }
-        };
-        try {
-            Method post = webView.getClass().getMethod("post",Runnable.class);
-            post.invoke(webView,jsLoader);
-        } catch(Exception e) {
-
-            ((Activity)(webView.getContext())).runOnUiThread(jsLoader);
-        }
+        });
     }
 
     /**
      * Convert JSON array of integers to List.
      *
-     * @param ary
-     *      Array of integers
+     * @param ary Array of integers.
      */
     private List<Integer> toList (JSONArray ary) {
-        ArrayList<Integer> list = new ArrayList<Integer>();
+        List<Integer> list = new ArrayList<Integer>();
 
         for (int i = 0; i < ary.length(); i++) {
             list.add(ary.optInt(i));
@@ -616,20 +589,10 @@ public class LocalNotification extends CordovaPlugin {
         return list;
     }
 
-    /**
-     * Current application state.
-     *
-     * @return
-     *      "background" or "foreground"
-     */
-    static String getApplicationState () {
-        return isInBackground ? "background" : "foreground";
-    }
-
     /**
      * Notification manager instance.
      */
-    private Manager getNotificationMgr() {
+    private Manager getNotMgr() {
         return Manager.getInstance(cordova.getActivity());
     }
 

+ 21 - 18
src/android/TriggerReceiver.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,14 +17,16 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.localnotification;
 
+import android.os.Bundle;
+
 import de.appplant.cordova.plugin.notification.Builder;
+import de.appplant.cordova.plugin.notification.Manager;
 import de.appplant.cordova.plugin.notification.Notification;
+import de.appplant.cordova.plugin.notification.receiver.AbstractTriggerReceiver;
 
 /**
  * The alarm receiver is triggered when a scheduled alarm is fired. This class
@@ -32,38 +34,39 @@ import de.appplant.cordova.plugin.notification.Notification;
  * Android notification bar. The notification uses the default notification
  * sound and it vibrates the phone.
  */
-public class TriggerReceiver extends de.appplant.cordova.plugin.notification.TriggerReceiver {
+public class TriggerReceiver extends AbstractTriggerReceiver {
 
     /**
      * Called when a local notification was triggered. Does present the local
      * notification, re-schedule the alarm if necessary and fire trigger event.
      *
-     * @param notification
-     *      Wrapper around the local notification
-     * @param updated
-     *      If an update has triggered or the original
+     * @param notification Wrapper around the local notification.
+     * @param bundle       The bundled extras.
      */
     @Override
-    public void onTrigger (Notification notification, boolean updated) {
-        super.onTrigger(notification, updated);
+    public void onTrigger (Notification notification, Bundle bundle) {
+        int badge = notification.getOptions().getBadgeNumber();
 
-        if (!updated) {
-            LocalNotification.fireEvent("trigger", notification);
+        if (badge > 0) {
+            Manager.getInstance(notification.getContext()).setBadge(badge);
         }
+
+        notification.show();
+        LocalNotification.fireEvent("trigger", notification);
     }
 
     /**
      * Build notification specified by options.
      *
-     * @param builder
-     *      Notification builder
+     * @param builder Notification builder.
+     * @param bundle  The bundled extras.
      */
     @Override
-    public Notification buildNotification (Builder builder) {
+    public Notification buildNotification (Builder builder, Bundle bundle) {
         return builder
-                .setTriggerReceiver(TriggerReceiver.class)
-                .setClickActivity(ClickActivity.class)
+                .setClickActivity(ClickReceiver.class)
                 .setClearReceiver(ClearReceiver.class)
+                .setExtras(bundle)
                 .build();
     }
 

+ 28 - 0
src/android/build/localnotification.gradle

@@ -0,0 +1,28 @@
+/*
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+repositories {
+    mavenCentral()
+}
+
+if (!project.ext.has('appShortcutBadgerVersion')) {
+    ext.appShortcutBadgerVersion = '1.1.19'
+}
+
+dependencies {
+    compile "me.leolin:ShortcutBadger:${appShortcutBadgerVersion}@aar"
+}

+ 0 - 122
src/android/notification/AbstractTriggerReceiver.java

@@ -1,122 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-package de.appplant.cordova.plugin.notification;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Calendar;
-
-/**
- * Abstract broadcast receiver for local notifications. Creates the
- * notification options and calls the event functions for further proceeding.
- */
-abstract public class AbstractTriggerReceiver extends BroadcastReceiver {
-
-    /**
-     * Called when an alarm was triggered.
-     *
-     * @param context
-     *      Application context
-     * @param intent
-     *      Received intent with content data
-     */
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Bundle bundle  = intent.getExtras();
-        Options options;
-
-        try {
-            String data = bundle.getString(Options.EXTRA);
-            JSONObject dict = new JSONObject(data);
-
-            options = new Options(context).parse(dict);
-        } catch (JSONException e) {
-            e.printStackTrace();
-            return;
-        }
-
-        if (options == null)
-            return;
-
-        if (isFirstAlarmInFuture(options))
-            return;
-
-        Builder builder = new Builder(options);
-        Notification notification = buildNotification(builder);
-        boolean updated = notification.isUpdate(false);
-
-        onTrigger(notification, updated);
-    }
-
-    /**
-     * Called when a local notification was triggered.
-     *
-     * @param notification
-     *      Wrapper around the local notification
-     * @param updated
-     *      If an update has triggered or the original
-     */
-    abstract public void onTrigger (Notification notification, boolean updated);
-
-    /**
-     * Build notification specified by options.
-     *
-     * @param builder
-     *      Notification builder
-     */
-    abstract public Notification buildNotification (Builder builder);
-
-    /*
-     * If you set a repeating alarm at 11:00 in the morning and it
-     * should trigger every morning at 08:00 o'clock, it will
-     * immediately fire. E.g. Android tries to make up for the
-     * 'forgotten' reminder for that day. Therefore we ignore the event
-     * if Android tries to 'catch up'.
-     */
-    private Boolean isFirstAlarmInFuture (Options options) {
-        Notification notification = new Builder(options).build();
-
-        if (!notification.isRepeating())
-            return false;
-
-        Calendar now    = Calendar.getInstance();
-        Calendar alarm  = Calendar.getInstance();
-
-        alarm.setTime(notification.getOptions().getTriggerDate());
-
-        int alarmHour   = alarm.get(Calendar.HOUR_OF_DAY);
-        int alarmMin    = alarm.get(Calendar.MINUTE);
-        int currentHour = now.get(Calendar.HOUR_OF_DAY);
-        int currentMin  = now.get(Calendar.MINUTE);
-
-        return (currentHour != alarmHour && currentMin != alarmMin);
-    }
-
-}

+ 265 - 73
src/android/notification/Builder.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,8 +17,6 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.notification;
@@ -26,18 +24,27 @@ package de.appplant.cordova.plugin.notification;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.net.Uri;
+import android.os.Bundle;
 import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.MessagingStyle.Message;
+import android.support.v4.media.app.NotificationCompat.MediaStyle;
+import android.support.v4.media.session.MediaSessionCompat;
 
-import org.json.JSONObject;
-
+import java.util.List;
 import java.util.Random;
 
+import de.appplant.cordova.plugin.notification.action.Action;
+
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+
 /**
  * Builder class for local notifications. Build fully configured local
  * notification specified by JSON object passed from JS side.
  */
-public class Builder {
+public final class Builder {
 
     // Application context passed by constructor
     private final Context context;
@@ -45,55 +52,32 @@ public class Builder {
     // Notification options passed by JS
     private final Options options;
 
-    // Receiver to handle the trigger event
-    private Class<?> triggerReceiver;
+    // To generate unique request codes
+    private final Random random = new Random();
 
     // Receiver to handle the clear event
-    private Class<?> clearReceiver = ClearReceiver.class;
+    private Class<?> clearReceiver;
 
     // Activity to handle the click event
-    private Class<?> clickActivity = ClickActivity.class;
+    private Class<?> clickActivity;
 
-    /**
-     * Constructor
-     *
-     * @param context
-     *      Application context
-     * @param options
-     *      Notification options
-     */
-    public Builder(Context context, JSONObject options) {
-        this.context = context;
-        this.options = new Options(context).parse(options);
-    }
+    // Additional extras to merge into each intent
+    private Bundle extras;
 
     /**
      * Constructor
      *
-     * @param options
-     *      Notification options
+     * @param options Notification options
      */
     public Builder(Options options) {
         this.context = options.getContext();
         this.options = options;
     }
 
-    /**
-     * Set trigger receiver.
-     *
-     * @param receiver
-     *      Broadcast receiver
-     */
-    public Builder setTriggerReceiver(Class<?> receiver) {
-        this.triggerReceiver = receiver;
-        return this;
-    }
-
     /**
      * Set clear receiver.
      *
-     * @param receiver
-     *      Broadcast receiver
+     * @param receiver Broadcast receiver for the clear event.
      */
     public Builder setClearReceiver(Class<?> receiver) {
         this.clearReceiver = receiver;
@@ -103,65 +87,223 @@ public class Builder {
     /**
      * Set click activity.
      *
-     * @param activity
-     *      Activity
+     * @param activity The activity to handler the click event.
      */
     public Builder setClickActivity(Class<?> activity) {
         this.clickActivity = activity;
         return this;
     }
 
+    /**
+     * Set bundle extras.
+     *
+     * @param extras The bundled extras to merge into.
+     */
+    public Builder setExtras(Bundle extras) {
+        this.extras = extras;
+        return this;
+    }
+
     /**
      * Creates the notification with all its options passed through JS.
+     *
+     * @return The final notification to display.
      */
     public Notification build() {
-        Uri sound     = options.getSoundUri();
-        int smallIcon = options.getSmallIcon();
-        int ledColor  = options.getLedColor();
-        long[] vibrate = options.getVibrate();
         NotificationCompat.Builder builder;
 
-        builder = new NotificationCompat.Builder(context)
-                .setDefaults(0)
+        if (options.isSilent()) {
+            return new Notification(context, options);
+        }
+
+        int smallIcon = options.getSmallIcon();
+        Uri sound     = options.getSound();
+        Bundle extras = new Bundle();
+
+        extras.putInt(Notification.EXTRA_ID, options.getId());
+        extras.putString(Options.EXTRA_SOUND, sound.toString());
+
+        builder = new NotificationCompat.Builder(context, Manager.CHANNEL_ID)
+                .setDefaults(options.getDefaults())
+                .setExtras(extras)
+                .setOnlyAlertOnce(true)
+                .setChannelId(options.getChannel())
                 .setContentTitle(options.getTitle())
                 .setContentText(options.getText())
-                .setNumber(options.getBadgeNumber())
                 .setTicker(options.getText())
+                .setNumber(options.getNumber())
                 .setAutoCancel(options.isAutoClear())
-                .setOngoing(options.isOngoing())
-                .setColor(options.getColor());
+                .setOngoing(options.isSticky())
+                .setColor(options.getColor())
+                .setSound(sound)
+                .setVisibility(options.getVisibility())
+                .setPriority(options.getPriority())
+                .setShowWhen(options.getShowWhen())
+                .setUsesChronometer(options.isWithProgressBar())
+                .setGroup(options.getGroup())
+                .setGroupSummary(options.getGroupSummary())
+                .setLights(options.getLedColor(), options.getLedOn(), options.getLedOff());
+
+        if (options.isWithProgressBar()) {
+            builder.setProgress(
+                    options.getProgressMaxValue(),
+                    options.getProgressValue(),
+                    options.isIndeterminateProgress());
+        }
 
-        if (ledColor != 0) {
-            builder.setLights(ledColor, options.getLedOnTime(), options.getLedOffTime());
+        if (smallIcon != 0) {
+            builder.setSmallIcon(smallIcon);
+            builder.setLargeIcon(options.getLargeIcon());
+        } else {
+            builder.setSmallIcon(options.getIcon());
         }
 
-        if (sound != null) {
-            builder.setSound(sound);
+        applyStyle(builder);
+        applyActions(builder);
+        applyDeleteReceiver(builder);
+        applyContentReceiver(builder);
+
+        return new Notification(context, options, builder);
+    }
+
+    /**
+     * Find out and set the notification style.
+     *
+     * @param builder Local notification builder instance.
+     */
+    private void applyStyle(NotificationCompat.Builder builder) {
+        Message[] messages = options.getMessages();
+        String summary     = options.getSummary();
+
+        if (messages != null) {
+            applyMessagingStyle(builder, messages);
+            return;
         }
 
-        if (vibrate != null) {
-            builder.setVibrate(vibrate);
+        MediaSessionCompat.Token token = options.getMediaSessionToken();
+
+        if (token != null) {
+            applyMediaStyle(builder, token);
+            return;
         }
 
-        if (smallIcon == 0) {
-            builder.setSmallIcon(options.getIcon());
-        } else {
-            builder.setSmallIcon(options.getSmallIcon());
-            builder.setLargeIcon(options.getIconBitmap());
+        List<Bitmap> pics = options.getAttachments();
+
+        if (pics.size() > 0) {
+            applyBigPictureStyle(builder, pics);
+            return;
         }
 
-        applyDeleteReceiver(builder);
-        applyContentReceiver(builder);
+        String text = options.getText();
+
+        if (text != null && text.contains("\n")) {
+            applyInboxStyle(builder);
+            return;
+        }
+
+        if (text == null || summary == null && text.length() < 45)
+            return;
+
+        applyBigTextStyle(builder);
+    }
+
+    /**
+     * Apply inbox style.
+     *
+     * @param builder  Local notification builder instance.
+     * @param messages The messages to add to the conversation.
+     */
+    private void applyMessagingStyle(NotificationCompat.Builder builder,
+                                     Message[] messages) {
+
+        NotificationCompat.MessagingStyle style;
+
+        style = new NotificationCompat.MessagingStyle("Me")
+                .setConversationTitle(options.getTitle());
+
+        for (Message msg : messages) {
+            style.addMessage(msg);
+        }
+
+        builder.setStyle(style);
+    }
+
+    /**
+     * Apply inbox style.
+     *
+     * @param builder Local notification builder instance.
+     * @param pics    The pictures to show.
+     */
+    private void applyBigPictureStyle(NotificationCompat.Builder builder,
+                                      List<Bitmap> pics) {
+
+        NotificationCompat.BigPictureStyle style;
+        String summary = options.getSummary();
+        String text    = options.getText();
+
+        style = new NotificationCompat.BigPictureStyle(builder)
+                .setSummaryText(summary == null ? text : summary)
+                .bigPicture(pics.get(0));
+
+        builder.setStyle(style);
+    }
+
+    /**
+     * Apply inbox style.
+     *
+     * @param builder Local notification builder instance.
+     */
+    private void applyInboxStyle(NotificationCompat.Builder builder) {
+        NotificationCompat.InboxStyle style;
+        String text = options.getText();
+
+        style = new NotificationCompat.InboxStyle(builder)
+                .setSummaryText(options.getSummary());
+
+        for (String line : text.split("\n")) {
+            style.addLine(line);
+        }
+
+        builder.setStyle(style);
+    }
+
+    /**
+     * Apply big text style.
+     *
+     * @param builder Local notification builder instance.
+     */
+    private void applyBigTextStyle(NotificationCompat.Builder builder) {
+        NotificationCompat.BigTextStyle style;
 
-        return new Notification(context, options, builder, triggerReceiver);
+        style = new NotificationCompat.BigTextStyle(builder)
+                .setSummaryText(options.getSummary())
+                .bigText(options.getText());
+
+        builder.setStyle(style);
+    }
+
+    /**
+     * Apply media style.
+     *
+     * @param builder Local notification builder instance.
+     * @param token   The media session token.
+     */
+    private void applyMediaStyle(NotificationCompat.Builder builder,
+                                 MediaSessionCompat.Token token) {
+        MediaStyle style;
+
+        style = new MediaStyle(builder)
+                .setMediaSession(token)
+                .setShowActionsInCompactView(1);
+
+        builder.setStyle(style);
     }
 
     /**
      * Set intent to handle the delete event. Will clean up some persisted
      * preferences.
      *
-     * @param builder
-     *      Local notification builder instance
+     * @param builder Local notification builder instance.
      */
     private void applyDeleteReceiver(NotificationCompat.Builder builder) {
 
@@ -169,11 +311,14 @@ public class Builder {
             return;
 
         Intent intent = new Intent(context, clearReceiver)
-                .setAction(options.getIdStr())
-                .putExtra(Options.EXTRA, options.toString());
+                .putExtras(extras)
+                .setAction(options.getIdentifier())
+                .putExtra(Notification.EXTRA_ID, options.getId());
+
+        int reqCode = random.nextInt();
 
         PendingIntent deleteIntent = PendingIntent.getBroadcast(
-                context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+                context, reqCode, intent, FLAG_UPDATE_CURRENT);
 
         builder.setDeleteIntent(deleteIntent);
     }
@@ -182,8 +327,7 @@ public class Builder {
      * Set intent to handle the click event. Will bring the app to
      * foreground.
      *
-     * @param builder
-     *      Local notification builder instance
+     * @param builder Local notification builder instance.
      */
     private void applyContentReceiver(NotificationCompat.Builder builder) {
 
@@ -191,15 +335,63 @@ public class Builder {
             return;
 
         Intent intent = new Intent(context, clickActivity)
-                .putExtra(Options.EXTRA, options.toString())
+                .putExtras(extras)
+                .putExtra(Notification.EXTRA_ID, options.getId())
+                .putExtra(Action.EXTRA_ID, Action.CLICK_ACTION_ID)
+                .putExtra(Options.EXTRA_LAUNCH, options.isLaunchingApp())
                 .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
 
-        int reqCode = new Random().nextInt();
+        int reqCode = random.nextInt();
 
         PendingIntent contentIntent = PendingIntent.getActivity(
-                context, reqCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+                context, reqCode, intent, FLAG_UPDATE_CURRENT);
 
         builder.setContentIntent(contentIntent);
     }
 
+    /**
+     * Add all actions to the builder if there are any actions.
+     *
+     * @param builder Local notification builder instance.
+     */
+    private void applyActions (NotificationCompat.Builder builder) {
+        Action[] actions = options.getActions();
+        NotificationCompat.Action.Builder btn;
+
+        if (actions == null || actions.length == 0)
+            return;
+
+        for (Action action : actions) {
+             btn = new NotificationCompat.Action.Builder(
+                     action.getIcon(), action.getTitle(),
+                     getPendingIntentForAction(action));
+
+            if (action.isWithInput()) {
+                btn.addRemoteInput(action.getInput());
+            }
+
+            builder.addAction(btn.build());
+        }
+    }
+
+    /**
+     * Returns a new PendingIntent for a notification action, including the
+     * action's identifier.
+     *
+     * @param action Notification action needing the PendingIntent
+     */
+    private PendingIntent getPendingIntentForAction (Action action) {
+        Intent intent = new Intent(context, clickActivity)
+                .putExtras(extras)
+                .putExtra(Notification.EXTRA_ID, options.getId())
+                .putExtra(Action.EXTRA_ID, action.getId())
+                .putExtra(Options.EXTRA_LAUNCH, action.isLaunchingApp())
+                .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+
+        int reqCode = random.nextInt();
+
+        return PendingIntent.getActivity(
+                context, reqCode, intent, FLAG_CANCEL_CURRENT);
+    }
+
 }

+ 0 - 61
src/android/notification/ClickActivity.java

@@ -1,61 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-package de.appplant.cordova.plugin.notification;
-
-/**
- * The receiver activity is triggered when a notification is clicked by a user.
- * The activity calls the background callback and brings the launch intent
- * up to foreground.
- */
-public class ClickActivity extends AbstractClickActivity {
-
-    /**
-     * Called when local notification was clicked by the user. Will
-     * move the app to foreground.
-     *
-     * @param notification
-     *      Wrapper around the local notification
-     */
-    @Override
-    public void onClick(Notification notification) {
-        launchApp();
-
-        if (notification.isRepeating()) {
-            notification.clear();
-        } else {
-            notification.cancel();
-        }
-    }
-
-    /**
-     * Build notification specified by options.
-     *
-     * @param builder
-     *      Notification builder
-     */
-    public Notification buildNotification (Builder builder) {
-        return builder.build();
-    }
-
-}

+ 193 - 211
src/android/notification/Manager.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,53 +17,62 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.notification;
 
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.service.notification.StatusBarNotification;
 import android.support.v4.app.NotificationManagerCompat;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
-import static de.appplant.cordova.plugin.notification.Notification.PREF_KEY;
+import de.appplant.cordova.plugin.badge.BadgeImpl;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.O;
+import static android.support.v4.app.NotificationManagerCompat.IMPORTANCE_DEFAULT;
+import static de.appplant.cordova.plugin.notification.Notification.PREF_KEY_ID;
+import static de.appplant.cordova.plugin.notification.Notification.Type.TRIGGERED;
 
 /**
  * Central way to access all or single local notifications set by specific
  * state like triggered or scheduled. Offers shortcut ways to schedule,
  * cancel or clear local notifications.
  */
-public class Manager {
+public final class Manager {
+
+    // TODO: temporary
+    static final String CHANNEL_ID = "default-channel-id";
+
+    // TODO: temporary
+    private static final CharSequence CHANNEL_NAME = "Default channel";
 
-    // Context passed through constructor and used for notification builder.
+    // The application context
     private Context context;
 
     /**
      * Constructor
      *
-     * @param context
-     *      Application context
+     * @param context Application context
      */
-    private Manager(Context context){
+    private Manager(Context context) {
         this.context = context;
+        createDefaultChannel();
     }
 
     /**
      * Static method to retrieve class instance.
      *
-     * @param context
-     *      Application context
+     * @param context Application context
      */
     public static Manager getInstance(Context context) {
         return new Manager(context);
@@ -73,110 +82,114 @@ public class Manager {
      * Check if app has local notification permission.
      */
     public boolean hasPermission () {
-        return getNotMgrCompat().areNotificationsEnabled();
+        return getNotCompMgr().areNotificationsEnabled();
     }
 
     /**
-     * Schedule local notification specified by JSON object.
+     * Schedule local notification specified by request.
      *
-     * @param options
-     *      JSON object with set of options
-     * @param receiver
-     *      Receiver to handle the trigger event
+     * @param request Set of notification options.
+     * @param receiver Receiver to handle the trigger event.
      */
-    public Notification schedule (JSONObject options, Class<?> receiver) {
-        return schedule(new Options(context).parse(options), receiver);
+    public Notification schedule (Request request, Class<?> receiver) {
+        Options options    = request.getOptions();
+        Notification toast = new Notification(context, options);
+
+        toast.schedule(request, receiver);
+
+        return toast;
     }
 
     /**
-     * Schedule local notification specified by options object.
-     *
-     * @param options
-     *      Set of notification options
-     * @param receiver
-     *      Receiver to handle the trigger event
+     * TODO: temporary
      */
-    public Notification schedule (Options options, Class<?> receiver) {
-        Notification notification = new Builder(options)
-                .setTriggerReceiver(receiver)
-                .build();
+    private void createDefaultChannel() {
+        NotificationManager mgr = getNotMgr();
 
-        notification.schedule();
+        if (SDK_INT < O)
+            return;
 
-        return notification;
+        NotificationChannel channel = mgr.getNotificationChannel(CHANNEL_ID);
+
+        if (channel != null)
+            return;
+
+        channel = new NotificationChannel(
+                CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_DEFAULT);
+
+        mgr.createNotificationChannel(channel);
     }
 
-    /**
-     * Clear local notification specified by ID.
-     *
-     * @param id
-     *      The notification ID
-     * @param updates
-     *      JSON object with notification options
-     * @param receiver
-     *      Receiver to handle the trigger event
-     */
-    public Notification update (int id, JSONObject updates, Class<?> receiver) {
-        Notification notification = get(id);
+    // /**
+    //  * Clear local notification specified by ID.
+    //  *
+    //  * @param id
+    //  *      The notification ID
+    //  * @param updates
+    //  *      JSON object with notification options
+    //  * @param receiver
+    //  *      Receiver to handle the trigger event
+    //  */
+    // public Notification update (int id, JSONObject updates, Class<?> receiver) {
+    //     Notification notification = get(id);
 
-        if (notification == null)
-            return null;
+    //     if (notification == null)
+    //         return null;
 
-        notification.cancel();
+    //     notification.cancel();
 
-        JSONObject options = mergeJSONObjects(
-                notification.getOptions().getDict(), updates);
+    //     JSONObject options = mergeJSONObjects(
+    //             notification.getOptions().getDict(), updates);
 
-        try {
-            options.put("updated", true);
-        } catch (JSONException ignore) {}
+    //     try {
+    //         options.put("updated", true);
+    //     } catch (JSONException ignore) {}
 
-        return schedule(options, receiver);
-    }
+    //     return schedule(options, receiver);
+    // }
 
     /**
      * Clear local notification specified by ID.
      *
-     * @param id
-     *      The notification ID
+     * @param id The notification ID.
      */
     public Notification clear (int id) {
-        Notification notification = get(id);
+        Notification toast = get(id);
 
-        if (notification != null) {
-            notification.clear();
+        if (toast != null) {
+            toast.clear();
         }
 
-        return notification;
+        return toast;
     }
 
     /**
-     * Clear local notification specified by ID.
-     *
-     * @param id
-     *      The notification ID
+     * Clear all local notifications.
      */
-    public Notification cancel (int id) {
-        Notification notification = get(id);
+    public void clearAll () {
+        List<Notification> toasts = getByType(TRIGGERED);
 
-        if (notification != null) {
-            notification.cancel();
+        for (Notification toast : toasts) {
+            toast.clear();
         }
 
-        return notification;
+        getNotCompMgr().cancelAll();
+        setBadge(0);
     }
 
     /**
-     * Clear all local notifications.
+     * Clear local notification specified by ID.
+     *
+     * @param id The notification ID
      */
-    public void clearAll () {
-        List<Notification> notifications = getAll();
+    public Notification cancel (int id) {
+        Notification toast = get(id);
 
-        for (Notification notification : notifications) {
-            notification.clear();
+        if (toast != null) {
+            toast.cancel();
         }
 
-        getNotMgr().cancelAll();
+        return toast;
     }
 
     /**
@@ -189,7 +202,8 @@ public class Manager {
             notification.cancel();
         }
 
-        getNotMgr().cancelAll();
+        getNotCompMgr().cancelAll();
+        setBadge(0);
     }
 
     /**
@@ -197,7 +211,7 @@ public class Manager {
      */
     public List<Integer> getIds() {
         Set<String> keys = getPrefs().getAll().keySet();
-        ArrayList<Integer> ids = new ArrayList<Integer>();
+        List<Integer> ids = new ArrayList<Integer>();
 
         for (String key : keys) {
             try {
@@ -213,40 +227,46 @@ public class Manager {
     /**
      * All local notification IDs for given type.
      *
-     * @param type
-     *      The notification life cycle type
+     * @param type The notification life cycle type
      */
     public List<Integer> getIdsByType(Notification.Type type) {
-        List<Notification> notifications = getAll();
-        ArrayList<Integer> ids = new ArrayList<Integer>();
 
-        for (Notification notification : notifications) {
-            if (notification.getType() == type) {
-                ids.add(notification.getId());
-            }
+        if (type == Notification.Type.ALL)
+            return getIds();
+
+        StatusBarNotification[] activeToasts = getNotMgr().getActiveNotifications();
+        List<Integer> activeIds = new ArrayList<Integer>();
+
+        for (StatusBarNotification toast : activeToasts) {
+            activeIds.add(toast.getId());
         }
 
+        if (type == TRIGGERED)
+            return activeIds;
+
+        List<Integer> ids = getIds();
+        ids.removeAll(activeIds);
+
         return ids;
     }
 
     /**
      * List of local notifications with matching ID.
      *
-     * @param ids
-     *      Set of notification IDs
+     * @param ids Set of notification IDs.
      */
-    public List<Notification> getByIds(List<Integer> ids) {
-        ArrayList<Notification> notifications = new ArrayList<Notification>();
+    private List<Notification> getByIds(List<Integer> ids) {
+        List<Notification> toasts = new ArrayList<Notification>();
 
         for (int id : ids) {
-            Notification notification = get(id);
+            Notification toast = get(id);
 
-            if (notification != null) {
-                notifications.add(notification);
+            if (toast != null) {
+                toasts.add(toast);
             }
         }
 
-        return notifications;
+        return toasts;
     }
 
     /**
@@ -259,72 +279,29 @@ public class Manager {
     /**
      * List of local notifications from given type.
      *
-     * @param type
-     *      The notification life cycle type
+     * @param type The notification life cycle type
      */
-    public List<Notification> getByType(Notification.Type type) {
-        List<Notification> notifications = getAll();
-        ArrayList<Notification> list = new ArrayList<Notification>();
+    private List<Notification> getByType(Notification.Type type) {
 
         if (type == Notification.Type.ALL)
-            return notifications;
-
-        for (Notification notification : notifications) {
-            if (notification.getType() == type) {
-                list.add(notification);
-            }
-        }
-
-        return list;
-    }
-
-    /**
-     * List of local notifications with matching ID from given type.
-     *
-     * @param type
-     *      The notification life cycle type
-     * @param ids
-     *      Set of notification IDs
-     */
-    @SuppressWarnings("UnusedDeclaration")
-    public List<Notification> getBy(Notification.Type type, List<Integer> ids) {
-        ArrayList<Notification> notifications = new ArrayList<Notification>();
+            return getAll();
 
-        for (int id : ids) {
-            Notification notification = get(id);
-
-            if (notification != null && notification.isScheduled()) {
-                notifications.add(notification);
-            }
-        }
+        List<Integer> ids = getIdsByType(type);
 
-        return notifications;
+        return getByIds(ids);
     }
 
     /**
      * If a notification with an ID exists.
      *
-     * @param id
-     *      Notification ID
+     * @param id Notification ID
+     *
+     * @return true if found.
      */
     public boolean exist (int id) {
         return get(id) != null;
     }
 
-    /**
-     * If a notification with an ID and type exists.
-     *
-     * @param id
-     *      Notification ID
-     * @param type
-     *      Notification type
-     */
-    public boolean exist (int id, Notification.Type type) {
-        Notification notification = get(id);
-
-        return notification != null && notification.getType() == type;
-    }
-
     /**
      * List of properties from all local notifications.
      */
@@ -335,21 +312,20 @@ public class Manager {
     /**
      * List of properties from local notifications with matching ID.
      *
-     * @param ids
-     *      Set of notification IDs
+     * @param ids Set of notification IDs
      */
     public List<JSONObject> getOptionsById(List<Integer> ids) {
-        ArrayList<JSONObject> options = new ArrayList<JSONObject>();
+        List<JSONObject> toasts = new ArrayList<JSONObject>();
 
         for (int id : ids) {
-            Notification notification = get(id);
+            Options options = getOptions(id);
 
-            if (notification != null) {
-                options.add(notification.getOptions().getDict());
+            if (options != null) {
+                toasts.add(options.getDict());
             }
         }
 
-        return options;
+        return toasts;
     }
 
     /**
@@ -370,97 +346,103 @@ public class Manager {
     }
 
     /**
-     * List of properties from local notifications with matching ID from
-     * given type.
+     * Get local notification options.
      *
-     * @param type
-     *      The notification life cycle type
-     * @param ids
-     *      Set of notification IDs
+     * @param id Notification ID.
+     *
+     * @return null if could not found.
      */
-    public List<JSONObject> getOptionsBy(Notification.Type type,
-                                         List<Integer> ids) {
+    public Options getOptions(int id) {
+        SharedPreferences prefs = getPrefs();
+        String toastId          = Integer.toString(id);
 
-        if (type == Notification.Type.ALL)
-            return getOptionsById(ids);
+        if (!prefs.contains(toastId))
+            return null;
 
-        ArrayList<JSONObject> options = new ArrayList<JSONObject>();
-        List<Notification> notifications = getByIds(ids);
+        try {
+            String json     = prefs.getString(toastId, null);
+            JSONObject dict = new JSONObject(json);
 
-        for (Notification notification : notifications) {
-            if (notification.getType() == type) {
-                options.add(notification.getOptions().getDict());
-            }
+            return new Options(context, dict);
+        } catch (JSONException e) {
+            e.printStackTrace();
+            return null;
         }
-
-        return options;
     }
 
     /**
      * Get existent local notification.
      *
-     * @param id
-     *      Notification ID
+     * @param id Notification ID.
+     *
+     * @return null if could not found.
      */
     public Notification get(int id) {
-        Map<String, ?> alarms = getPrefs().getAll();
-        String notId          = Integer.toString(id);
-        JSONObject options;
+        Options options = getOptions(id);
 
-        if (!alarms.containsKey(notId))
+        if (options == null)
             return null;
 
-
-        try {
-            String json = alarms.get(notId).toString();
-            options = new JSONObject(json);
-        } catch (JSONException e) {
-            e.printStackTrace();
-            return null;
-        }
-
-        Builder builder = new Builder(context, options);
-
-        return builder.build();
+        return new Notification(context, options);
     }
 
+    // /**
+    //  * Merge two JSON objects.
+    //  *
+    //  * @param obj1
+    //  *      JSON object
+    //  * @param obj2
+    //  *      JSON object with new options
+    //  */
+    // private JSONObject mergeJSONObjects (JSONObject obj1, JSONObject obj2) {
+    //     Iterator it = obj2.keys();
+
+    //     while (it.hasNext()) {
+    //         try {
+    //             String key = (String)it.next();
+
+    //             obj1.put(key, obj2.opt(key));
+    //         } catch (JSONException e) {
+    //             e.printStackTrace();
+    //         }
+    //     }
+
+    //     return obj1;
+    // }
+
     /**
-     * Merge two JSON objects.
+     * Set the badge number of the app icon.
      *
-     * @param obj1
-     *      JSON object
-     * @param obj2
-     *      JSON object with new options
+     * @param badge The badge number.
      */
-    private JSONObject mergeJSONObjects (JSONObject obj1, JSONObject obj2) {
-        Iterator it = obj2.keys();
-
-        while (it.hasNext()) {
-            try {
-                String key = (String)it.next();
-
-                obj1.put(key, obj2.opt(key));
-            } catch (JSONException e) {
-                e.printStackTrace();
-            }
+    public void setBadge (int badge) {
+        if (badge == 0) {
+            new BadgeImpl(context).clearBadge();
+        } else {
+            new BadgeImpl(context).setBadge(badge);
         }
-
-        return obj1;
     }
 
     /**
      * Shared private preferences for the application.
      */
     private SharedPreferences getPrefs () {
-        return context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+        return context.getSharedPreferences(PREF_KEY_ID, Context.MODE_PRIVATE);
     }
 
     /**
      * Notification manager for the application.
      */
-    private NotificationManager getNotMgr () {
-        return (NotificationManager) context
-                .getSystemService(Context.NOTIFICATION_SERVICE);
+    private NotificationManager getNotMgr() {
+        return (NotificationManager) context.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+    }
+
+    /**
+     * Notification compat manager for the application.
+     */
+    private NotificationManagerCompat getNotCompMgr() {
+        return NotificationManagerCompat.from(context);
     }
     
     /**

+ 185 - 150
src/android/notification/Notification.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,43 +17,56 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.notification;
 
-
 import android.app.AlarmManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.os.Build;
+import android.net.Uri;
+import android.service.notification.StatusBarNotification;
 import android.support.v4.app.NotificationCompat;
+import android.support.v4.util.ArraySet;
+import android.support.v4.util.Pair;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import static android.app.AlarmManager.RTC;
+import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.support.v4.app.NotificationManagerCompat.IMPORTANCE_MAX;
+import static android.support.v4.app.NotificationManagerCompat.IMPORTANCE_MIN;
 
 /**
  * Wrapper class around OS notification class. Handles basic operations
  * like show, delete, cancel for a single local notification instance.
  */
-public class Notification {
+public final class Notification {
 
     // Used to differ notifications by their life cycle state
     public enum Type {
         ALL, SCHEDULED, TRIGGERED
     }
 
-    // Default receiver to handle the trigger event
-    private static Class<?> defaultReceiver = TriggerReceiver.class;
+    // Extra key for the id
+    public static final String EXTRA_ID = "NOTIFICATION_ID";
+
+    // Key for private preferences
+    static final String PREF_KEY_ID = "NOTIFICATION_ID";
 
     // Key for private preferences
-    static final String PREF_KEY = "LocalNotification";
+    private static final String PREF_KEY_PID = "NOTIFICATION_PID";
 
     // Application context passed by constructor
     private final Context context;
@@ -64,27 +77,29 @@ public class Notification {
     // Builder with full configuration
     private final NotificationCompat.Builder builder;
 
-    // Receiver to handle the trigger event
-    private Class<?> receiver = defaultReceiver;
-
     /**
      * Constructor
      *
-     * @param context
-     *      Application context
-     * @param options
-     *      Parsed notification options
-     * @param builder
-     *      Pre-configured notification builder
+     * @param context Application context.
+     * @param options Parsed notification options.
+     * @param builder Pre-configured notification builder.
      */
-    protected Notification (Context context, Options options,
-                    NotificationCompat.Builder builder, Class<?> receiver) {
-
-        this.context = context;
-        this.options = options;
-        this.builder = builder;
+    Notification (Context context, Options options, NotificationCompat.Builder builder) {
+        this.context  = context;
+        this.options  = options;
+        this.builder  = builder;
+    }
 
-        this.receiver = receiver != null ? receiver : defaultReceiver;
+    /**
+     * Constructor
+     *
+     * @param context Application context.
+     * @param options Parsed notification options.
+     */
+    public Notification(Context context, Options options) {
+        this.context  = context;
+        this.options  = options;
+        this.builder  = null;
     }
 
     /**
@@ -111,88 +126,122 @@ public class Notification {
     /**
      * If it's a repeating notification.
      */
-    public boolean isRepeating () {
-        return getOptions().getRepeatInterval() > 0;
+    private boolean isRepeating () {
+        return getOptions().getTrigger().has("every");
     }
 
     /**
-     * If the notification was in the past.
+     * Notification type can be one of triggered or scheduled.
      */
-    public boolean wasInThePast () {
-        return new Date().after(options.getTriggerDate());
-    }
+    public Type getType () {
+        StatusBarNotification[] toasts = getNotMgr().getActiveNotifications();
+        int id = getId();
 
-    /**
-     * If the notification is scheduled.
-     */
-    public boolean isScheduled () {
-        return isRepeating() || !wasInThePast();
-    }
+        for (StatusBarNotification toast : toasts) {
+            if (toast.getId() == id) {
+                return Type.TRIGGERED;
+            }
+        }
 
-    /**
-     * If the notification is triggered.
-     */
-    public boolean isTriggered () {
-        return wasInThePast();
+        return Type.SCHEDULED;
     }
 
     /**
-     * If the notification is an update.
+     * Schedule the local notification.
      *
-     * @param keepFlag
-     *      Set to false to remove the flag from the option map
+     * @param request Set of notification options.
+     * @param receiver Receiver to handle the trigger event.
      */
-    protected boolean isUpdate (boolean keepFlag) {
-        boolean updated = options.getDict().optBoolean("updated", false);
+    void schedule(Request request, Class<?> receiver) {
+        List<Pair<Date, Intent>> intents = new ArrayList<Pair<Date, Intent>>();
+        Set<String> ids                  = new ArraySet<String>();
+        AlarmManager mgr                 = getAlarmMgr();
 
-        if (!keepFlag) {
-            options.getDict().remove("updated");
-        }
+        do {
+            Date date = request.getTriggerDate();
 
-        return updated;
-    }
+            if (date == null)
+                continue;
 
-    /**
-     * Notification type can be one of pending or scheduled.
-     */
-    public Type getType () {
-        return isScheduled() ? Type.SCHEDULED : Type.TRIGGERED;
+            Intent intent = new Intent(context, receiver)
+                    .setAction(PREF_KEY_ID + request.getIdentifier())
+                    .putExtra(Notification.EXTRA_ID, options.getId())
+                    .putExtra(Request.EXTRA_OCCURRENCE, request.getOccurrence());
+
+            ids.add(intent.getAction());
+            intents.add(new Pair<Date, Intent>(date, intent));
+        }
+        while (request.moveNext());
+
+        if (intents.isEmpty())
+            return;
+
+        persist(ids);
+
+        Intent last = intents.get(intents.size() - 1).second;
+        last.putExtra(Request.EXTRA_LAST, true);
+
+        for (Pair<Date, Intent> pair : intents) {
+            Date date     = pair.first;
+            long time     = date.getTime();
+            Intent intent = pair.second;
+
+            if (!date.after(new Date()) && trigger(intent, receiver))
+                continue;
+
+            PendingIntent pi = PendingIntent.getBroadcast(
+                    context, 0, intent, FLAG_CANCEL_CURRENT);
+
+            try {
+                switch (options.getPriority()) {
+                    case IMPORTANCE_MIN:
+                        mgr.setExact(RTC, time, pi);
+                        break;
+                    case IMPORTANCE_MAX:
+                        mgr.setExactAndAllowWhileIdle(RTC_WAKEUP, time, pi);
+                        break;
+                    default:
+                        mgr.setExact(RTC_WAKEUP, time, pi);
+                        break;
+                }
+            } catch (Exception ignore) {
+                // Samsung devices have a known bug where a 500 alarms limit
+                // can crash the app
+            }
+        }
     }
 
     /**
-     * Schedule the local notification.
+     * Trigger local notification specified by options.
+     *
+     * @param intent The intent to broadcast.
+     * @param cls    The broadcast class.
      */
-    public void schedule() {
-        long triggerTime = options.getTriggerTime();
-
-        persist();
-
-        // Intent gets called when the Notification gets fired
-        Intent intent = new Intent(context, receiver)
-                .setAction(options.getIdStr())
-                .putExtra(Options.EXTRA, options.toString());
+    private boolean trigger (Intent intent, Class<?> cls) {
+        BroadcastReceiver receiver;
 
-        PendingIntent pi = PendingIntent.getBroadcast(
-                context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-
-        if (isRepeating()) {
-            getAlarmMgr().setRepeating(AlarmManager.RTC_WAKEUP,
-                    triggerTime, options.getRepeatInterval(), pi);
-        } else {
-            getAlarmMgr().set(AlarmManager.RTC_WAKEUP, triggerTime, pi);
+        try {
+            receiver = (BroadcastReceiver) cls.newInstance();
+        } catch (InstantiationException e) {
+            return false;
+        } catch (IllegalAccessException e) {
+            return false;
         }
+
+        receiver.onReceive(context, intent);
+        return true;
     }
 
     /**
      * Clear the local notification without canceling repeating alarms.
      */
-    public void clear () {
+    public void clear() {
+        getNotMgr().cancel(getId());
 
-        if (!isRepeating() && wasInThePast())
-            unpersist();
+        if (isRepeating())
+            return;
 
-        if (!isRepeating())
-            getNotMgr().cancel(getId());
+        unpersist();
     }
 
     /**
@@ -204,56 +253,38 @@ public class Notification {
      * method and cancel it.
      */
     public void cancel() {
-        Intent intent = new Intent(context, receiver)
-                .setAction(options.getIdStr());
-
-        PendingIntent pi = PendingIntent.
-                getBroadcast(context, 0, intent, 0);
+        SharedPreferences prefs = getPrefs(PREF_KEY_PID);
+        String id               = options.getIdentifier();
+        Set<String> actions     = prefs.getStringSet(id, null);
 
-        getAlarmMgr().cancel(pi);
+        unpersist();
         getNotMgr().cancel(options.getId());
 
-        unpersist();
-    }
+        if (actions == null)
+            return;
 
-    /**
-     * Present the local notification to user.
-     */
-    public void show () {
-        // TODO Show dialog when in foreground
-        showNotification();
-    }
+        for (String action : actions) {
+            Intent intent = new Intent(action);
 
-    /**
-     * Show as local notification when in background.
-     */
-    @SuppressWarnings("deprecation")
-    private void showNotification () {
-        int id = getOptions().getId();
-
-        if (Build.VERSION.SDK_INT <= 15) {
-            // Notification for HoneyComb to ICS
-            getNotMgr().notify(id, builder.getNotification());
-        } else {
-            // Notification for Jellybean and above
-            getNotMgr().notify(id, builder.build());
+            PendingIntent pi = PendingIntent.getBroadcast(
+                    context, 0, intent, 0);
+
+            if (pi != null) {
+                getAlarmMgr().cancel(pi);
+            }
         }
     }
 
     /**
-     * Count of triggers since schedule.
+     * Present the local notification to user.
      */
-    public int getTriggerCountSinceSchedule() {
-        long now = System.currentTimeMillis();
-        long triggerTime = options.getTriggerTime();
-
-        if (!wasInThePast())
-            return 0;
+    public void show() {
 
-        if (!isRepeating())
-            return 1;
+        if (builder == null)
+            return;
 
-        return (int) ((now - triggerTime) / options.getRepeatInterval());
+        grantPermissionToPlaySoundFromExternal();
+        getNotMgr().notify(getId(), builder.build());
     }
 
     /**
@@ -269,11 +300,6 @@ public class Notification {
             e.printStackTrace();
         }
 
-        json.remove("firstAt");
-        json.remove("updated");
-        json.remove("soundUri");
-        json.remove("iconUri");
-
         return json.toString();
     }
 
@@ -281,39 +307,58 @@ public class Notification {
      * Persist the information of this notification to the Android Shared
      * Preferences. This will allow the application to restore the notification
      * upon device reboot, app restart, retrieve notifications, aso.
+     *
+     * @param ids List of intent actions to persist.
      */
-    private void persist () {
-        SharedPreferences.Editor editor = getPrefs().edit();
+    private void persist (Set<String> ids) {
+        String id = options.getIdentifier();
+        SharedPreferences.Editor editor;
 
-        editor.putString(options.getIdStr(), options.toString());
+        editor = getPrefs(PREF_KEY_ID).edit();
+        editor.putString(id, options.toString());
+        editor.apply();
 
-        if (Build.VERSION.SDK_INT < 9) {
-            editor.commit();
-        } else {
-            editor.apply();
-        }
+        editor = getPrefs(PREF_KEY_PID).edit();
+        editor.putStringSet(id, ids);
+        editor.apply();
     }
 
     /**
      * Remove the notification from the Android shared Preferences.
      */
     private void unpersist () {
-        SharedPreferences.Editor editor = getPrefs().edit();
+        String[] keys = { PREF_KEY_ID, PREF_KEY_PID };
+        String id     = options.getIdentifier();
+        SharedPreferences.Editor editor;
 
-        editor.remove(options.getIdStr());
-
-        if (Build.VERSION.SDK_INT < 9) {
-            editor.commit();
-        } else {
+        for (String key : keys) {
+            editor = getPrefs(key).edit();
+            editor.remove(id);
             editor.apply();
         }
     }
 
+    /**
+     * Since Android 7 the app will crash if an external process has no
+     * permission to access the referenced sound file.
+     */
+    private void grantPermissionToPlaySoundFromExternal() {
+        if (builder == null)
+            return;
+
+        String sound = builder.getExtras().getString(Options.EXTRA_SOUND);
+        Uri soundUri = Uri.parse(sound);
+
+        context.grantUriPermission(
+                "com.android.systemui", soundUri,
+                Intent.FLAG_GRANT_READ_URI_PERMISSION);
+    }
+
     /**
      * Shared private preferences for the application.
      */
-    private SharedPreferences getPrefs () {
-        return context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+    private SharedPreferences getPrefs (String key) {
+        return context.getSharedPreferences(key, Context.MODE_PRIVATE);
     }
 
     /**
@@ -331,14 +376,4 @@ public class Notification {
         return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
     }
 
-    /**
-     * Set default receiver to handle the trigger event.
-     *
-     * @param receiver
-     *      broadcast receiver
-     */
-    public static void setDefaultTriggerReceiver (Class<?> receiver) {
-        defaultReceiver = receiver;
-    }
-
 }

+ 446 - 172
src/android/notification/Options.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,131 +17,85 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 package de.appplant.cordova.plugin.notification;
 
-import android.app.AlarmManager;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.net.Uri;
 import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.MessagingStyle.Message;
+import android.support.v4.media.session.MediaSessionCompat;
 
-import org.json.JSONException;
+import org.json.JSONArray;
 import org.json.JSONObject;
 import org.json.JSONArray;
 
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
+
+import de.appplant.cordova.plugin.notification.action.Action;
+import de.appplant.cordova.plugin.notification.action.ActionGroup;
+import de.appplant.cordova.plugin.notification.util.AssetUtil;
+
+import static android.support.v4.app.NotificationCompat.DEFAULT_LIGHTS;
+import static android.support.v4.app.NotificationCompat.DEFAULT_SOUND;
+import static android.support.v4.app.NotificationCompat.DEFAULT_VIBRATE;
+import static android.support.v4.app.NotificationCompat.PRIORITY_MAX;
+import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
+import static android.support.v4.app.NotificationCompat.VISIBILITY_PUBLIC;
+import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
 
 /**
  * Wrapper around the JSON object passed through JS which contains all
  * possible option values. Class provides simple readers and more advanced
  * methods to convert independent values into platform specific values.
  */
-public class Options {
+public final class Options {
 
-    // Key name for bundled extras
-    static final String EXTRA = "NOTIFICATION_OPTIONS";
+    // Key name for bundled sound extra
+    static final String EXTRA_SOUND = "NOTIFICATION_SOUND";
 
-    // The original JSON object
-    private JSONObject options = new JSONObject();
+    // Key name for bundled launch extra
+    public static final String EXTRA_LAUNCH = "NOTIFICATION_LAUNCH";
+
+    // Default icon path
+    private static final String DEFAULT_ICON = "res://icon";
 
-    // Repeat interval
-    private long interval = 0;
+    // The original JSON object
+    private final JSONObject options;
 
-    // Application context
+    // The application context
     private final Context context;
 
     // Asset util instance
     private final AssetUtil assets;
 
-
     /**
-     * Constructor
+     * When creating without a context, various methods might not work well.
      *
-     * @param context
-     *      Application context
+     * @param options The options dict map.
      */
-    public Options(Context context){
-    	this.context = context;
-        this.assets  = AssetUtil.getInstance(context);
+    public Options(JSONObject options) {
+        this.options = options;
+        this.context = null;
+        this.assets  = null;
     }
 
     /**
-     * Parse given JSON properties.
+     * Constructor
      *
-     * @param options
-     *      JSON properties
+     * @param context The application context.
+     * @param options The options dict map.
      */
-    public Options parse (JSONObject options) {
+    public Options(Context context, JSONObject options) {
+        this.context = context;
         this.options = options;
-
-        parseInterval();
-        parseAssets();
-
-        return this;
-    }
-
-    /**
-     * Parse repeat interval.
-     */
-    private void parseInterval() {
-        String every = options.optString("every").toLowerCase();
-
-        if (every.isEmpty()) {
-            interval = 0;
-        } else
-        if (every.equals("second")) {
-            interval = 1000;
-        } else
-        if (every.equals("minute")) {
-            interval = AlarmManager.INTERVAL_FIFTEEN_MINUTES / 15;
-        } else
-        if (every.equals("hour")) {
-            interval = AlarmManager.INTERVAL_HOUR;
-        } else
-        if (every.equals("day")) {
-            interval = AlarmManager.INTERVAL_DAY;
-        } else
-        if (every.equals("week")) {
-            interval = AlarmManager.INTERVAL_DAY * 7;
-        } else
-        if (every.equals("month")) {
-            interval = AlarmManager.INTERVAL_DAY * 31;
-        } else
-        if (every.equals("quarter")) {
-            interval = AlarmManager.INTERVAL_HOUR * 2190;
-        } else
-        if (every.equals("year")) {
-            interval = AlarmManager.INTERVAL_DAY * 365;
-        } else {
-            try {
-                interval = Integer.parseInt(every) * 60000;
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }
-    }
-
-    /**
-     * Parse asset URIs.
-     */
-    private void parseAssets() {
-
-        if (options.has("iconUri") && !options.optBoolean("updated"))
-            return;
-
-        Uri iconUri  = assets.parse(options.optString("icon", "res://icon"));
-        Uri soundUri = assets.parseSound(options.optString("sound", null));
-
-        try {
-            options.put("iconUri", iconUri.toString());
-            options.put("soundUri", soundUri.toString());
-        } catch (JSONException e) {
-            e.printStackTrace();
-        }
+        this.assets  = AssetUtil.getInstance(context);
     }
 
     /**
@@ -154,22 +108,33 @@ public class Options {
     /**
      * Wrapped JSON object.
      */
-    JSONObject getDict () {
+    public JSONObject getDict() {
         return options;
     }
 
     /**
-     * Text for the local notification.
+     * JSON object as string.
      */
-    public String getText() {
-        return options.optString("text", "");
+    public String toString() {
+        return options.toString();
     }
 
     /**
-     * Repeat interval (day, week, month, year, aso.)
+     * Gets the ID for the local notification.
+     *
+     * @return 0 if the user did not specify.
      */
-    public long getRepeatInterval() {
-        return interval;
+    public Integer getId() {
+        return options.optInt("id", 0);
+    }
+
+    /**
+     * The identifier for the local notification.
+     *
+     * @return The notification ID as the string
+     */
+    public String getIdentifier() {
+        return getId().toString();
     }
 
     /**
@@ -179,46 +144,75 @@ public class Options {
         return options.optInt("badge", 0);
     }
 
+    /**
+     * Number for the local notification.
+     */
+    public int getNumber() {
+        return options.optInt("number", 0);
+    }
+
     /**
      * ongoing flag for local notifications.
      */
-    public Boolean isOngoing() {
-        return options.optBoolean("ongoing", false);
+    public Boolean isSticky() {
+        return options.optBoolean("sticky", false);
     }
 
     /**
      * autoClear flag for local notifications.
      */
-    public Boolean isAutoClear() {
+    Boolean isAutoClear() {
         return options.optBoolean("autoClear", false);
     }
 
     /**
-     * ID for the local notification as a number.
+     * Gets the raw trigger spec as provided by the user.
      */
-    public Integer getId() {
-        return options.optInt("id", 0);
+    public JSONObject getTrigger() {
+        return options.optJSONObject("trigger");
     }
 
     /**
-     * ID for the local notification as a string.
+     * Gets the value of the silent flag.
      */
-    public String getIdStr() {
-        return getId().toString();
+    boolean isSilent() {
+        return options.optBoolean("silent", false);
+    }
+
+    /**
+     * The group for that notification.
+     */
+    String getGroup() {
+        return options.optString("group", null);
+    }
+
+    /**
+     * launch flag for the notification.
+     */
+    boolean isLaunchingApp() {
+        return options.optBoolean("launch", true);
+    }
+
+    /**
+     * The channel id of that notification.
+     */
+    String getChannel() {
+        return options.optString("channel", Manager.CHANNEL_ID);
     }
 
     /**
-     * Trigger date.
+     * If the group shall show a summary.
      */
-    public Date getTriggerDate() {
-        return new Date(getTriggerTime());
+    boolean getGroupSummary() {
+        return options.optBoolean("groupSummary", false);
     }
 
     /**
-     * Trigger date in milliseconds.
+     * Text for the local notification.
      */
-    public long getTriggerTime() {
-        return options.optLong("at", 0) * 1000;
+    public String getText() {
+        Object text = options.opt("text");
+        return text instanceof String ? (String) text : "";
     }
 
     /**
@@ -236,87 +230,107 @@ public class Options {
     }
 
     /**
-     * @return
-     *      The notification color for LED
+     * The notification color for LED.
      */
-    public int getLedColor() {
-        String hex = options.optString("led", null);
+    int getLedColor() {
+        Object cfg = options.opt("led");
+        String hex = null;
 
-        if (hex == null) {
-            return 0;
+        if (cfg instanceof String) {
+            hex = options.optString("led");
+        } else
+        if (cfg instanceof JSONArray) {
+            hex = options.optJSONArray("led").optString(0);
+        } else
+        if (cfg instanceof JSONObject) {
+            hex = options.optJSONObject("led").optString("color");
         }
 
-        int aRGB = Integer.parseInt(hex, 16);
+        if (hex == null)
+            return 0;
+
+        try {
+            hex      = stripHex(hex);
+            int aRGB = Integer.parseInt(hex, 16);
 
-        return aRGB + 0xFF000000;
+            return aRGB + 0xFF000000;
+        } catch (NumberFormatException e) {
+            e.printStackTrace();
+        }
+
+        return 0;
     }
 
     /**
-     * @return
-     *      The time that the LED should be on (in milliseconds).
+     * The notification color for LED.
      */
-    public int getLedOnTime() {
-        String timeOn = options.optString("ledOnTime", null);
+    int getLedOn() {
+        Object cfg = options.opt("led");
+        int defVal = 1000;
 
-        if (timeOn == null) {
-            return 1000;
-        }
+        if (cfg instanceof JSONArray)
+            return options.optJSONArray("led").optInt(1, defVal);
 
-        try {
-            return Integer.parseInt(timeOn);
-        } catch (NumberFormatException e) {
-           return 1000;
-        }
+        if (cfg instanceof JSONObject)
+            return options.optJSONObject("led").optInt("on", defVal);
+
+        return defVal;
     }
 
     /**
-     * @return
-     *      The time that the LED should be off (in milliseconds).
+     * The notification color for LED.
      */
-    public int getLedOffTime() {
-        String timeOff = options.optString("ledOffTime", null);
+    int getLedOff() {
+        Object cfg = options.opt("led");
+        int defVal = 1000;
 
-        if (timeOff == null) {
-            return 1000;
-        }
+        if (cfg instanceof JSONArray)
+            return options.optJSONArray("led").optInt(2, defVal);
 
-        try {
-            return Integer.parseInt(timeOff);
-        } catch (NumberFormatException e) {
-           return 1000;
-        }
+        if (cfg instanceof JSONObject)
+            return options.optJSONObject("led").optInt("off", defVal);
+
+        return defVal;
     }
 
     /**
-     * @return
-     *      The notification background color for the small icon
-     *      Returns null, if no color is given.
+     * The notification background color for the small icon.
+     *
+     * @return null, if no color is given.
      */
     public int getColor() {
         String hex = options.optString("color", null);
 
-        if (hex == null) {
+        if (hex == null)
             return NotificationCompat.COLOR_DEFAULT;
-        }
 
-        int aRGB = Integer.parseInt(hex, 16);
+        try {
+            hex = stripHex(hex);
+
+            if (hex.matches("[^0-9]*")) {
+                return Color.class
+                        .getDeclaredField(hex.toUpperCase())
+                        .getInt(null);
+            }
+
+            int aRGB = Integer.parseInt(hex, 16);
+            return aRGB + 0xFF000000;
+        } catch (NumberFormatException e) {
+            e.printStackTrace();
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
 
-        return aRGB + 0xFF000000;
+        return NotificationCompat.COLOR_DEFAULT;
     }
 
     /**
      * Sound file path for the local notification.
      */
-    public Uri getSoundUri() {
-        Uri uri = null;
-
-        try{
-            uri = Uri.parse(options.optString("soundUri"));
-        } catch (Exception e){
-            e.printStackTrace();
-        }
-
-        return uri;
+    public Uri getSound() {
+        return assets.parse(options.optString("sound", null));
     }
 
     public long[] getVibrate() {
@@ -337,15 +351,14 @@ public class Options {
     /**
      * Icon bitmap for the local notification.
      */
-    public Bitmap getIconBitmap() {
-        Bitmap bmp;
+    Bitmap getLargeIcon() {
+        Uri uri    = assets.parse(options.optString("icon", DEFAULT_ICON));
+        Bitmap bmp = null;
 
         try {
-            Uri uri = Uri.parse(options.optString("iconUri"));
             bmp = assets.getIconFromUri(uri);
         } catch (Exception e){
             e.printStackTrace();
-            bmp = assets.getIconFromDrawable("icon");
         }
 
         return bmp;
@@ -355,14 +368,22 @@ public class Options {
      * Icon resource ID for the local notification.
      */
     public int getIcon () {
-        String icon = options.optString("icon", "");
+        String icon = options.optString("icon", DEFAULT_ICON);
 
-        int resId = assets.getResIdForDrawable(icon);
+        int resId = assets.getResId(icon);
 
         if (resId == 0) {
             resId = getSmallIcon();
         }
 
+        if (resId == 0) {
+            resId = assets.getResId(DEFAULT_ICON);
+        }
+
+        if (resId == 0) {
+            resId = context.getApplicationInfo().icon;
+        }
+
         if (resId == 0) {
             resId = android.R.drawable.ic_popup_reminder;
         }
@@ -373,17 +394,270 @@ public class Options {
     /**
      * Small icon resource ID for the local notification.
      */
-    public int getSmallIcon () {
+    int getSmallIcon() {
         String icon = options.optString("smallIcon", "");
+        return assets.getResId(icon);
+    }
+
+    /**
+     * If the phone should vibrate.
+     */
+    private boolean isWithVibration() {
+        return options.optBoolean("vibrate", true);
+    }
 
-        return assets.getResIdForDrawable(icon);
+    /**
+     * If the phone should play no sound.
+     */
+    private boolean isWithoutSound() {
+        Object value = options.opt("sound");
+        return value == null || value.equals(false);
     }
 
     /**
-     * JSON object as string.
+     * If the phone should play the default sound.
      */
-    public String toString() {
-        return options.toString();
+    private boolean isWithDefaultSound() {
+        Object value = options.opt("sound");
+        return value != null && value.equals(true);
+    }
+
+    /**
+     * If the phone should show no LED light.
+     */
+    private boolean isWithoutLights() {
+        Object value = options.opt("led");
+        return value == null || value.equals(false);
+    }
+
+    /**
+     * If the phone should show the default LED lights.
+     */
+    private boolean isWithDefaultLights() {
+        Object value = options.opt("led");
+        return value != null && value.equals(true);
+    }
+
+    /**
+     * Set the default notification options that will be used.
+     * The value should be one or more of the following fields combined with
+     * bitwise-or: DEFAULT_SOUND, DEFAULT_VIBRATE, DEFAULT_LIGHTS.
+     */
+    int getDefaults() {
+        int defaults = options.optInt("defaults", 0);
+
+        if (isWithVibration()) {
+            defaults |= DEFAULT_VIBRATE;
+        } else {
+            defaults &= DEFAULT_VIBRATE;
+        }
+
+        if (isWithDefaultSound()) {
+            defaults |= DEFAULT_SOUND;
+        } else
+        if (isWithoutSound()) {
+            defaults &= DEFAULT_SOUND;
+        }
+
+        if (isWithDefaultLights()) {
+            defaults |= DEFAULT_LIGHTS;
+        } else
+        if (isWithoutLights()) {
+            defaults &= DEFAULT_LIGHTS;
+        }
+
+        return defaults;
+    }
+
+    /**
+     * Gets the visibility for the notification.
+     *
+     * @return VISIBILITY_PUBLIC or VISIBILITY_SECRET
+     */
+    int getVisibility() {
+        if (options.optBoolean("lockscreen", true)) {
+            return VISIBILITY_PUBLIC;
+        } else {
+            return VISIBILITY_SECRET;
+        }
+    }
+
+    /**
+     * Gets the notifications priority.
+     */
+    int getPriority() {
+        int prio = options.optInt("priority");
+
+        return Math.min(Math.max(prio, PRIORITY_MIN), PRIORITY_MAX);
+    }
+
+    /**
+     * If the notification shall show the when date.
+     */
+    boolean getShowWhen() {
+        return options.optBoolean("showWhen", true);
+    }
+
+    /**
+     * If the notification shall display a progress bar.
+     */
+    boolean isWithProgressBar() {
+        return options
+                .optJSONObject("progressBar")
+                .optBoolean("enabled", false);
+    }
+
+    /**
+     * Gets the progress value.
+     *
+     * @return 0 by default.
+     */
+    int getProgressValue() {
+        return options
+                .optJSONObject("progressBar")
+                .optInt("value", 0);
+    }
+
+    /**
+     * Gets the progress value.
+     *
+     * @return 100 by default.
+     */
+    int getProgressMaxValue() {
+        return options
+                .optJSONObject("progressBar")
+                .optInt("maxValue", 100);
+    }
+
+    /**
+     * Gets the progress indeterminate value.
+     *
+     * @return false by default.
+     */
+    boolean isIndeterminateProgress() {
+        return options
+                .optJSONObject("progressBar")
+                .optBoolean("indeterminate", false);
+    }
+
+    /**
+     * The summary for inbox style notifications.
+     */
+    String getSummary() {
+        return options.optString("summary", null);
+    }
+
+    /**
+     * Image attachments for image style notifications.
+     *
+     * @return For now it only returns the first item as Android does not
+     *         support multiple attachments like iOS.
+     */
+    List<Bitmap> getAttachments() {
+        JSONArray paths   = options.optJSONArray("attachments");
+        List<Bitmap> pics = new ArrayList<Bitmap>();
+
+        if (paths == null)
+            return pics;
+
+        for (int i = 0; i < paths.length(); i++) {
+            Uri uri = assets.parse(paths.optString(i));
+
+            if (uri == Uri.EMPTY)
+                continue;
+
+            try {
+                Bitmap pic = assets.getIconFromUri(uri);
+                pics.add(pic);
+                break;
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return pics;
+    }
+
+    /**
+     * Gets the list of actions to display.
+     */
+    Action[] getActions() {
+        String groupId    = options.optString("actionGroupId", null);
+        JSONArray actions = options.optJSONArray("actions");
+        ActionGroup group = null;
+
+        if (actions != null && actions.length() > 0) {
+            group = ActionGroup.parse(context, options);
+        }
+
+        if (group == null && groupId != null) {
+            group = ActionGroup.lookup(groupId);
+        }
+
+        if (group != null) {
+            ActionGroup.register(group);
+            return group.getActions();
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the list of messages to display.
+     *
+     * @return null if there are no messages.
+     */
+    Message[] getMessages() {
+        Object text = options.opt("text");
+
+        if (text == null || text instanceof String)
+            return null;
+
+        JSONArray list = (JSONArray) text;
+
+        if (list.length() == 0)
+            return null;
+
+        Message[] messages = new Message[list.length()];
+        long now           = new Date().getTime();
+
+        for (int i = 0; i < messages.length; i++) {
+            JSONObject msg = list.optJSONObject(i);
+            String message = msg.optString("message");
+            long timestamp = msg.optLong("date", now);
+            String person  = msg.optString("person", null);
+
+            messages[i] = new Message(message, timestamp, person);
+        }
+
+        return messages;
+    }
+
+    /**
+     * Gets the token for the specified media session.
+     *
+     * @return null if there no session.
+     */
+    MediaSessionCompat.Token getMediaSessionToken() {
+        String tag = options.optString("mediaSession", null);
+
+        if (tag == null)
+            return null;
+
+        MediaSessionCompat session = new MediaSessionCompat(context, tag);
+
+        return session.getSessionToken();
+    }
+
+    /**
+     * Strips the hex code #FF00FF => FF00FF
+     *
+     * @param hex The hex code to strip.
+     *
+     * @return The stripped hex code without a leading #
+     */
+    private String stripHex(String hex) {
+        return (hex.charAt(0) == '#') ? hex.substring(1) : hex;
     }
 
 }

+ 231 - 0
src/android/notification/Request.java

@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2014-2015 by appPlant UG. All rights reserved.
+ *
+ * @APPPLANT_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPPLANT_LICENSE_HEADER_END@
+ */
+
+package de.appplant.cordova.plugin.notification;
+
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import de.appplant.cordova.plugin.notification.trigger.DateTrigger;
+import de.appplant.cordova.plugin.notification.trigger.IntervalTrigger;
+import de.appplant.cordova.plugin.notification.trigger.MatchTrigger;
+
+import static de.appplant.cordova.plugin.notification.trigger.IntervalTrigger.Unit;
+
+/**
+ * An object you use to specify a notification’s content and the condition
+ * that triggers its delivery.
+ */
+public final class Request {
+
+    // Key name for bundled extras
+    static final String EXTRA_OCCURRENCE = "NOTIFICATION_OCCURRENCE";
+
+    // Key name for bundled extras
+    public static final String EXTRA_LAST = "NOTIFICATION_LAST";
+
+    // The options spec
+    private final Options options;
+
+    // The right trigger for the options
+    private final DateTrigger trigger;
+
+    // How often the trigger shall occur
+    private final int count;
+
+    // The trigger spec
+    private final JSONObject spec;
+
+    // The current trigger date
+    private Date triggerDate;
+
+    /**
+     * Constructor
+     *
+     * @param options The options spec.
+     */
+    public Request(Options options) {
+        this.options     = options;
+        this.spec        = options.getTrigger();
+        this.count       = spec.optInt("count", 1);
+        this.trigger     = buildTrigger();
+        this.triggerDate = trigger.getNextTriggerDate(getBaseDate());
+    }
+
+    /**
+     * Gets the options spec.
+     */
+    public Options getOptions() {
+        return options;
+    }
+
+    /**
+     * The identifier for the request.
+     *
+     * @return The notification ID as the string
+     */
+    public String getIdentifier() {
+        return options.getId().toString() + "-" + getOccurrence();
+    }
+
+    /**
+     * The value of the internal occurrence counter.
+     */
+    int getOccurrence() {
+        return trigger.getOccurrence();
+    }
+
+    /**
+     * If there's one more trigger date to calculate.
+     */
+    private boolean hasNext() {
+        return triggerDate != null && getOccurrence() < count;
+    }
+
+    /**
+     * Moves the internal occurrence counter by one.
+     */
+    boolean moveNext() {
+        if (hasNext()) {
+            triggerDate = getNextTriggerDate();
+        } else {
+            triggerDate = null;
+        }
+
+        return this.triggerDate != null;
+    }
+
+    /**
+     * Gets the current trigger date.
+     *
+     * @return null if there's no trigger date.
+     */
+    Date getTriggerDate() {
+        Calendar now = Calendar.getInstance();
+
+        if (triggerDate == null)
+            return null;
+
+        if ((now.getTimeInMillis() - triggerDate.getTime()) > 60000) {
+            return null;
+        }
+
+        return triggerDate;
+    }
+
+    /**
+     * Gets the next trigger date based on the current trigger date.
+     */
+    private Date getNextTriggerDate() {
+        return trigger.getNextTriggerDate(triggerDate);
+    }
+
+    /**
+     * Build the trigger specified in options.
+     */
+    private DateTrigger buildTrigger() {
+        Object every = spec.opt("every");
+
+        if (every instanceof JSONObject) {
+            return new MatchTrigger(getDateMatchingComponents());
+        }
+
+        Unit unit = getUnit();
+        int ticks = getTicks();
+
+        return new IntervalTrigger(ticks, unit);
+    }
+
+    /**
+     * Gets the unit value.
+     */
+    private Unit getUnit() {
+        Object every = spec.opt("every");
+        String unit  = "SECOND";
+
+        if (spec.has("unit")) {
+            unit = spec.optString("unit", "second");
+        } else
+        if (every instanceof String) {
+            unit = spec.optString("every", "second");
+        }
+
+        return Unit.valueOf(unit.toUpperCase());
+    }
+
+    /**
+     * Gets the tick value.
+     */
+    private int getTicks() {
+        Object every = spec.opt("every");
+        int ticks    = 0;
+
+        if (spec.has("at")) {
+            ticks = 0;
+        } else
+        if (spec.has("in")) {
+            ticks = spec.optInt("in", 0);
+        } else
+        if (every instanceof String) {
+            ticks = 1;
+        } else
+        if (!(every instanceof JSONObject)) {
+            ticks = spec.optInt("every", 0);
+        }
+
+        return ticks;
+    }
+
+    /**
+     * Gets an array of all date parts to construct a datetime instance.
+     *
+     * @return [min, hour, day, month, year]
+     */
+    private List<Integer> getDateMatchingComponents() {
+        JSONObject every = spec.optJSONObject("every");
+
+        return Arrays.asList(
+                (Integer) every.opt("minute"),
+                (Integer) every.opt("hour"),
+                (Integer) every.opt("day"),
+                (Integer) every.opt("month"),
+                (Integer) every.opt("year")
+        );
+    }
+
+    /**
+     * Gets the base date from where to calculate the next trigger date.
+     */
+    private Date getBaseDate() {
+        if (spec.has("at")) {
+            return new Date(spec.optLong("at", 0) * 1000);
+        } else {
+            return new Date();
+        }
+    }
+
+}

+ 0 - 59
src/android/notification/TriggerReceiver.java

@@ -1,59 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-package de.appplant.cordova.plugin.notification;
-
-/**
- * The alarm receiver is triggered when a scheduled alarm is fired. This class
- * reads the information in the intent and displays this information in the
- * Android notification bar. The notification uses the default notification
- * sound and it vibrates the phone.
- */
-public class TriggerReceiver extends AbstractTriggerReceiver {
-
-    /**
-     * Called when a local notification was triggered. Does present the local
-     * notification and re-schedule the alarm if necessary.
-     *
-     * @param notification
-     *      Wrapper around the local notification
-     * @param updated
-     *      If an update has triggered or the original
-     */
-    @Override
-    public void onTrigger (Notification notification, boolean updated) {
-        notification.show();
-    }
-
-    /**
-     * Build notification specified by options.
-     *
-     * @param builder
-     *      Notification builder
-     */
-    @Override
-    public Notification buildNotification (Builder builder) {
-        return builder.build();
-    }
-
-}

+ 137 - 0
src/android/notification/action/Action.java

@@ -0,0 +1,137 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.action;
+
+import android.content.Context;
+import android.support.v4.app.RemoteInput;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import de.appplant.cordova.plugin.notification.util.AssetUtil;
+
+/**
+ * Holds the icon and title components that would be used in a
+ * NotificationCompat.Action object. Does not include the PendingIntent so
+ * that it may be generated each time the notification is built. Necessary to
+ * compensate for missing functionality in the support library.
+ */
+public final class Action {
+
+    // Key name for bundled extras
+    public static final String EXTRA_ID = "NOTIFICATION_ACTION_ID";
+
+    // The id for the click action
+    public static final String CLICK_ACTION_ID = "click";
+
+    // The application context
+    private final Context context;
+
+    // The action spec
+    private final JSONObject options;
+
+    /**
+     * Structure to encapsulate a named action that can be shown as part of
+     * this notification.
+     *
+     * @param context The application context.
+     * @param options The action options.
+     */
+    Action (Context context, JSONObject options) {
+        this.context = context;
+        this.options = options;
+    }
+
+    /**
+     * Gets the ID for the action.
+     */
+    public String getId() {
+        return options.optString("id", getTitle());
+    }
+
+    /**
+     * Gets the Title for the action.
+     */
+    public String getTitle() {
+        return options.optString("title", "unknown");
+    }
+
+    /**
+     * Gets the icon for the action.
+     */
+    public int getIcon() {
+        AssetUtil assets = AssetUtil.getInstance(context);
+        String resPath   = options.optString("icon");
+        int resId        = assets.getResId(resPath);
+
+        if (resId == 0) {
+            resId = android.R.drawable.screen_background_dark;
+        }
+
+        return resId;
+    }
+
+    /**
+     * Gets the value of the launch flag.
+     */
+    public boolean isLaunchingApp() {
+        return options.optBoolean("launch", false);
+    }
+
+    /**
+     * Gets the type for the action.
+     */
+    public boolean isWithInput() {
+        String type = options.optString("type");
+        return type.equals("input");
+    }
+
+    /**
+     * Gets the input config in case of the action is of type input.
+     */
+    public RemoteInput getInput() {
+        return new RemoteInput.Builder(getId())
+                .setLabel(options.optString("emptyText"))
+                .setAllowFreeFormInput(options.optBoolean("editable", true))
+                .setChoices(getChoices())
+                .build();
+    }
+
+    /**
+     * List of possible choices for input actions.
+     */
+    private String[] getChoices() {
+        JSONArray opts = options.optJSONArray("choices");
+
+        if (opts == null)
+            return null;
+
+        String[] choices = new String[opts.length()];
+
+        for (int i = 0; i < choices.length; i++) {
+            choices[i] = opts.optString(i);
+        }
+
+        return choices;
+    }
+
+}

+ 139 - 0
src/android/notification/action/ActionGroup.java

@@ -0,0 +1,139 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.action;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.N;
+
+public final class ActionGroup {
+
+    // Default action group id
+    private static final String GENERAL_ACTION_GROUP = "DEFAULT_GROUP";
+
+    // Saves all groups for later lookup.
+    private static final Map<String, ActionGroup> groups =
+            new HashMap<String, ActionGroup>();
+
+    // The ID of the action group.
+    private final String id;
+
+    // List of actions
+    private final Action[] actions;
+
+    /**
+     * Lookup the action groups with the specified group id.
+     *
+     * @param id The ID of the action group to find.
+     *
+     * @return Null if no group was found.
+     */
+    public static ActionGroup lookup(String id) {
+        return groups.get(id);
+    }
+
+    /**
+     * Register the action group for later lookup.
+     *
+     * @param group The action group to register.
+     */
+    public static void register (ActionGroup group) {
+        if (!group.getId().equalsIgnoreCase(GENERAL_ACTION_GROUP)) {
+            groups.put(group.getId(), group);
+        }
+    }
+
+    /**
+     * Creates an action group by parsing the specified action specs.
+     *
+     * @param spec The action group spec containing the id and list of actions.
+     *
+     * @return A new action group.
+     */
+    public static ActionGroup parse (Context context, JSONObject spec) {
+        String id = spec.optString("actionGroupId", GENERAL_ACTION_GROUP);
+        JSONArray list = spec.optJSONArray("actions");
+
+        if (list == null || list.length() == 0)
+            return null;
+
+        List<Action> actions = new ArrayList<Action>(list.length());
+
+        for (int i = 0; i < list.length(); i++) {
+            JSONObject opts = list.optJSONObject(i);
+            String type     = opts.optString("type", "button");
+
+            if (type.equals("input") && SDK_INT < N) {
+                Log.w("Action", "Type input is not supported");
+                continue;
+            }
+
+            if (!(type.equals("button") || type.equals("input"))) {
+                Log.w("Action", "Unknown type: " + type);
+                continue;
+            }
+
+            actions.add(new Action(context, opts));
+        }
+
+        if (actions.isEmpty())
+            return null;
+
+        return new ActionGroup(id, actions.toArray(new Action[actions.size()]));
+    }
+
+    /**
+     * Creates an action group.
+     *
+     * @param id      The ID of the group.
+     * @param actions The list of actions.
+     */
+    private ActionGroup(String id, Action[] actions) {
+        this.id      = id;
+        this.actions = actions;
+    }
+
+    /**
+     * Gets the action group id.
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * Gets the action list.
+     */
+    public Action[] getActions() {
+        return actions;
+    }
+
+}

+ 15 - 26
src/android/notification/AbstractClearReceiver.java → src/android/notification/receiver/AbstractClearReceiver.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,19 +17,17 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
-package de.appplant.cordova.plugin.notification;
+package de.appplant.cordova.plugin.notification.receiver;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 
-import org.json.JSONException;
-import org.json.JSONObject;
+import de.appplant.cordova.plugin.notification.Manager;
+import de.appplant.cordova.plugin.notification.Notification;
 
 /**
  * Abstract delete receiver for local notifications. Creates the local
@@ -40,36 +38,27 @@ abstract public class AbstractClearReceiver extends BroadcastReceiver {
     /**
      * Called when the notification was cleared from the notification center.
      *
-     * @param context
-     *      Application context
-     * @param intent
-     *      Received intent with content data
+     * @param context Application context
+     * @param intent  Received intent with content data
      */
     @Override
     public void onReceive(Context context, Intent intent) {
-        Bundle bundle  = intent.getExtras();
-        JSONObject options;
+        Bundle bundle      = intent.getExtras();
+        int toastId        = bundle.getInt(Notification.EXTRA_ID);
+        Notification toast = Manager.getInstance(context).get(toastId);
 
-        try {
-            String data = bundle.getString(Options.EXTRA);
-            options = new JSONObject(data);
-        } catch (JSONException e) {
-            e.printStackTrace();
+        if (toast == null)
             return;
-        }
-
-        Notification notification =
-                new Builder(context, options).build();
 
-        onClear(notification);
+        onClear(toast, bundle);
     }
 
     /**
      * Called when a local notification was cleared from outside of the app.
      *
-     * @param notification
-     *      Wrapper around the local notification
+     * @param notification Wrapper around the local notification.
+     * @param bundle The bundled extras.
      */
-    abstract public void onClear (Notification notification);
+    abstract public void onClear (Notification notification, Bundle bundle);
 
 }

+ 29 - 36
src/android/notification/AbstractClickActivity.java → src/android/notification/receiver/AbstractClickReceiver.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,54 +17,48 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
-package de.appplant.cordova.plugin.notification;
+package de.appplant.cordova.plugin.notification.receiver;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 
-import org.json.JSONException;
-import org.json.JSONObject;
+import de.appplant.cordova.plugin.notification.Manager;
+import de.appplant.cordova.plugin.notification.Notification;
+
+import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
+import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
+import static de.appplant.cordova.plugin.notification.action.Action.CLICK_ACTION_ID;
+import static de.appplant.cordova.plugin.notification.action.Action.EXTRA_ID;
 
 /**
  * Abstract content receiver activity for local notifications. Creates the
  * local notification and calls the event functions for further proceeding.
  */
-abstract public class AbstractClickActivity extends Activity {
+abstract public class AbstractClickReceiver extends Activity {
 
     /**
      * Called when local notification was clicked to launch the main intent.
      *
-     * @param state
-     *      Saved instance state
+     * @param state Saved instance state
      */
     @Override
     public void onCreate (Bundle state) {
         super.onCreate(state);
 
-        Intent intent   = getIntent();
-        Bundle bundle   = intent.getExtras();
-        Context context = getApplicationContext();
-
-        try {
-            String data = bundle.getString(Options.EXTRA);
-            JSONObject options = new JSONObject(data);
+        Intent intent      = getIntent();
+        Bundle bundle      = intent.getExtras();
+        Context context    = getApplicationContext();
+        int toastId        = bundle.getInt(Notification.EXTRA_ID);
+        Notification toast = Manager.getInstance(context).get(toastId);
 
-            Builder builder =
-                    new Builder(context, options);
+        if (toast == null)
+            return;
 
-            Notification notification =
-                    buildNotification(builder);
-
-            onClick(notification);
-        } catch (JSONException e) {
-            e.printStackTrace();
-        }
+        onClick(toast, bundle);
     }
 
     /**
@@ -80,23 +74,22 @@ abstract public class AbstractClickActivity extends Activity {
     /**
      * Called when local notification was clicked by the user.
      *
-     * @param notification
-     *      Wrapper around the local notification
+     * @param notification Wrapper around the local notification.
+     * @param bundle The bundled extras.
      */
-    abstract public void onClick (Notification notification);
+    abstract public void onClick (Notification notification, Bundle bundle);
 
     /**
-     * Build notification specified by options.
-     *
-     * @param builder
-     *      Notification builder
+     * The invoked action.
      */
-    abstract public Notification buildNotification (Builder builder);
+    protected String getAction() {
+        return getIntent().getExtras().getString(EXTRA_ID, CLICK_ACTION_ID);
+    }
 
     /**
      * Launch main intent from package.
      */
-    public void launchApp() {
+    protected void launchApp() {
         Context context = getApplicationContext();
         String pkgName  = context.getPackageName();
 
@@ -105,7 +98,7 @@ abstract public class AbstractClickActivity extends Activity {
                 .getLaunchIntentForPackage(pkgName);
 
         intent.addFlags(
-                Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+                FLAG_ACTIVITY_REORDER_TO_FRONT | FLAG_ACTIVITY_SINGLE_TOP);
 
         context.startActivity(intent);
     }

+ 0 - 0
src/android/notification/AbstractRestoreReceiver.java → src/android/notification/receiver/AbstractRestoreReceiver.java


+ 81 - 0
src/android/notification/receiver/AbstractTriggerReceiver.java

@@ -0,0 +1,81 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import de.appplant.cordova.plugin.notification.Builder;
+import de.appplant.cordova.plugin.notification.Manager;
+import de.appplant.cordova.plugin.notification.Notification;
+import de.appplant.cordova.plugin.notification.Options;
+
+/**
+ * Abstract broadcast receiver for local notifications. Creates the
+ * notification options and calls the event functions for further proceeding.
+ */
+abstract public class AbstractTriggerReceiver extends BroadcastReceiver {
+
+    /**
+     * Called when an alarm was triggered.
+     *
+     * @param context Application context
+     * @param intent  Received intent with content data
+     */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Bundle bundle   = intent.getExtras();
+        int toastId     = bundle.getInt(Notification.EXTRA_ID);
+        Options options = Manager.getInstance(context).getOptions(toastId);
+
+        if (options == null)
+            return;
+
+        Builder builder    = new Builder(options);
+        Notification toast = buildNotification(builder, bundle);
+
+        if (toast == null)
+            return;
+
+        onTrigger(toast, bundle);
+    }
+
+    /**
+     * Called when a local notification was triggered.
+     *
+     * @param notification Wrapper around the local notification.
+     * @param bundle       The bundled extras.
+     */
+    abstract public void onTrigger (Notification notification, Bundle bundle);
+
+    /**
+     * Build notification specified by options.
+     *
+     * @param builder Notification builder.
+     * @param bundle  The bundled extras.
+     */
+    abstract public Notification buildNotification (Builder builder,
+                                                    Bundle bundle);
+
+}

+ 70 - 0
src/android/notification/trigger/DateTrigger.java

@@ -0,0 +1,70 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.trigger;
+
+import java.util.Calendar;
+import java.util.Date;
+
+abstract public class DateTrigger {
+
+    // Default unit is SECOND
+    public enum Unit { SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR }
+
+    // Internal counter
+    private int occurrence = 1;
+
+    /**
+     * Gets the next trigger date.
+     *
+     * @param base The date from where to calculate the trigger date.
+     *
+     * @return null if there's none next trigger date.
+     */
+    abstract public Date getNextTriggerDate(Date base);
+
+    /**
+     * The value of the occurrence.
+     */
+    public int getOccurrence() {
+        return occurrence;
+    }
+
+    /**
+     * Increase the occurrence by 1.
+     */
+    void incOccurrence() {
+        occurrence += 1;
+    }
+
+    /**
+     * Gets a calendar instance pointing to the specified date.
+     *
+     * @param date The date to point.
+     */
+    Calendar getCal (Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+
+        return cal;
+    }
+
+}

+ 105 - 0
src/android/notification/trigger/IntervalTrigger.java

@@ -0,0 +1,105 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.trigger;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Trigger class for interval based notification. Trigger by a fixed interval
+ * from now.
+ */
+public class IntervalTrigger extends DateTrigger {
+
+    // The number of ticks per interval
+    private final int ticks;
+
+    // The unit of the ticks
+    final Unit unit;
+
+    /**
+     * Interval trigger based from now.
+     *
+     * @param ticks The number of ticks per interval.
+     * @param unit  The unit of the ticks.
+     */
+    public IntervalTrigger(int ticks, Unit unit) {
+        this.ticks = ticks;
+        this.unit  = unit;
+    }
+
+    /**
+     * Gets the next trigger date.
+     *
+     * @param base The date from where to calculate the trigger date.
+     *
+     * @return null if there's none next trigger date.
+     */
+    @Override
+    public Date getNextTriggerDate(Date base) {
+        Calendar cal = getCal(base);
+
+        addInterval(cal);
+        incOccurrence();
+
+        return cal.getTime();
+    }
+
+    /**
+     * Adds the amount of ticks to the calendar.
+     *
+     * @param cal The calendar to manipulate.
+     *
+     * @return The calendar instance.
+     */
+    Calendar addInterval(Calendar cal) {
+        switch (unit) {
+            case SECOND:
+                cal.add(Calendar.SECOND, ticks);
+                break;
+            case MINUTE:
+                cal.add(Calendar.MINUTE, ticks);
+                break;
+            case HOUR:
+                cal.add(Calendar.HOUR_OF_DAY, ticks);
+                break;
+            case DAY:
+                cal.add(Calendar.DAY_OF_YEAR, ticks);
+                break;
+            case WEEK:
+                cal.add(Calendar.WEEK_OF_YEAR, ticks);
+                break;
+            case MONTH:
+                cal.add(Calendar.MONTH, ticks);
+                break;
+            case QUARTER:
+                cal.add(Calendar.MONTH, ticks * 3);
+                break;
+            case YEAR:
+                cal.add(Calendar.YEAR, ticks);
+                break;
+        }
+
+        return cal;
+    }
+
+}

+ 208 - 0
src/android/notification/trigger/MatchTrigger.java

@@ -0,0 +1,208 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.trigger;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.DAY;
+import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.HOUR;
+import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.MINUTE;
+import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.MONTH;
+import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.YEAR;
+
+/**
+ * Trigger for date matching components.
+ */
+public class MatchTrigger extends IntervalTrigger {
+
+    // Used to determine the interval
+    private static Unit[] INTERVALS = { null, MINUTE, HOUR, DAY, MONTH, YEAR };
+
+    // The date matching components
+    private final List<Integer> matchers;
+
+    /**
+     * Date matching trigger from now.
+     *
+     * @param matchers Describes the date matching parts.
+     *                 { day: 15, month: ... }
+     */
+    public MatchTrigger(List<Integer> matchers) {
+        super(1, INTERVALS[1 + matchers.indexOf(null)]);
+        this.matchers = matchers;
+    }
+
+    /**
+     * Gets the date from where to start calculating the initial trigger date.
+     */
+    private Calendar getBaseTriggerDate(Date date) {
+        Calendar cal = getCal(date);
+
+        cal.set(Calendar.SECOND, 0);
+
+        if (matchers.get(0) != null) {
+            cal.set(Calendar.MINUTE, matchers.get(0));
+        } else {
+            cal.set(Calendar.MINUTE, 0);
+        }
+
+        if (matchers.get(1) != null) {
+            cal.set(Calendar.HOUR_OF_DAY, matchers.get(1));
+        } else {
+            cal.set(Calendar.HOUR_OF_DAY, 0);
+        }
+
+        if (matchers.get(2) != null) {
+            cal.set(Calendar.DAY_OF_MONTH, matchers.get(2));
+        }
+
+        if (matchers.get(3) != null) {
+            cal.set(Calendar.MONTH, matchers.get(3));
+        }
+
+        if (matchers.get(4) != null) {
+            cal.set(Calendar.YEAR, matchers.get(4));
+        }
+
+        return cal;
+    }
+
+    /**
+     * Gets the first trigger date.
+     *
+     * @param base The date from where to calculate the trigger date.
+     *
+     * @return null if there's none trigger date.
+     */
+    private Date getTriggerDate (Date base) {
+        Calendar date = getBaseTriggerDate(base);
+        Calendar now  = getCal(base);
+
+        if (date.compareTo(now) >= 0)
+            return date.getTime();
+
+        if (unit == null || date.get(Calendar.YEAR) < now.get(Calendar.YEAR))
+            return null;
+
+        if (date.get(Calendar.MONTH) < now.get(Calendar.MONTH)) {
+            switch (unit) {
+                case MINUTE:
+                case HOUR:
+                case DAY:
+                    if (matchers.get(4) == null) {
+                        return addToDate(date, now, Calendar.YEAR, 1);
+                    } else break;
+                case YEAR:
+                    return addToDate(date, now, Calendar.YEAR, 1);
+            }
+        } else
+        if (date.get(Calendar.DAY_OF_YEAR) < now.get(Calendar.DAY_OF_YEAR)) {
+            switch (unit) {
+                case MINUTE:
+                case HOUR:
+                    if (matchers.get(3) == null) {
+                        return addToDate(date, now, Calendar.MONTH, 1);
+                    } else
+                    if (matchers.get(4) == null) {
+                        return addToDate(date, now, Calendar.YEAR, 1);
+                    }
+                    else break;
+                case MONTH:
+                    return addToDate(date, now, Calendar.MONTH, 1);
+                case YEAR:
+                    return addToDate(date, now, Calendar.YEAR, 1);
+            }
+        } else
+        if (date.get(Calendar.HOUR_OF_DAY) < now.get(Calendar.HOUR_OF_DAY)) {
+            switch (unit) {
+                case MINUTE:
+                    if (matchers.get(2) == null) {
+                        return addToDate(date, now, Calendar.DAY_OF_YEAR, 1);
+                    } else
+                    if (matchers.get(3) == null) {
+                        return addToDate(date, now, Calendar.MONTH, 1);
+                    }
+                    else break;
+                case HOUR:
+                    return addToDate(date, now, Calendar.HOUR_OF_DAY, 0);
+                case DAY:
+                    return addToDate(date, now, Calendar.DAY_OF_YEAR, 1);
+                case MONTH:
+                    return addToDate(date, now, Calendar.MONTH, 1);
+                case YEAR:
+                    return addToDate(date, now, Calendar.YEAR, 1);
+            }
+        } else
+        if (date.get(Calendar.MINUTE) < now.get(Calendar.MINUTE)) {
+            switch (unit) {
+                case MINUTE:
+                    return addToDate(date, now, Calendar.MINUTE, 1);
+                case HOUR:
+                    return addToDate(date, now, Calendar.HOUR_OF_DAY, 1);
+                case DAY:
+                    return addToDate(date, now, Calendar.DAY_OF_YEAR, 1);
+                case MONTH:
+                    return addToDate(date, now, Calendar.MONTH, 1);
+                case YEAR:
+                    return addToDate(date, now, Calendar.YEAR, 1);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the next trigger date.
+     *
+     * @param base The date from where to calculate the trigger date.
+     *
+     * @return null if there's none next trigger date.
+     */
+    @Override
+    public Date getNextTriggerDate (Date base) {
+        Date date = base;
+
+        if (getOccurrence() > 1) {
+            Calendar cal = getCal(base);
+            addInterval(cal);
+            date = cal.getTime();
+        }
+
+        incOccurrence();
+
+        return getTriggerDate(date);
+    }
+
+    /**
+     * Sets the field value of now to date and adds by count.
+     *
+     * @return The new date.
+     */
+    private Date addToDate(Calendar date, Calendar now, int field, int count) {
+        date.set(field, now.get(field));
+        date.add(field, count);
+        return date.getTime();
+    }
+
+}

+ 26 - 0
src/android/notification/util/AssetProvider.java

@@ -0,0 +1,26 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+package de.appplant.cordova.plugin.notification.util;
+
+import android.support.v4.content.FileProvider;
+
+public class AssetProvider extends FileProvider {
+    // Nothing to do here
+}

+ 73 - 138
src/android/notification/AssetUtil.java → src/android/notification/util/AssetUtil.java

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,18 +17,16 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
-package de.appplant.cordova.plugin.notification;
+package de.appplant.cordova.plugin.notification.util;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetManager;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.StrictMode;
 import android.util.Log;
@@ -50,14 +48,11 @@ import java.util.UUID;
  * within the asset resources. And res:// means a resource from the native
  * res folder. Remote assets are accessible via http:// for example.
  */
-class AssetUtil {
+public final class AssetUtil {
 
     // Name of the storage folder
     private static final String STORAGE_FOLDER = "/localnotification";
 
-    // Placeholder URI for default sound
-    private static final String DEFAULT_SOUND = "res://platform_default";
-
     // Ref to the context passed through the constructor to access the
     // resources and app directory.
     private final Context context;
@@ -65,8 +60,7 @@ class AssetUtil {
     /**
      * Constructor
      *
-     * @param context
-     *      Application context
+     * @param context Application context.
      */
     private AssetUtil(Context context) {
         this.context = context;
@@ -75,41 +69,21 @@ class AssetUtil {
     /**
      * Static method to retrieve class instance.
      *
-     * @param context
-     *      Application context
+     * @param context Application context.
      */
-    static AssetUtil getInstance(Context context) {
+    public static AssetUtil getInstance(Context context) {
         return new AssetUtil(context);
     }
 
-    /**
-     * Parse path path to native URI.
-     *
-     * @param path
-     *      Path to path file
-     */
-    Uri parseSound (String path) {
-
-        if (path == null || path.isEmpty())
-            return Uri.EMPTY;
-
-        if (path.equalsIgnoreCase(DEFAULT_SOUND)) {
-            return RingtoneManager.getDefaultUri(RingtoneManager
-                    .TYPE_NOTIFICATION);
-        }
-
-        return parse(path);
-    }
-
     /**
      * The URI for a path.
      *
-     * @param path
-     *      The given path
+     * @param path The given path.
      */
-    Uri parse (String path) {
-
-        if (path.startsWith("res:")) {
+    public Uri parse (String path) {
+        if (path == null || path.isEmpty()) {
+            return Uri.EMPTY;
+        } else if (path.startsWith("res:")) {
             return getUriForResourcePath(path);
         } else if (path.startsWith("file:///")) {
             return getUriFromPath(path);
@@ -125,11 +99,9 @@ class AssetUtil {
     /**
      * URI for a file.
      *
-     * @param path
-     *      Absolute path like file:///...
+     * @param path Absolute path like file:///...
      *
-     * @return
-     *      URI pointing to the given path
+     * @return URI pointing to the given path.
      */
     private Uri getUriFromPath(String path) {
         String absPath = path.replaceFirst("file://", "");
@@ -140,17 +112,15 @@ class AssetUtil {
             return Uri.EMPTY;
         }
 
-        return Uri.fromFile(file);
+        return getUriFromFile(file);
     }
 
     /**
      * URI for an asset.
      *
-     * @param path
-     *      Asset path like file://...
+     * @param path Asset path like file://...
      *
-     * @return
-     *      URI pointing to the given path
+     * @return URI pointing to the given path.
      */
     private Uri getUriFromAsset(String path) {
         String resPath  = path.replaceFirst("file:/", "www");
@@ -172,7 +142,7 @@ class AssetUtil {
             outStream.flush();
             outStream.close();
 
-            return Uri.fromFile(file);
+            return getUriFromFile(file);
 
         } catch (Exception e) {
             Log.e("Asset", "File not found: assets/" + resPath);
@@ -185,53 +155,34 @@ class AssetUtil {
     /**
      * The URI for a resource.
      *
-     * @param path
-     *            The given relative path
+     * @param path The given relative path.
      *
-     * @return
-     *      URI pointing to the given path
+     * @return URI pointing to the given path.
      */
     private Uri getUriForResourcePath(String path) {
+        Resources res  = context.getResources();
         String resPath = path.replaceFirst("res://", "");
-        int resId      = getResIdForDrawable(resPath);
-        File file      = getTmpFile();
+        int resId      = getResId(resPath);
 
         if (resId == 0) {
             Log.e("Asset", "File not found: " + resPath);
             return Uri.EMPTY;
         }
 
-        if (file == null) {
-            Log.e("Asset", "Missing external cache dir");
-            return Uri.EMPTY;
-        }
-
-        try {
-            Resources res = context.getResources();
-            FileOutputStream outStream = new FileOutputStream(file);
-            InputStream inputStream = res.openRawResource(resId);
-            copyFile(inputStream, outStream);
-
-            outStream.flush();
-            outStream.close();
-
-            return Uri.fromFile(file);
-
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-
-        return Uri.EMPTY;
+        return new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+                .authority(res.getResourcePackageName(resId))
+                .appendPath(res.getResourceTypeName(resId))
+                .appendPath(res.getResourceEntryName(resId))
+                .build();
     }
 
     /**
      * Uri from remote located content.
      *
-     * @param path
-     *      Remote address
+     * @param path Remote address.
      *
-     * @return
-     *      Uri of the downloaded file
+     * @return Uri of the downloaded file.
      */
     private Uri getUriFromRemote(String path) {
         File file = getTmpFile();
@@ -262,7 +213,7 @@ class AssetUtil {
             outStream.flush();
             outStream.close();
 
-            return Uri.fromFile(file);
+            return getUriFromFile(file);
 
         } catch (MalformedURLException e) {
             Log.e("Asset", "Incorrect URL");
@@ -281,10 +232,8 @@ class AssetUtil {
     /**
      * Copy content from input stream into output stream.
      *
-     * @param in
-     *      The input stream
-     * @param out
-     *      The output stream
+     * @param in  The input stream.
+     * @param out The output stream.
      */
     private void copyFile(InputStream in, OutputStream out) throws IOException {
         byte[] buffer = new byte[1024];
@@ -298,14 +247,15 @@ class AssetUtil {
     /**
      * Resource ID for drawable.
      *
-     * @param resPath
-     *      Resource path as string
+     * @param resPath Resource path as string.
+     *
+     * @return The resource ID or 0 if not found.
      */
-    int getResIdForDrawable(String resPath) {
-        int resId = getResIdForDrawable(getPkgName(), resPath);
+    public int getResId(String resPath) {
+        int resId = getResId(context.getResources(), resPath);
 
         if (resId == 0) {
-            resId = getResIdForDrawable("android", resPath);
+            resId = getResId(Resources.getSystem(), resPath);
         }
 
         return resId;
@@ -314,64 +264,39 @@ class AssetUtil {
     /**
      * Resource ID for drawable.
      *
-     * @param clsName
-     *      Relative package or global android name space
-     * @param resPath
-     *      Resource path as string
-     */
-    int getResIdForDrawable(String clsName, String resPath) {
-        String drawable = getBaseName(resPath);
-        int resId = 0;
-
-        try {
-            Class<?> cls  = Class.forName(clsName + ".R$drawable");
-
-            resId = (Integer) cls.getDeclaredField(drawable).get(Integer.class);
-        } catch (Exception ignore) {}
-
-        return resId;
-    }
-
-    /**
-     * Convert drawable resource to bitmap.
+     * @param res     The resources where to look for.
+     * @param resPath The name of the resource.
      *
-     * @param drawable
-     *      Drawable resource name
+     * @return The resource ID or 0 if not found.
      */
-    Bitmap getIconFromDrawable (String drawable) {
-        Resources res = context.getResources();
-        int iconId;
-
-        iconId = getResIdForDrawable(getPkgName(), drawable);
+    private int getResId(Resources res, String resPath) {
+        String pkgName = getPkgName(res);
+        String resName = getBaseName(resPath);
+        int resId;
 
-        if (iconId == 0) {
-            iconId = getResIdForDrawable("android", drawable);
-        }
+        resId = res.getIdentifier(resName, "mipmap", pkgName);
 
-        if (iconId == 0) {
-            iconId = android.R.drawable.screen_background_dark_transparent;
+        if (resId == 0) {
+            resId = res.getIdentifier(resName, "drawable", pkgName);
         }
 
-        return BitmapFactory.decodeResource(res, iconId);
+        return resId;
     }
 
     /**
      * Convert URI to Bitmap.
      *
-     * @param uri
-     *      Internal image URI
+     * @param uri Internal image URI
      */
-    Bitmap getIconFromUri (Uri uri) throws IOException {
+    public Bitmap getIconFromUri(Uri uri) throws IOException {
         InputStream input = context.getContentResolver().openInputStream(uri);
-
         return BitmapFactory.decodeStream(input);
     }
 
     /**
      * Extract name of drawable resource from path.
      *
-     * @param resPath
-     *      Resource path as string
+     * @param resPath Resource path as string.
      */
     private String getBaseName (String resPath) {
         String drawable = resPath;
@@ -390,8 +315,7 @@ class AssetUtil {
     /**
      * Returns a file located under the external cache dir of that app.
      *
-     * @return
-     *      File with a random UUID name
+     * @return File with a random UUID name.
      */
     private File getTmpFile () {
         // If random UUID is not be enough see
@@ -402,10 +326,9 @@ class AssetUtil {
     /**
      * Returns a file located under the external cache dir of that app.
      *
-     * @param name
-     *      The name of the file
-     * @return
-     *      File with the provided name
+     * @param name The name of the file.
+     *
+     * @return File with the provided name.
      */
     private File getTmpFile (String name) {
         File dir = context.getExternalCacheDir();
@@ -424,10 +347,22 @@ class AssetUtil {
     }
 
     /**
-     * Package name specified by context.
+     * Get content URI for the specified file.
+     *
+     * @param file The file to get the URI.
+     *
+     * @return content://...
+     */
+    private Uri getUriFromFile(File file) {
+        String authority = context.getPackageName() + ".provider";
+        return AssetProvider.getUriForFile(context, authority, file);
+    }
+
+    /**
+     * Package name specified by the resource bundle.
      */
-    private String getPkgName () {
-        return context.getPackageName();
+    private String getPkgName (Resources res) {
+        return res == Resources.getSystem() ? "android" : context.getPackageName();
     }
 
 }

+ 24 - 0
src/android/xml/localnotification_provider_paths.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <external-path name="external_files" path="."/>
+</paths>

+ 37 - 41
src/ios/APPLocalNotification.h

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,62 +17,58 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
-#import <Foundation/Foundation.h>
 #import <Cordova/CDVPlugin.h>
 
-@interface APPLocalNotification : CDVPlugin
+@import UserNotifications;
+
+@interface APPLocalNotification : CDVPlugin <UNUserNotificationCenterDelegate>
 
+// Set launchDetails object
+- (void) launch:(CDVInvokedUrlCommand*)command;
 // Execute all queued events
-- (void) deviceready:(CDVInvokedUrlCommand*)command;
+- (void) ready:(CDVInvokedUrlCommand*)command;
+
+// Check permission to show notifications
+- (void) check:(CDVInvokedUrlCommand*)command;
+// Request permission to show notifications
+- (void) request:(CDVInvokedUrlCommand*)command;
 
-// Inform if the app has the permission to show notifications
-- (void) hasPermission:(CDVInvokedUrlCommand*)command;
-// Register permission to show notifications
-- (void) registerPermission:(CDVInvokedUrlCommand*)command;
+// Register/update an action group
+- (void) actions:(CDVInvokedUrlCommand*)command;
 
-// Schedule set of notifications
+// Schedule notifications
 - (void) schedule:(CDVInvokedUrlCommand*)command;
 // Update set of notifications
 - (void) update:(CDVInvokedUrlCommand*)command;
-// Cancel set of notifications
-- (void) cancel:(CDVInvokedUrlCommand*)command;
-// Cancel all notifications
-- (void) cancelAll:(CDVInvokedUrlCommand*)command;
-// Clear set of notifications
+// Clear notifications by id
 - (void) clear:(CDVInvokedUrlCommand*)command;
 // Clear all notifications
 - (void) clearAll:(CDVInvokedUrlCommand*)command;
+// Cancel notifications by id
+- (void) cancel:(CDVInvokedUrlCommand*)command;
+// Cancel all notifications
+- (void) cancelAll:(CDVInvokedUrlCommand*)command;
 
-// If a notification with an ID is present
-- (void) isPresent:(CDVInvokedUrlCommand*)command;
-// If a notification with an ID is scheduled
-- (void) isScheduled:(CDVInvokedUrlCommand*)command;
-// If a notification with an ID is triggered
-- (void) isTriggered:(CDVInvokedUrlCommand*)command;
+// Notification type
+- (void) type:(CDVInvokedUrlCommand*)command;
 
-// List all ids from all local notifications
-- (void) getAllIds:(CDVInvokedUrlCommand*)command;
-// List all ids from all pending notifications
-- (void) getScheduledIds:(CDVInvokedUrlCommand*)command;
-// List all ids from all triggered notifications
-- (void) getTriggeredIds:(CDVInvokedUrlCommand*)command;
+// List of all notification IDs
+- (void) ids:(CDVInvokedUrlCommand*)command;
+// List of all scheduled notification IDs
+- (void) scheduledIds:(CDVInvokedUrlCommand*)command;
+// List of all triggered notification IDs
+- (void) triggeredIds:(CDVInvokedUrlCommand*)command;
 
-// Propertys for given local notification
-- (void) getSingle:(CDVInvokedUrlCommand*)command;
-// Propertya for given scheduled notification
-- (void) getSingleScheduled:(CDVInvokedUrlCommand*)command;
-// Propertys for given triggered notification
-- (void) getSingleTriggered:(CDVInvokedUrlCommand*)command;
+// Notification by id
+- (void) notification:(CDVInvokedUrlCommand*)command;
 
-// Property list for given local notifications
-- (void) getAll:(CDVInvokedUrlCommand*)command;
-// Property list for given scheduled notifications
-- (void) getScheduled:(CDVInvokedUrlCommand*)command;
-// Property list for given triggered notifications
-- (void) getTriggered:(CDVInvokedUrlCommand*)command;
+// List of notifications by id
+- (void) notifications:(CDVInvokedUrlCommand*)command;
+// List of scheduled notifications by id
+- (void) scheduledNotifications:(CDVInvokedUrlCommand*)command;
+// List of triggered notifications by id
+- (void) triggeredNotifications:(CDVInvokedUrlCommand*)command;
 
 @end

+ 332 - 358
src/ios/APPLocalNotification.m

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,39 +17,58 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
 #import "APPLocalNotification.h"
-#import "APPLocalNotificationOptions.h"
-#import "UIApplication+APPLocalNotification.h"
-#import "UILocalNotification+APPLocalNotification.h"
+#import "APPNotificationOptions.h"
+#import "UNUserNotificationCenter+APPLocalNotification.h"
+#import "UNNotificationRequest+APPLocalNotification.h"
+#import "APPNotificationContent.h"
 
 @interface APPLocalNotification ()
 
-// Retrieves the application state
-@property (readonly, getter=applicationState) NSString* applicationState;
-// All events will be queued until deviceready has been fired
+@property (strong, nonatomic) UNUserNotificationCenter* center;
 @property (readwrite, assign) BOOL deviceready;
-// Event queue
+@property (readwrite, assign) BOOL isActive;
+@property (readonly, nonatomic, retain) NSArray* launchDetails;
 @property (readonly, nonatomic, retain) NSMutableArray* eventQueue;
-// Needed when calling `registerPermission`
-@property (nonatomic, retain) CDVInvokedUrlCommand* command;
 
 @end
 
 @implementation APPLocalNotification
 
-@synthesize deviceready, eventQueue;
+@synthesize deviceready, isActive, eventQueue;
 
 #pragma mark -
 #pragma mark Interface
 
+/**
+ * Set launchDetails object.
+ *
+ * @return [ Void ]
+ */
+- (void) launch:(CDVInvokedUrlCommand*)command
+{
+    NSString* js;
+
+    if (!_launchDetails)
+        return;
+
+    js = [NSString stringWithFormat:
+          @"cordova.plugins.notification.local.launchDetails = {id:%@, action:'%@'}",
+          _launchDetails[0], _launchDetails[1]];
+
+    [self.commandDelegate evalJs:js];
+
+    _launchDetails = NULL;
+}
+
 /**
  * Execute all queued events.
+ *
+ * @return [ Void ]
  */
-- (void) deviceready:(CDVInvokedUrlCommand*)command
+- (void) ready:(CDVInvokedUrlCommand*)command
 {
     deviceready = YES;
 
@@ -61,10 +80,11 @@
 }
 
 /**
- * Schedule a set of notifications.
+ * Schedule notifications.
  *
- * @param properties
- *      A dict of properties for each notification
+ * @param [Array<Hash>] properties A list of key-value properties.
+ *
+ * @return [ Void ]
  */
 - (void) schedule:(CDVInvokedUrlCommand*)command
 {
@@ -72,28 +92,24 @@
 
     [self.commandDelegate runInBackground:^{
         for (NSDictionary* options in notifications) {
-            UILocalNotification* notification;
+            APPNotificationContent* notification;
 
-            notification = [[UILocalNotification alloc]
+            notification = [[APPNotificationContent alloc]
                             initWithOptions:options];
 
-            [self scheduleLocalNotification:[notification copy]];
-            [self fireEvent:@"schedule" notification:notification];
-
-            if (notifications.count > 1) {
-                [NSThread sleepForTimeInterval:0.01];
-            }
+            [self scheduleNotification:notification];
         }
 
         [self execCallback:command];
-    }];
+     }];
 }
 
 /**
- * Update a set of notifications.
+ * Update notifications.
+ *
+ * @param [Array<Hash>] properties A list of key-value properties.
  *
- * @param properties
- *      A dict of properties for each notification
+ * @return [ Void ]
  */
 - (void) update:(CDVInvokedUrlCommand*)command
 {
@@ -102,21 +118,17 @@
     [self.commandDelegate runInBackground:^{
         for (NSDictionary* options in notifications) {
             NSNumber* id = [options objectForKey:@"id"];
-            UILocalNotification* notification;
+            UNNotificationRequest* notification;
 
-            notification = [self.app localNotificationWithId:id];
+            notification = [_center getNotificationWithId:id];
 
             if (!notification)
                 continue;
 
-            [self updateLocalNotification:[notification copy]
-                              withOptions:options];
+            [self updateNotification:[notification copy]
+                         withOptions:options];
 
             [self fireEvent:@"update" notification:notification];
-
-            if (notifications.count > 1) {
-                [NSThread sleepForTimeInterval:0.01];
-            }
         }
 
         [self execCallback:command];
@@ -124,24 +136,25 @@
 }
 
 /**
- * Cancel a set of notifications.
+ * Clear notifications by id.
  *
- * @param ids
- *      The IDs of the notifications
+ * @param [ Array<Int> ] The IDs of the notifications to clear.
+ *
+ * @return [ Void ]
  */
-- (void) cancel:(CDVInvokedUrlCommand*)command
+- (void) clear:(CDVInvokedUrlCommand*)command
 {
     [self.commandDelegate runInBackground:^{
         for (NSNumber* id in command.arguments) {
-            UILocalNotification* notification;
+            UNNotificationRequest* notification;
 
-            notification = [self.app localNotificationWithId:id];
+            notification = [_center getNotificationWithId:id];
 
             if (!notification)
                 continue;
 
-            [self.app cancelLocalNotification:notification];
-            [self fireEvent:@"cancel" notification:notification];
+            [_center clearNotification:notification];
+            [self fireEvent:@"clear" notification:notification];
         }
 
         [self execCallback:command];
@@ -149,36 +162,40 @@
 }
 
 /**
- * Cancel all local notifications.
+ * Clear all local notifications.
+ *
+ * @return [ Void ]
  */
-- (void) cancelAll:(CDVInvokedUrlCommand*)command
+- (void) clearAll:(CDVInvokedUrlCommand*)command
 {
     [self.commandDelegate runInBackground:^{
-        [self cancelAllLocalNotifications];
-        [self fireEvent:@"cancelall"];
+        [_center clearAllNotifications];
+        [self clearApplicationIconBadgeNumber];
+        [self fireEvent:@"clearall"];
         [self execCallback:command];
     }];
 }
 
 /**
- * Clear a set of notifications.
+ * Cancel notifications by id.
+ *
+ * @param [ Array<Int> ] The IDs of the notifications to clear.
  *
- * @param ids
- *      The IDs of the notifications
+ * @return [ Void ]
  */
-- (void) clear:(CDVInvokedUrlCommand*)command
+- (void) cancel:(CDVInvokedUrlCommand*)command
 {
     [self.commandDelegate runInBackground:^{
         for (NSNumber* id in command.arguments) {
-            UILocalNotification* notification;
+            UNNotificationRequest* notification;
 
-            notification = [self.app localNotificationWithId:id];
+            notification = [_center getNotificationWithId:id];
 
             if (!notification)
                 continue;
 
-            [self.app clearLocalNotification:notification];
-            [self fireEvent:@"clear" notification:notification];
+            [_center cancelNotification:notification];
+            [self fireEvent:@"cancel" notification:notification];
         }
 
         [self execCallback:command];
@@ -186,73 +203,47 @@
 }
 
 /**
- * Clear all local notifications.
+ * Cancel all local notifications.
+ *
+ * @return [ Void ]
  */
-- (void) clearAll:(CDVInvokedUrlCommand*)command
+- (void) cancelAll:(CDVInvokedUrlCommand*)command
 {
     [self.commandDelegate runInBackground:^{
-        [self clearAllLocalNotifications];
-        [self fireEvent:@"clearall"];
+        [_center cancelAllNotifications];
+        [self clearApplicationIconBadgeNumber];
+        [self fireEvent:@"cancelall"];
         [self execCallback:command];
     }];
 }
 
 /**
- * If a notification by ID is present.
- *
- * @param id
- *      The ID of the notification
- */
-- (void) isPresent:(CDVInvokedUrlCommand *)command
-{
-    [self isPresent:command type:NotifcationTypeAll];
-}
-
-/**
- * If a notification by ID is scheduled.
- *
- * @param id
- *      The ID of the notification
- */
-- (void) isScheduled:(CDVInvokedUrlCommand*)command
-{
-    [self isPresent:command type:NotifcationTypeScheduled];
-}
-
-/**
- * Check if a notification with an ID is triggered.
+ * Get type of notification.
  *
- * @param id
- *      The ID of the notification
- */
-- (void) isTriggered:(CDVInvokedUrlCommand*)command
-{
-    [self isPresent:command type:NotifcationTypeTriggered];
-}
-
-/**
- * Check if a notification with an ID exists.
+ * @param [ Int ] id The ID of the notification.
  *
- * @param type
- *      The notification life cycle type
+ * @return [ Void ]
  */
-- (void) isPresent:(CDVInvokedUrlCommand*)command
-              type:(APPLocalNotificationType)type;
+- (void) type:(CDVInvokedUrlCommand*)command
 {
     [self.commandDelegate runInBackground:^{
         NSNumber* id = [command argumentAtIndex:0];
-        BOOL exist;
-
-        CDVPluginResult* result;
-
-        if (type == NotifcationTypeAll) {
-            exist = [self.app localNotificationExist:id];
-        } else {
-            exist = [self.app localNotificationExist:id type:type];
+        NSString* type;
+
+        switch ([_center getTypeOfNotificationWithId:id]) {
+            case NotifcationTypeScheduled:
+                type = @"scheduled";
+                break;
+            case NotifcationTypeTriggered:
+                type = @"triggered";
+                break;
+            default:
+                type = @"unknown";
         }
 
+        CDVPluginResult* result;
         result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
-                                     messageAsBool:exist];
+                                     messageAsString:type];
 
         [self.commandDelegate sendPluginResult:result
                                     callbackId:command.callbackId];
@@ -260,50 +251,49 @@
 }
 
 /**
- * List all ids from all local notifications.
+ * List of all notification IDs.
+ *
+ * @return [ Void ]
  */
-- (void) getAllIds:(CDVInvokedUrlCommand*)command
+- (void) ids:(CDVInvokedUrlCommand*)command
 {
-    [self getIds:command byType:NotifcationTypeAll];
+    [self ids:command byType:NotifcationTypeAll];
 }
 
 /**
- * List all ids from all pending notifications.
+ * List of all scheduled notification IDs.
+ *
+ * @return [ Void ]
  */
-- (void) getScheduledIds:(CDVInvokedUrlCommand*)command
+- (void) scheduledIds:(CDVInvokedUrlCommand*)command
 {
-    [self getIds:command byType:NotifcationTypeScheduled];
+    [self ids:command byType:NotifcationTypeScheduled];
 }
 
 /**
- * List all ids from all triggered notifications.
+ * List of all triggered notification IDs.
+ *
+ * @return [ Void ]
  */
-- (void) getTriggeredIds:(CDVInvokedUrlCommand*)command
+- (void) triggeredIds:(CDVInvokedUrlCommand*)command
 {
-    [self getIds:command byType:NotifcationTypeTriggered];
+    [self ids:command byType:NotifcationTypeTriggered];
 }
 
 /**
  * List of ids for given local notifications.
  *
- * @param type
- *      Notification life cycle type
- * @param ids
- *      The IDs of the notifications
+ * @param [ APPNotificationType ] type The type of notifications to look for.
+ *
+ * @return [ Void ]
  */
-- (void) getIds:(CDVInvokedUrlCommand*)command
-         byType:(APPLocalNotificationType)type;
+- (void) ids:(CDVInvokedUrlCommand*)command
+      byType:(APPNotificationType)type;
 {
     [self.commandDelegate runInBackground:^{
-        CDVPluginResult* result;
-        NSArray* ids;
-
-        if (type == NotifcationTypeAll) {
-            ids = [self.app localNotificationIds];
-        } else {
-            ids = [self.app localNotificationIdsByType:type];
-        }
+        NSArray* ids = [_center getNotificationIdsByType:type];
 
+        CDVPluginResult* result;
         result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
                                     messageAsArray:ids];
 
@@ -313,86 +303,89 @@
 }
 
 /**
- * Propertys for given local notification.
+ * Notification by id.
+ *
+ * @param [ Number ] id The id of the notification to return.
+ *
+ * @return [ Void ]
  */
-- (void) getSingle:(CDVInvokedUrlCommand*)command
+- (void) notification:(CDVInvokedUrlCommand*)command
 {
-    [self getOption:command byType:NotifcationTypeAll];
-}
+    [self.commandDelegate runInBackground:^{
+        NSArray* ids = command.arguments;
 
-/**
- * Propertya for given scheduled notification.
- */
-- (void) getSingleScheduled:(CDVInvokedUrlCommand*)command
-{
-    [self getOption:command byType:NotifcationTypeScheduled];
-}
+        NSArray* notifications;
+        notifications = [_center getNotificationOptionsById:ids];
 
-// Propertys for given triggered notification
-- (void) getSingleTriggered:(CDVInvokedUrlCommand*)command
-{
-    [self getOption:command byType:NotifcationTypeTriggered];
+        CDVPluginResult* result;
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
+                               messageAsDictionary:[notifications firstObject]];
+
+        [self.commandDelegate sendPluginResult:result
+                                    callbackId:command.callbackId];
+    }];
 }
 
 /**
- * Property list for given local notifications.
+ * List of notifications by id.
+ *
+ * @param [ Array<Number> ] ids The ids of the notifications to return.
  *
- * @param ids
- *      The IDs of the notifications
+ * @return [ Void ]
  */
-- (void) getAll:(CDVInvokedUrlCommand*)command
+- (void) notifications:(CDVInvokedUrlCommand*)command
 {
-    [self getOptions:command byType:NotifcationTypeAll];
+    [self notifications:command byType:NotifcationTypeAll];
 }
 
 /**
- * Property list for given scheduled notifications.
+ * List of scheduled notifications by id.
+ *
+ * @param [ Array<Number> ] ids The ids of the notifications to return.
  *
- * @param ids
- *      The IDs of the notifications
+ * @return [ Void ]
  */
-- (void) getScheduled:(CDVInvokedUrlCommand*)command
+- (void) scheduledNotifications:(CDVInvokedUrlCommand*)command
 {
-    [self getOptions:command byType:NotifcationTypeScheduled];
+    [self notifications:command byType:NotifcationTypeScheduled];
 }
 
 /**
- * Property list for given triggered notifications.
+ * List of triggered notifications by id.
  *
- * @param ids
- *      The IDs of the notifications
+ * @param [ Array<Number> ] ids The ids of the notifications to return.
+ *
+ * @return [ Void ]
  */
-- (void) getTriggered:(CDVInvokedUrlCommand *)command
+- (void) triggeredNotifications:(CDVInvokedUrlCommand *)command
 {
-    [self getOptions:command byType:NotifcationTypeTriggered];
+    [self notifications:command byType:NotifcationTypeTriggered];
 }
 
 /**
- * Propertys for given triggered notification.
+ * List of notifications by type or id.
+ *
+ * @param [ APPNotificationType ] type The type of notifications to look for.
  *
- * @param type
- *      Notification life cycle type
- * @param ids
- *      The ID of the notification
+ * @return [ Void ]
  */
-- (void) getOption:(CDVInvokedUrlCommand*)command
-            byType:(APPLocalNotificationType)type;
+- (void) notifications:(CDVInvokedUrlCommand*)command
+                byType:(APPNotificationType)type;
 {
     [self.commandDelegate runInBackground:^{
         NSArray* ids = command.arguments;
         NSArray* notifications;
-        CDVPluginResult* result;
 
-        if (type == NotifcationTypeAll) {
-            notifications = [self.app localNotificationOptionsById:ids];
+        if (ids.count > 0) {
+            notifications = [_center getNotificationOptionsById:ids];
         }
         else {
-            notifications = [self.app localNotificationOptionsByType:type
-                                                               andId:ids];
+            notifications = [_center getNotificationOptionsByType:type];
         }
 
+        CDVPluginResult* result;
         result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
-                               messageAsDictionary:[notifications firstObject]];
+                                    messageAsArray:notifications];
 
         [self.commandDelegate sendPluginResult:result
                                     callbackId:command.callbackId];
@@ -400,37 +393,20 @@
 }
 
 /**
- * Property list for given triggered notifications.
+ * Check for permission to show notifications.
  *
- * @param type
- *      Notification life cycle type
- * @param ids
- *      The IDs of the notifications
+ * @return [ Void ]
  */
-- (void) getOptions:(CDVInvokedUrlCommand*)command
-             byType:(APPLocalNotificationType)type;
+- (void) check:(CDVInvokedUrlCommand*)command
 {
-    [self.commandDelegate runInBackground:^{
-        NSArray* ids = command.arguments;
-        NSArray* notifications;
-        CDVPluginResult* result;
-
-        if (type == NotifcationTypeAll && ids.count == 0) {
-            notifications = [self.app localNotificationOptions];
-        }
-        else if (type == NotifcationTypeAll) {
-            notifications = [self.app localNotificationOptionsById:ids];
-        }
-        else if (ids.count == 0) {
-            notifications = [self.app localNotificationOptionsByType:type];
-        }
-        else {
-            notifications = [self.app localNotificationOptionsByType:type
-                                                               andId:ids];
-        }
+    [_center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) {
+        BOOL authorized = settings.authorizationStatus == UNAuthorizationStatusAuthorized;
+        BOOL enabled = settings.notificationCenterSetting == UNNotificationSettingEnabled;
+        BOOL permitted = authorized && enabled;
 
+        CDVPluginResult* result;
         result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
-                                    messageAsArray:notifications];
+                                     messageAsBool:permitted];
 
         [self.commandDelegate sendPluginResult:result
                                     callbackId:command.callbackId];
@@ -438,186 +414,142 @@
 }
 
 /**
- * Inform if the app has the permission to show
- * badges and local notifications.
+ * Request for permission to show notifcations.
+ *
+ * @return [ Void ]
  */
-- (void) hasPermission:(CDVInvokedUrlCommand*)command
+- (void) request:(CDVInvokedUrlCommand*)command
 {
-    [self.commandDelegate runInBackground:^{
-        CDVPluginResult* result;
-        BOOL hasPermission;
-
-        hasPermission = [self.app hasPermissionToScheduleLocalNotifications];
+    UNAuthorizationOptions options =
+    (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert);
 
-        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
-                                     messageAsBool:hasPermission];
-
-        [self.commandDelegate sendPluginResult:result
-                                    callbackId:command.callbackId];
+    [_center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError* e) {
+        [self check:command];
     }];
 }
 
 /**
- * Ask for permission to show badges.
+ * Register/update an action group.
+ *
+ * @return [ Void ]
  */
-- (void) registerPermission:(CDVInvokedUrlCommand*)command
+- (void) actions:(CDVInvokedUrlCommand *)command
 {
-    if ([[UIApplication sharedApplication]
-         respondsToSelector:@selector(registerUserNotificationSettings:)])
-    {
-        _command = command;
+    [self.commandDelegate runInBackground:^{
+        NSDictionary* options = command.arguments[0];
+        APPNotificationContent* notification;
 
-        [self.commandDelegate runInBackground:^{
-            [self.app registerPermissionToScheduleLocalNotifications];
-        }];
-    } else {
-        [self hasPermission:command];
-    }
+        notification = [[APPNotificationContent alloc]
+                        initWithOptions:options];
+
+        [_center addNotificationCategory:notification.category];
+        [self execCallback:command];
+    }];
 }
 
 #pragma mark -
-#pragma mark Core Logic
+#pragma mark Private
 
 /**
  * Schedule the local notification.
+ *
+ * @param [ APPNotificationContent* ] notification The notification to schedule.
+ *
+ * @return [ Void ]
  */
-- (void) scheduleLocalNotification:(UILocalNotification*)notification
+- (void) scheduleNotification:(APPNotificationContent*)notification
 {
-    [self cancelForerunnerLocalNotification:notification];
-    [self.app scheduleLocalNotification:notification];
+    __weak APPLocalNotification* weakSelf  = self;
+    UNNotificationRequest* request = notification.request;
+    NSString* event = [notification.request wasUpdated] ? @"update" : @"add";
+
+    [_center addNotificationCategory:notification.category];
+
+    [_center addNotificationRequest:request withCompletionHandler:^(NSError* e) {
+        __strong APPLocalNotification* strongSelf = weakSelf;
+        [strongSelf fireEvent:event notification:request];
+    }];
 }
 
 /**
  * Update the local notification.
+ *
+ * @param [ UNNotificationRequest* ] notification The notification to update.
+ * @param [ NSDictionary* ] options The options to update.
+ *
+ * @return [ Void ]
  */
-- (void) updateLocalNotification:(UILocalNotification*)notification
-                     withOptions:(NSDictionary*)newOptions
+- (void) updateNotification:(UNNotificationRequest*)notification
+                withOptions:(NSDictionary*)newOptions
 {
-    NSMutableDictionary* options = [notification.userInfo mutableCopy];
+    NSMutableDictionary* options = [notification.content.userInfo mutableCopy];
 
     [options addEntriesFromDictionary:newOptions];
     [options setObject:[NSDate date] forKey:@"updatedAt"];
 
-    notification = [[UILocalNotification alloc]
-                    initWithOptions:options];
+    APPNotificationContent*
+    newNotification = [[APPNotificationContent alloc] initWithOptions:options];
 
-    [self scheduleLocalNotification:notification];
+    [self scheduleNotification:newNotification];
 }
 
-/**
- * Cancel all local notifications.
- */
-- (void) cancelAllLocalNotifications
-{
-    [self.app cancelAllLocalNotifications];
-    [self.app setApplicationIconBadgeNumber:0];
-}
-
-/**
- * Clear all local notifications.
- */
-- (void) clearAllLocalNotifications
-{
-    [self.app clearAllLocalNotifications];
-    [self.app setApplicationIconBadgeNumber:0];
-}
-
-/**
- * Cancel a maybe given forerunner with the same ID.
- */
-- (void) cancelForerunnerLocalNotification:(UILocalNotification*)notification
-{
-    NSNumber* id = notification.options.id;
-    UILocalNotification* forerunner;
-
-    forerunner = [self.app localNotificationWithId:id];
-
-    if (!forerunner)
-        return;
-
-    [self.app cancelLocalNotification:forerunner];
-}
+#pragma mark -
+#pragma mark UNUserNotificationCenterDelegate
 
 /**
- * Cancels all non-repeating local notification older then
- * a specific amount of seconds
+ * Called when a notification is delivered to the app while being in foreground.
  */
-- (void) cancelAllNotificationsWhichAreOlderThen:(float)seconds
+- (void) userNotificationCenter:(UNUserNotificationCenter *)center
+        willPresentNotification:(UNNotification *)notification
+          withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
 {
-    NSArray* notifications;
-
-    notifications = [self.app localNotifications];
+    if (![notification.request wasUpdated]) {
+        [self fireEvent:@"trigger" notification:notification.request];
+    }
 
-    for (UILocalNotification* notification in notifications)
-    {
-        if (![notification isRepeating]
-            && notification.timeIntervalSinceFireDate > seconds)
-        {
-            [self.app cancelLocalNotification:notification];
-            [self fireEvent:@"cancel" notification:notification];
-        }
+    if (notification.request.options.silent) {
+        completionHandler(UNNotificationPresentationOptionNone);
+    } else {
+        completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);
     }
 }
 
-#pragma mark -
-#pragma mark Delegates
-
 /**
- * Calls the cancel or trigger event after a local notification was received.
- * Cancels the local notification if autoCancel was set to true.
+ * Called to let your app know which action was selected by the user for a given
+ * notification.
  */
-- (void) didReceiveLocalNotification:(NSNotification*)localNotification
+- (void) userNotificationCenter:(UNUserNotificationCenter *)center
+ didReceiveNotificationResponse:(UNNotificationResponse *)response
+          withCompletionHandler:(void (^)())completionHandler
 {
-    UILocalNotification* notification = [localNotification object];
-
-    if ([notification userInfo] == NULL || [notification wasUpdated])
-        return;
+    UNNotificationRequest* notification = response.notification.request;
+    NSMutableDictionary* data           = [[NSMutableDictionary alloc] init];
+    NSString* action                    = response.actionIdentifier;
+    NSString* event                     = action;
 
-    NSTimeInterval timeInterval = [notification timeIntervalSinceLastTrigger];
-    NSString* event = timeInterval < 0.2 && deviceready ? @"trigger" : @"click";
+    completionHandler();
 
-    [self fireEvent:event notification:notification];
+    if ([action isEqualToString:UNNotificationDefaultActionIdentifier]) {
+        event = @"click";
+    } else
+    if ([action isEqualToString:UNNotificationDismissActionIdentifier]) {
+        event = @"clear";
+    }
 
-    if (![event isEqualToString:@"click"])
-        return;
+    if (!deviceready && [event isEqualToString:@"click"]) {
+        _launchDetails = @[notification.options.id, event];
+    }
 
-    if ([notification isRepeating]) {
+    if (![event isEqualToString:@"clear"]) {
         [self fireEvent:@"clear" notification:notification];
-    } else {
-        [self.app cancelLocalNotification:notification];
-        [self fireEvent:@"cancel" notification:notification];
     }
-}
-
-/**
- * Called when app has started
- * (by clicking on a local notification).
- */
-- (void) didFinishLaunchingWithOptions:(NSNotification*)notification
-{
-    NSDictionary* launchOptions = [notification userInfo];
-
-    UILocalNotification* localNotification;
 
-    localNotification = [launchOptions objectForKey:
-                         UIApplicationLaunchOptionsLocalNotificationKey];
-
-    if (localNotification) {
-        [self didReceiveLocalNotification:
-         [NSNotification notificationWithName:CDVLocalNotification
-                                       object:localNotification]];
+    if ([response isKindOfClass:UNTextInputNotificationResponse.class]) {
+        [data setObject:((UNTextInputNotificationResponse*) response).userText
+                 forKey:@"text"];
     }
-}
 
-/**
- * Called on otification settings registration is completed.
- */
-- (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings*)settings
-{
-    if (_command) {
-        [self hasPermission:_command];
-        _command = NULL;
-    }
+    [self fireEvent:event notification:notification data:data];
 }
 
 #pragma mark -
@@ -629,33 +561,41 @@
 - (void) pluginInitialize
 {
     eventQueue = [[NSMutableArray alloc] init];
+    _center    = [UNUserNotificationCenter currentNotificationCenter];
+
+    _center.delegate = self;
+    [_center registerGeneralNotificationCategory];
+
+    [self monitorAppStateChanges];
 }
 
 /**
- * Clears all single repeating notifications which are older then 5 days
- * before the app terminates.
+ * Monitor changes of the app state and update the _isActive flag.
  */
-- (void) onAppTerminate
+- (void) monitorAppStateChanges
 {
-    [self cancelAllNotificationsWhichAreOlderThen:432000];
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+
+    [center addObserverForName:UIApplicationDidBecomeActiveNotification
+                        object:NULL queue:[NSOperationQueue mainQueue]
+                    usingBlock:^(NSNotification *e) { isActive = YES; }];
+
+    [center addObserverForName:UIApplicationDidEnterBackgroundNotification
+                        object:NULL queue:[NSOperationQueue mainQueue]
+                    usingBlock:^(NSNotification *e) { isActive = NO; }];
 }
 
 #pragma mark -
 #pragma mark Helper
 
 /**
- * Retrieves the application state
- *
- * @return
- *      Either "background" or "foreground"
+ * Removes the badge number from the app icon.
  */
-- (NSString*) applicationState
+- (void) clearApplicationIconBadgeNumber
 {
-    UIApplicationState state = [self.app applicationState];
-
-    bool isActive = state == UIApplicationStateActive;
-
-    return isActive ? @"foreground" : @"background";
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
+    });
 }
 
 /**
@@ -671,36 +611,70 @@
 }
 
 /**
- * Short hand for shared application instance.
+ * Fire general event.
+ *
+ * @param [ NSString* ] event The name of the event to fire.
+ *
+ * @return [ Void ]
  */
-- (UIApplication*) app
+- (void) fireEvent:(NSString*)event
 {
-    return [UIApplication sharedApplication];
+    NSMutableDictionary* data = [[NSMutableDictionary alloc] init];
+
+    [self fireEvent:event notification:NULL data:data];
 }
 
 /**
- * Fire general event.
+ * Fire event for about a local notification.
+ *
+ * @param [ NSString* ] event The name of the event to fire.
+ * @param [ APPNotificationRequest* ] notification The local notification.
+ *
+ * @return [ Void ]
  */
 - (void) fireEvent:(NSString*)event
+      notification:(UNNotificationRequest*)notitification
 {
-    [self fireEvent:event notification:NULL];
+    NSMutableDictionary* data = [[NSMutableDictionary alloc] init];
+
+    [self fireEvent:event notification:notitification data:data];
 }
 
 /**
- * Fire event for local notification.
+ * Fire event for about a local notification.
+ *
+ * @param [ NSString* ] event The name of the event to fire.
+ * @param [ APPNotificationRequest* ] notification The local notification.
+ * @param [ NSMutableDictionary* ] data Event object with additional data.
+ *
+ * @return [ Void ]
  */
-- (void) fireEvent:(NSString*)event notification:(UILocalNotification*)notification
+- (void) fireEvent:(NSString*)event
+      notification:(UNNotificationRequest*)request
+              data:(NSMutableDictionary*)data
 {
-    NSString* js;
-    NSString* params = [NSString stringWithFormat:
-                        @"\"%@\"", self.applicationState];
+    NSString *js, *params, *notiAsJSON, *dataAsJSON;
+    NSData* dataAsData;
+
+    [data setObject:event           forKey:@"event"];
+    [data setObject:@(isActive)     forKey:@"foreground"];
+    [data setObject:@(!deviceready) forKey:@"queued"];
+
+    if (request) {
+        notiAsJSON = [request encodeToJSON];
+        [data setObject:request.options.id forKey:@"notification"];
+    }
+
+    dataAsData =
+    [NSJSONSerialization dataWithJSONObject:data options:0 error:NULL];
 
-    if (notification) {
-        NSString* args = [notification encodeToJSON];
+    dataAsJSON =
+    [[NSString alloc] initWithData:dataAsData encoding:NSUTF8StringEncoding];
 
-        params = [NSString stringWithFormat:
-                  @"%@,'%@'",
-                  args, self.applicationState];
+    if (request) {
+        params = [NSString stringWithFormat:@"%@,%@", notiAsJSON, dataAsJSON];
+    } else {
+        params = [NSString stringWithFormat:@"%@", dataAsJSON];
     }
 
     js = [NSString stringWithFormat:

+ 0 - 249
src/ios/APPLocalNotificationOptions.m

@@ -1,249 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-#import "APPLocalNotificationOptions.h"
-
-// Default sound ressource path
-NSString* const DEFAULT_SOUND = @"res://platform_default";
-
-@interface APPLocalNotificationOptions ()
-
-// The dictionary which contains all notification properties
-@property(nonatomic, retain) NSDictionary* dict;
-
-@end
-
-@implementation APPLocalNotificationOptions
-
-@synthesize dict;
-
-#pragma mark -
-#pragma mark Initialization
-
-/**
- * Initialize the object with the given options when calling on JS side:
- * notification.local.add(options)
- */
-- (id) initWithDict:(NSDictionary*)dictionary
-{
-    self = [self init];
-
-    self.dict = dictionary;
-
-    return self;
-}
-
-#pragma mark -
-#pragma mark Attributes
-
-/**
- * The notification's ID.
- */
-- (NSNumber*) id
-{
-    NSInteger id = [[dict objectForKey:@"id"] integerValue];
-
-    return [NSNumber numberWithInteger:id];
-}
-
-/**
- * The notification's title.
- */
-- (NSString*) title
-{
-    return [dict objectForKey:@"title"];
-}
-
-/**
- * The notification's message.
- */
-- (NSString*) text
-{
-    return [dict objectForKey:@"text"];
-}
-
-/**
- * The notification's badge number.
- */
-- (NSInteger) badgeNumber
-{
-    return [[dict objectForKey:@"badge"] intValue];
-}
-
-#pragma mark -
-#pragma mark Complex Attributes
-
-/**
- * The notification's alert body.
- */
-- (NSString*) alertBody
-{
-    NSString* title = [self title];
-    NSString* msg = [self text];
-
-    NSString* alertBody = msg;
-
-    if (![self stringIsNullOrEmpty:title])
-    {
-        alertBody = [NSString stringWithFormat:@"%@\n%@",
-                     title, msg];
-    }
-
-    return alertBody;
-}
-
-/**
- * The notification's sound path.
- */
-- (NSString*) soundName
-{
-    NSString* path = [dict objectForKey:@"sound"];
-
-    if ([self stringIsNullOrEmpty:path])
-        return NULL;
-
-    if ([path isEqualToString:DEFAULT_SOUND])
-        return UILocalNotificationDefaultSoundName;
-
-    if ([path hasPrefix:@"file:/"])
-        return [self soundNameForAsset:path];
-
-    if ([path hasPrefix:@"res:"])
-        return [self soundNameForResource:path];
-
-    return NULL;
-}
-
-/**
- * The notification's fire date.
- */
-- (NSDate*) fireDate
-{
-    double timestamp = [[dict objectForKey:@"at"]
-                        doubleValue];
-
-    return [NSDate dateWithTimeIntervalSince1970:timestamp];
-}
-
-/**
- * The notification's repeat interval.
- */
-- (NSCalendarUnit) repeatInterval
-{
-    NSString* interval = [dict objectForKey:@"every"];
-
-    if ([self stringIsNullOrEmpty:interval]) {
-        return NSCalendarUnitEra;
-    }
-    else if ([interval isEqualToString:@"second"]) {
-        return NSCalendarUnitSecond;
-    }
-    else if ([interval isEqualToString:@"minute"]) {
-        return NSCalendarUnitMinute;
-    }
-    else if ([interval isEqualToString:@"hour"]) {
-        return NSCalendarUnitHour;
-    }
-    else if ([interval isEqualToString:@"day"]) {
-        return NSCalendarUnitDay;
-    }
-    else if ([interval isEqualToString:@"week"]) {
-        return NSCalendarUnitWeekOfYear;
-    }
-    else if ([interval isEqualToString:@"month"]) {
-        return NSCalendarUnitMonth;
-    }
-    else if ([interval isEqualToString:@"quarter"]) {
-        return NSCalendarUnitQuarter;
-    }
-    else if ([interval isEqualToString:@"year"]) {
-        return NSCalendarUnitYear;
-    }
-
-    return NSCalendarUnitEra;
-}
-
-#pragma mark -
-#pragma mark Methods
-
-/**
- * The notification's user info dict.
- */
-- (NSDictionary*) userInfo
-{
-    if ([dict objectForKey:@"updatedAt"]) {
-        NSMutableDictionary* data = [dict mutableCopy];
-
-        [data removeObjectForKey:@"updatedAt"];
-
-        return data;
-    }
-
-    return dict;
-}
-
-/**
- * If it's a repeating notification.
- */
-- (BOOL) isRepeating
-{
-    NSCalendarUnit interval = self.repeatInterval;
-
-    return !(interval == NSCalendarUnitEra || interval == 0);
-}
-
-#pragma mark -
-#pragma mark Helpers
-
-/**
- * Convert relative path to valid sound name attribute.
- */
-- (NSString*) soundNameForAsset:(NSString*)path
-{
-    return [path stringByReplacingOccurrencesOfString:@"file:/"
-                                           withString:@"www"];
-}
-
-/**
- * Convert resource path to valid sound name attribute.
- */
-- (NSString*) soundNameForResource:(NSString*)path
-{
-    return [path pathComponents].lastObject;
-}
-
-/**
- * If the string is empty.
- */
-- (BOOL) stringIsNullOrEmpty:(NSString*)str
-{
-    if (str == (NSString*)[NSNull null])
-        return YES;
-
-    if ([str isEqualToString:@""])
-        return YES;
-
-    return NO;
-}
-
-@end

+ 9 - 18
src/ios/APPLocalNotificationOptions.h → src/ios/APPNotificationContent.h

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,26 +17,17 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
- 
-#import <Foundation/Foundation.h>
-#import <UIKit/UIKit.h>
 
-@interface APPLocalNotificationOptions : NSObject
+#import "APPNotificationOptions.h"
 
-- (id) initWithDict:(NSDictionary*)dict;
+@import UserNotifications;
 
-@property (readonly, getter=id) NSNumber* id;
-@property (readonly, getter=badgeNumber) NSInteger badgeNumber;
-@property (readonly, getter=alertBody) NSString* alertBody;
-@property (readonly, getter=soundName) NSString* soundName;
-@property (readonly, getter=fireDate) NSDate* fireDate;
-@property (readonly, getter=repeatInterval) NSCalendarUnit repeatInterval;
-@property (readonly, getter=userInfo) NSDictionary* userInfo;
+@interface APPNotificationContent : UNMutableNotificationContent
 
-// If it's a repeating notification
-- (BOOL) isRepeating;
+- (id) initWithOptions:(NSDictionary*)dict;
+- (APPNotificationOptions*) options;
+- (UNNotificationRequest*) request;
+- (UNNotificationCategory*) category;
 
 @end

+ 152 - 0
src/ios/APPNotificationContent.m

@@ -0,0 +1,152 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+#import "APPNotificationContent.h"
+#import "APPNotificationOptions.h"
+#import <objc/runtime.h>
+
+@import UserNotifications;
+
+static char optionsKey;
+
+@implementation APPNotificationContent : UNMutableNotificationContent
+
+#pragma mark -
+#pragma mark Init
+
+/**
+ * Initialize a notification with the given options.
+ *
+ * @param [ NSDictionary* ] dict A key-value property map.
+ *
+ * @return [ UNMutableNotificationContent ]
+ */
+- (id) initWithOptions:(NSDictionary*)dict
+{
+    self = [self init];
+
+    [self setUserInfo:dict];
+    [self __init];
+
+    return self;
+}
+
+/**
+ * Initialize a notification by using the options found under userInfo.
+ *
+ * @return [ Void ]
+ */
+- (void) __init
+{
+    APPNotificationOptions* options = self.options;
+
+    self.title              = options.title;
+    self.subtitle           = options.subtitle;
+    self.body               = options.text;
+    self.sound              = options.sound;
+    self.badge              = options.badge;
+    self.attachments        = options.attachments;
+    self.categoryIdentifier = options.categoryId;
+}
+
+#pragma mark -
+#pragma mark Public
+
+/**
+ * The options used to initialize the notification.
+ *
+ * @return [ APPNotificationOptions* ] options
+ */
+- (APPNotificationOptions*) options
+{
+    APPNotificationOptions* options = [self getOptions];
+
+    if (!options) {
+        options = [[APPNotificationOptions alloc]
+                   initWithDict:[self userInfo]];
+
+        [self setOptions:options];
+    }
+
+    return options;
+}
+
+/**
+ * The notifcations request ready to add to the notification center including
+ * all informations about trigger behavior.
+ *
+ * @return [ UNNotificationRequest* ]
+ */
+- (UNNotificationRequest*) request
+{
+    APPNotificationOptions* opts = [self getOptions];
+
+    return [UNNotificationRequest requestWithIdentifier:opts.identifier
+                                                content:self
+                                                trigger:opts.trigger];
+}
+
+/**
+ * The category for the notification with all the actions.
+ *
+ * @return [ UNNotificationCategory* ]
+ */
+- (UNNotificationCategory*) category
+{
+    NSString* categoryId = self.categoryIdentifier;
+    NSArray* actions     = self.options.actions;
+
+    if (!actions.count)
+        return NULL;
+
+    return [UNNotificationCategory categoryWithIdentifier:categoryId
+                                                  actions:actions
+                                        intentIdentifiers:@[]
+                                                  options:UNNotificationCategoryOptionCustomDismissAction];
+}
+
+#pragma mark -
+#pragma mark Private
+
+/**
+ * The options used to initialize the notification.
+ *
+ * @return [ APPNotificationOptions* ]
+ */
+- (APPNotificationOptions*) getOptions
+{
+    return objc_getAssociatedObject(self, &optionsKey);
+}
+
+/**
+ * Set the options used to initialize the notification.
+ *
+ * @param [ NSDictionary* ] dict A key-value property map.
+ *
+ * @return [ Void ]
+ */
+- (void) setOptions:(APPNotificationOptions*)options
+{
+    objc_setAssociatedObject(self, &optionsKey,
+                             options, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+@end

+ 42 - 0
src/ios/APPNotificationOptions.h

@@ -0,0 +1,42 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+@import UserNotifications;
+
+@interface APPNotificationOptions : NSObject
+
+@property (readonly, getter=id)          NSNumber*            id;
+@property (readonly, getter=identifier)  NSString*            identifier;
+@property (readonly, getter=categoryId)  NSString*            categoryId;
+@property (readonly, getter=title)       NSString*            title;
+@property (readonly, getter=subtitle)    NSString*            subtitle;
+@property (readonly, getter=badge)       NSNumber*            badge;
+@property (readonly, getter=text)        NSString*            text;
+@property (readonly, getter=silent)      BOOL                 silent;
+@property (readonly, getter=sound)       UNNotificationSound* sound;
+@property (readonly, getter=userInfo)    NSDictionary*        userInfo;
+@property (readonly, getter=actions)     NSArray<UNNotificationAction *> * actions;
+@property (readonly, getter=attachments) NSArray<UNNotificationAttachment *> * attachments;
+
+- (id) initWithDict:(NSDictionary*)dict;
+- (UNNotificationTrigger*) trigger;
+
+@end

+ 821 - 0
src/ios/APPNotificationOptions.m

@@ -0,0 +1,821 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+#import "APPNotificationOptions.h"
+#import "UNUserNotificationCenter+APPLocalNotification.h"
+
+@import CoreLocation;
+@import UserNotifications;
+
+@interface APPNotificationOptions ()
+
+// The dictionary which contains all notification properties
+@property(nonatomic, retain) NSDictionary* dict;
+
+@end
+
+@implementation APPNotificationOptions : NSObject
+
+@synthesize dict;
+
+#pragma mark -
+#pragma mark Initialization
+
+/**
+ * Initialize by using the given property values.
+ *
+ * @param [ NSDictionary* ] dict A key-value property map.
+ *
+ * @return [ APPNotificationOptions ]
+ */
+- (id) initWithDict:(NSDictionary*)dictionary
+{
+    self = [self init];
+
+    self.dict = dictionary;
+
+    [self actions];
+
+    return self;
+}
+
+#pragma mark -
+#pragma mark Properties
+
+/**
+ * The ID for the notification.
+ *
+ * @return [ NSNumber* ]
+ */
+- (NSNumber*) id
+{
+    NSInteger id = [[dict objectForKey:@"id"] integerValue];
+
+    return [NSNumber numberWithInteger:id];
+}
+
+/**
+ * The ID for the notification.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) identifier
+{
+    return [NSString stringWithFormat:@"%@", self.id];
+}
+
+/**
+ * The title for the notification.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) title
+{
+    return [dict objectForKey:@"title"];
+}
+
+/**
+ * The subtitle for the notification.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) subtitle
+{
+    NSArray *parts = [self.title componentsSeparatedByString:@"\n"];
+
+    return parts.count < 2 ? @"" : [parts objectAtIndex:1];
+}
+
+/**
+ * The text for the notification.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) text
+{
+    return [dict objectForKey:@"text"];
+}
+
+/**
+ * Show notification in foreground.
+ *
+ * @return [ BOOL ]
+ */
+- (BOOL) silent
+{
+    return [[dict objectForKey:@"silent"] boolValue];
+}
+
+/**
+ * The badge number for the notification.
+ *
+ * @return [ NSNumber* ]
+ */
+- (NSNumber*) badge
+{
+    id value = [dict objectForKey:@"badge"];
+
+    return (value == NULL) ? NULL : [NSNumber numberWithInt:[value intValue]];
+}
+
+/**
+ * The category of the notification.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) categoryId
+{
+    NSString* value = [dict objectForKey:@"actionGroupId"];
+
+    return value.length ? value : kAPPGeneralCategory;
+}
+
+/**
+ * The sound file for the notification.
+ *
+ * @return [ UNNotificationSound* ]
+ */
+- (UNNotificationSound*) sound
+{
+    NSString* path = [dict objectForKey:@"sound"];
+    NSString* file;
+
+    if ([path isKindOfClass:NSNumber.class]) {
+        return [path boolValue] ? [UNNotificationSound defaultSound] : NULL;
+    }
+
+    if (!path.length)
+        return NULL;
+
+    if ([path hasPrefix:@"file:/"]) {
+        file = [self soundNameForAsset:path];
+    } else
+    if ([path hasPrefix:@"res:"]) {
+        file = [self soundNameForResource:path];
+    }
+
+    return [UNNotificationSound soundNamed:file];
+}
+
+
+/**
+ * Additional content to attach.
+ *
+ * @return [ UNNotificationSound* ]
+ */
+- (NSArray<UNNotificationAttachment *> *) attachments
+{
+    NSArray* paths              = [dict objectForKey:@"attachments"];
+    NSMutableArray* attachments = [[NSMutableArray alloc] init];
+
+    if (!paths)
+        return attachments;
+
+    for (NSString* path in paths) {
+        NSURL* url = [self urlForAttachmentPath:path];
+
+        UNNotificationAttachment* attachment;
+        attachment = [UNNotificationAttachment attachmentWithIdentifier:path
+                                                                    URL:url
+                                                                options:NULL
+                                                                  error:NULL];
+
+        if (attachment) {
+            [attachments addObject:attachment];
+        }
+    }
+
+    return attachments;
+}
+
+/**
+ * Additional actions for the notification.
+ *
+ * @return [ NSArray* ]
+ */
+- (NSArray<UNNotificationAction *> *) actions
+{
+    NSArray* items          = [dict objectForKey:@"actions"];
+    NSMutableArray* actions = [[NSMutableArray alloc] init];
+
+    if (!items)
+        return actions;
+
+    for (NSDictionary* item in items) {
+        NSString* id    = [item objectForKey:@"id"];
+        NSString* title = [item objectForKey:@"title"];
+        NSString* type  = [item objectForKey:@"type"];
+
+        UNNotificationActionOptions options = UNNotificationActionOptionNone;
+        UNNotificationAction* action;
+
+        if ([[item objectForKey:@"launch"] boolValue]) {
+            options = UNNotificationActionOptionForeground;
+        }
+
+        if ([[item objectForKey:@"ui"] isEqualToString:@"decline"]) {
+            options = options | UNNotificationActionOptionDestructive;
+        }
+
+        if ([[item objectForKey:@"needsAuth"] boolValue]) {
+            options = options | UNNotificationActionOptionAuthenticationRequired;
+        }
+
+        if ([type isEqualToString:@"input"]) {
+            NSString* submitTitle = [item objectForKey:@"submitTitle"];
+            NSString* placeholder = [item objectForKey:@"emptyText"];
+
+            if (!submitTitle.length) {
+                submitTitle = @"Submit";
+            }
+
+            action = [UNTextInputNotificationAction actionWithIdentifier:id
+                                                                   title:title
+                                                                 options:options
+                                                    textInputButtonTitle:submitTitle
+                                                    textInputPlaceholder:placeholder];
+        } else
+        if (!type.length || [type isEqualToString:@"button"]) {
+            action = [UNNotificationAction actionWithIdentifier:id
+                                                          title:title
+                                                        options:options];
+        } else {
+            NSLog(@"Unknown action type: %@", type);
+        }
+
+        if (action) {
+            [actions addObject:action];
+        }
+    }
+
+    return actions;
+}
+
+#pragma mark -
+#pragma mark Public
+
+/**
+ * Specify how and when to trigger the notification.
+ *
+ * @return [ UNNotificationTrigger* ]
+ */
+- (UNNotificationTrigger*) trigger
+{
+    NSString* type = [self valueForTriggerOption:@"type"];
+
+    if ([type isEqualToString:@"location"])
+        return [self triggerWithRegion];
+
+    if (![type isEqualToString:@"calendar"])
+        NSLog(@"Unknown type: %@", type);
+
+    if ([self isRepeating])
+        return [self repeatingTrigger];
+
+    return [self nonRepeatingTrigger];
+}
+
+/**
+ * The notification's user info dict.
+ *
+ * @return [ NSDictionary* ]
+ */
+- (NSDictionary*) userInfo
+{
+    if ([dict objectForKey:@"updatedAt"]) {
+        NSMutableDictionary* data = [dict mutableCopy];
+
+        [data removeObjectForKey:@"updatedAt"];
+
+        return data;
+    }
+
+    return dict;
+}
+
+#pragma mark -
+#pragma mark Private
+
+- (id) valueForTriggerOption:(NSString*)key
+{
+    return [[dict objectForKey:@"trigger"] objectForKey:key];
+}
+
+/**
+ * The date when to fire the notification.
+ *
+ * @return [ NSDate* ]
+ */
+- (NSDate*) triggerDate
+{
+    double timestamp = [[self valueForTriggerOption:@"at"] doubleValue];
+
+    return [NSDate dateWithTimeIntervalSince1970:timestamp];
+}
+
+/**
+ * If the notification shall be repeating.
+ *
+ * @return [ BOOL ]
+ */
+- (BOOL) isRepeating
+{
+    id every = [self valueForTriggerOption:@"every"];
+
+    if ([every isKindOfClass:NSString.class])
+        return ((NSString*) every).length > 0;
+
+    if ([every isKindOfClass:NSDictionary.class])
+        return ((NSDictionary*) every).count > 0;
+
+    return every > 0;
+}
+
+/**
+ * Non repeating trigger.
+ *
+ * @return [ UNTimeIntervalNotificationTrigger* ]
+ */
+- (UNNotificationTrigger*) nonRepeatingTrigger
+{
+    id timestamp = [self valueForTriggerOption:@"at"];
+
+    if (timestamp) {
+        return [self triggerWithDateMatchingComponents:NO];
+    }
+
+    return [UNTimeIntervalNotificationTrigger
+            triggerWithTimeInterval:[self timeInterval] repeats:NO];
+}
+
+/**
+ * Repeating trigger.
+ *
+ * @return [ UNNotificationTrigger* ]
+ */
+- (UNNotificationTrigger*) repeatingTrigger
+{
+    id every = [self valueForTriggerOption:@"every"];
+
+    if ([every isKindOfClass:NSString.class])
+        return [self triggerWithDateMatchingComponents:YES];
+
+    if ([every isKindOfClass:NSDictionary.class])
+        return [self triggerWithCustomDateMatchingComponents];
+
+    return [self triggerWithTimeInterval];
+}
+
+/**
+ * A trigger based on a calendar time defined by the user.
+ *
+ * @return [ UNTimeIntervalNotificationTrigger* ]
+ */
+- (UNTimeIntervalNotificationTrigger*) triggerWithTimeInterval
+{
+    double ticks   = [[self valueForTriggerOption:@"every"] doubleValue];
+    NSString* unit = [self valueForTriggerOption:@"unit"];
+    double seconds = [self convertTicksToSeconds:ticks unit:unit];
+
+    if (seconds < 60) {
+        NSLog(@"time interval must be at least 60 sec if repeating");
+        seconds = 60;
+    }
+
+    return [UNTimeIntervalNotificationTrigger
+            triggerWithTimeInterval:seconds repeats:YES];
+}
+
+/**
+ * A repeating trigger based on a calendar time intervals defined by the plugin.
+ *
+ * @return [ UNCalendarNotificationTrigger* ]
+ */
+- (UNCalendarNotificationTrigger*) triggerWithDateMatchingComponents:(BOOL)repeats
+{
+    NSCalendar* cal = [[NSCalendar alloc]
+                       initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
+
+    NSDateComponents *date = [cal components:[self repeatInterval]
+                                    fromDate:[self triggerDate]];
+
+    date.timeZone = [NSTimeZone defaultTimeZone];
+
+    return [UNCalendarNotificationTrigger
+            triggerWithDateMatchingComponents:date repeats:repeats];
+}
+
+/**
+ * A repeating trigger based on a calendar time intervals defined by the user.
+ *
+ * @return [ UNCalendarNotificationTrigger* ]
+ */
+- (UNCalendarNotificationTrigger*) triggerWithCustomDateMatchingComponents
+{
+    NSCalendar* cal = [[NSCalendar alloc]
+                       initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
+
+    NSDateComponents *date = [self customDateComponents];
+
+    date.calendar = cal;
+    date.timeZone = [NSTimeZone defaultTimeZone];
+
+    return [UNCalendarNotificationTrigger
+            triggerWithDateMatchingComponents:date repeats:YES];
+}
+
+/**
+ * A repeating trigger based on a location region.
+ *
+ * @return [ UNLocationNotificationTrigger* ]
+ */
+- (UNLocationNotificationTrigger*) triggerWithRegion
+{
+    NSArray* center = [self valueForTriggerOption:@"center"];
+    double radius   = [[self valueForTriggerOption:@"radius"] doubleValue];
+
+    CLLocationCoordinate2D coord =
+    CLLocationCoordinate2DMake([center[0] doubleValue], [center[1] doubleValue]);
+
+    CLCircularRegion* region =
+    [[CLCircularRegion alloc] initWithCenter:coord
+                                      radius:radius
+                                  identifier:self.identifier];
+
+    region.notifyOnEntry = [[self valueForTriggerOption:@"notifyOnEntry"] boolValue];
+    region.notifyOnExit  = [[self valueForTriggerOption:@"notifyOnExit"] boolValue];
+
+    return [UNLocationNotificationTrigger triggerWithRegion:region
+                                                    repeats:YES];
+}
+
+/**
+ * The time interval between the next fire date and now.
+ *
+ * @return [ double ]
+ */
+- (double) timeInterval
+{
+    double ticks   = [[self valueForTriggerOption:@"in"] doubleValue];
+    NSString* unit = [self valueForTriggerOption:@"unit"];
+    double seconds = [self convertTicksToSeconds:ticks unit:unit];
+
+    return MAX(0.01f, seconds);
+}
+
+/**
+ * The repeat interval for the notification.
+ *
+ * @return [ NSCalendarUnit ]
+ */
+- (NSCalendarUnit) repeatInterval
+{
+    NSString* interval = [self valueForTriggerOption:@"every"];
+    NSCalendarUnit units = NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond;
+
+    if ([interval isEqualToString:@"minute"])
+        return NSCalendarUnitSecond;
+
+    if ([interval isEqualToString:@"hour"])
+        return NSCalendarUnitMinute|NSCalendarUnitSecond;
+
+    if ([interval isEqualToString:@"day"])
+        return NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond;
+
+    if ([interval isEqualToString:@"week"])
+        return NSCalendarUnitWeekday|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond;
+
+    if ([interval isEqualToString:@"month"])
+        return NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond;
+
+    if ([interval isEqualToString:@"year"])
+        return NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond;
+
+    return units;
+}
+
+/**
+ * The repeat interval for the notification.
+ *
+ * @return [ NSDateComponents* ]
+ */
+- (NSDateComponents*) customDateComponents
+{
+    NSDateComponents* date  = [[NSDateComponents alloc] init];
+    NSDictionary* every     = [self valueForTriggerOption:@"every"];
+
+    date.second = 0;
+
+    for (NSString* key in every) {
+        long value = [[every valueForKey:key] longValue];
+
+        if ([key isEqualToString:@"minute"]) {
+            date.minute = value;
+        } else
+        if ([key isEqualToString:@"hour"]) {
+            date.hour = value;
+        } else
+        if ([key isEqualToString:@"day"]) {
+            date.day = value;
+        } else
+        if ([key isEqualToString:@"weekday"]) {
+            date.weekday = value;
+        } else
+        if ([key isEqualToString:@"weekdayOrdinal"]) {
+            date.weekdayOrdinal = value;
+        } else
+        if ([key isEqualToString:@"week"]) {
+            date.weekOfYear = value;
+        } else
+        if ([key isEqualToString:@"weekOfMonth"]) {
+            date.weekOfMonth = value;
+        } else
+        if ([key isEqualToString:@"month"]) {
+            date.month = value;
+        } else
+        if ([key isEqualToString:@"quarter"]) {
+            date.quarter = value;
+        } else
+        if ([key isEqualToString:@"year"]) {
+            date.year = value;
+        }
+    }
+
+    return date;
+}
+
+/**
+ * Convert an assets path to an valid sound name attribute.
+ *
+ * @param [ NSString* ] path A relative assets file path.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) soundNameForAsset:(NSString*)path
+{
+    return [path stringByReplacingOccurrencesOfString:@"file:/"
+                                           withString:@"www"];
+}
+
+/**
+ * Convert a ressource path to an valid sound name attribute.
+ *
+ * @param [ NSString* ] path A relative ressource file path.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) soundNameForResource:(NSString*)path
+{
+    return [path pathComponents].lastObject;
+}
+
+/**
+ * URL for the specified attachment path.
+ *
+ * @param [ NSString* ] path Absolute/relative path or a base64 data.
+ *
+ * @return [ NSURL* ]
+ */
+- (NSURL*) urlForAttachmentPath:(NSString*)path
+{
+    if ([path hasPrefix:@"file:///"])
+    {
+        return [self urlForFile:path];
+    }
+    else if ([path hasPrefix:@"res:"])
+    {
+        return [self urlForResource:path];
+    }
+    else if ([path hasPrefix:@"file://"])
+    {
+        return [self urlForAsset:path];
+    }
+    else if ([path hasPrefix:@"base64:"])
+    {
+        return [self urlFromBase64:path];
+    }
+
+    NSFileManager* fm = [NSFileManager defaultManager];
+
+    if (![fm fileExistsAtPath:path]){
+        NSLog(@"File not found: %@", path);
+    }
+
+    return [NSURL fileURLWithPath:path];
+}
+
+/**
+ * URL to an absolute file path.
+ *
+ * @param [ NSString* ] path An absolute file path.
+ *
+ * @return [ NSURL* ]
+ */
+- (NSURL*) urlForFile:(NSString*)path
+{
+    NSFileManager* fm = [NSFileManager defaultManager];
+
+    NSString* absPath;
+    absPath = [path stringByReplacingOccurrencesOfString:@"file://"
+                                              withString:@""];
+
+    if (![fm fileExistsAtPath:absPath]) {
+        NSLog(@"File not found: %@", absPath);
+    }
+
+    return [NSURL fileURLWithPath:absPath];
+}
+
+/**
+ * URL to a resource file.
+ *
+ * @param [ NSString* ] path A relative file path.
+ *
+ * @return [ NSURL* ]
+ */
+- (NSURL*) urlForResource:(NSString*)path
+{
+    NSFileManager* fm    = [NSFileManager defaultManager];
+    NSBundle* mainBundle = [NSBundle mainBundle];
+    NSString* bundlePath = [mainBundle resourcePath];
+
+    if ([path isEqualToString:@"res://icon"]) {
+        path = @"res://AppIcon60x60@3x.png";
+    }
+
+    NSString* absPath;
+    absPath = [path stringByReplacingOccurrencesOfString:@"res:/"
+                                              withString:@""];
+
+    absPath = [bundlePath stringByAppendingString:absPath];
+
+    if (![fm fileExistsAtPath:absPath]) {
+        NSLog(@"File not found: %@", absPath);
+    }
+
+    return [NSURL fileURLWithPath:absPath];
+}
+
+/**
+ * URL to an asset file.
+ *
+ * @param path A relative www file path.
+ *
+ * @return [ NSURL* ]
+ */
+- (NSURL*) urlForAsset:(NSString*)path
+{
+    NSFileManager* fm    = [NSFileManager defaultManager];
+    NSBundle* mainBundle = [NSBundle mainBundle];
+    NSString* bundlePath = [mainBundle bundlePath];
+
+    NSString* absPath;
+    absPath = [path stringByReplacingOccurrencesOfString:@"file:/"
+                                              withString:@"/www"];
+
+    absPath = [bundlePath stringByAppendingString:absPath];
+
+    if (![fm fileExistsAtPath:absPath]) {
+        NSLog(@"File not found: %@", absPath);
+    }
+
+    return [NSURL fileURLWithPath:absPath];
+}
+
+/**
+ * URL for a base64 encoded string.
+ *
+ * @param [ NSString* ] base64String Base64 encoded string.
+ *
+ * @return [ NSURL* ]
+ */
+- (NSURL*) urlFromBase64:(NSString*)base64String
+{
+    NSString *filename = [self basenameFromAttachmentPath:base64String];
+    NSUInteger length = [base64String length];
+    NSRegularExpression *regex;
+    NSString *dataString;
+
+    regex = [NSRegularExpression regularExpressionWithPattern:@"^base64:[^/]+.."
+                                                      options:NSRegularExpressionCaseInsensitive
+                                                        error:Nil];
+
+    dataString = [regex stringByReplacingMatchesInString:base64String
+                                                 options:0
+                                                   range:NSMakeRange(0, length)
+                                            withTemplate:@""];
+
+    NSData* data = [[NSData alloc] initWithBase64EncodedString:dataString
+                                                       options:0];
+
+
+    return [self urlForData:data withFileName:filename];
+}
+
+/**
+ * Extract the attachments basename.
+ *
+ * @param [ NSString* ] path The file path or base64 data.
+ *
+ * @return [ NSString* ]
+ */
+- (NSString*) basenameFromAttachmentPath:(NSString*)path
+{
+    if ([path hasPrefix:@"base64:"]) {
+        NSString* pathWithoutPrefix;
+        pathWithoutPrefix = [path stringByReplacingOccurrencesOfString:@"base64:"
+                                                            withString:@""];
+
+        return [pathWithoutPrefix substringToIndex:
+                [pathWithoutPrefix rangeOfString:@"//"].location];
+    }
+
+    return path;
+}
+
+/**
+ * Write the data into a temp file.
+ *
+ * @param [ NSData* ]   data The data to save to file.
+ * @param [ NSString* ] name The name of the file.
+ *
+ * @return [ NSURL* ]
+ */
+- (NSURL*) urlForData:(NSData*)data withFileName:(NSString*) filename
+{
+    NSFileManager* fm = [NSFileManager defaultManager];
+    NSString* tempDir = NSTemporaryDirectory();
+
+    [fm createDirectoryAtPath:tempDir withIntermediateDirectories:YES
+                   attributes:NULL
+                        error:NULL];
+
+    NSString* absPath = [tempDir stringByAppendingPathComponent:filename];
+
+    NSURL* url = [NSURL fileURLWithPath:absPath];
+    [data writeToURL:url atomically:NO];
+
+    if (![fm fileExistsAtPath:absPath]) {
+        NSLog(@"File not found: %@", absPath);
+    }
+
+    return url;
+}
+
+/**
+ * Convert the amount of ticks into seconds.
+ *
+ * @param [ double ]    ticks The amount of ticks.
+ * @param [ NSString* ] unit  The unit of the ticks (minute, hour, day, ...)
+ *
+ * @return [ double ] Amount of ticks in seconds.
+ */
+- (double) convertTicksToSeconds:(double)ticks unit:(NSString*)unit
+{
+    if ([unit isEqualToString:@"second"]) {
+        return ticks;
+    } else
+    if ([unit isEqualToString:@"minute"]) {
+        return ticks * 60;
+    } else
+    if ([unit isEqualToString:@"hour"]) {
+        return ticks * 60 * 60;
+    } else
+    if ([unit isEqualToString:@"day"]) {
+        return ticks * 60 * 60 * 24;
+    } else
+    if ([unit isEqualToString:@"week"]) {
+        return ticks * 60 * 60 * 24 * 7;
+    } else
+    if ([unit isEqualToString:@"month"]) {
+        return ticks * 60 * 60 * 24 * 30.438;
+    } else
+    if ([unit isEqualToString:@"quarter"]) {
+        return ticks * 60 * 60 * 24 * 91.313;
+    } else
+    if ([unit isEqualToString:@"year"]) {
+        return ticks * 60 * 60 * 24 * 365;
+    }
+
+    return 0;
+}
+
+@end

+ 0 - 63
src/ios/UIApplication+APPLocalNotification.h

@@ -1,63 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-#import "UILocalNotification+APPLocalNotification.h"
-
-@interface UIApplication (APPLocalNotification)
-
-@property (readonly, getter=localNotifications) NSArray* localNotifications;
-@property (readonly, getter=localNotificationIds) NSArray* localNotificationIds;
-
-// If the app has the permission to schedule local notifications
-- (BOOL) hasPermissionToScheduleLocalNotifications;
-// Ask for permission to schedule local notifications
-- (void) registerPermissionToScheduleLocalNotifications;
-
-// List of all local notification IDs from given type
-- (NSArray*) localNotificationIdsByType:(APPLocalNotificationType)type;
-
-// If local notification with ID exists
-- (BOOL) localNotificationExist:(NSNumber*)id;
-// If local notification with ID and type exists
-- (BOOL) localNotificationExist:(NSNumber*)id type:(APPLocalNotificationType)type;
-
-// Local notification by ID
-- (UILocalNotification*) localNotificationWithId:(NSNumber*)id;
-// Local notification by ID and type
-- (UILocalNotification*) localNotificationWithId:(NSNumber*)id andType:(APPLocalNotificationType)type;
-
-// Property list from all local notifications
-- (NSArray*) localNotificationOptions;
-// Property list from given local notifications
-- (NSArray*) localNotificationOptionsById:(NSArray*)ids;
-// Property list from all local notifications with type constraint
-- (NSArray*) localNotificationOptionsByType:(APPLocalNotificationType)type;
-// Property list from given local notifications with type constraint
-- (NSArray*) localNotificationOptionsByType:(APPLocalNotificationType)type andId:(NSArray*)ids;
-
-// Clear single local notfications
-- (void) clearLocalNotification:(UILocalNotification*)notification;
-// Clear all local notfications
-- (void) clearAllLocalNotifications;
-
-@end

+ 0 - 333
src/ios/UIApplication+APPLocalNotification.m

@@ -1,333 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-#import "UIApplication+APPLocalNotification.h"
-#import "UILocalNotification+APPLocalNotification.h"
-
-@implementation UIApplication (APPLocalNotification)
-
-#pragma mark -
-#pragma mark Permissions
-
-/**
- * If the app has the permission to schedule local notifications.
- */
-- (BOOL) hasPermissionToScheduleLocalNotifications
-{
-    if ([[UIApplication sharedApplication]
-         respondsToSelector:@selector(registerUserNotificationSettings:)])
-    {
-        UIUserNotificationType types;
-        UIUserNotificationSettings *settings;
-
-        settings = [[UIApplication sharedApplication]
-                    currentUserNotificationSettings];
-
-        types = UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound;
-
-        return (settings.types & types);
-    } else {
-        return YES;
-    }
-}
-
-/**
- * Ask for permission to schedule local notifications.
- */
-- (void) registerPermissionToScheduleLocalNotifications
-{
-    if ([[UIApplication sharedApplication]
-         respondsToSelector:@selector(registerUserNotificationSettings:)])
-    {
-        UIUserNotificationType types;
-        UIUserNotificationSettings *settings;
-
-        settings = [[UIApplication sharedApplication]
-                    currentUserNotificationSettings];
-
-        types = settings.types|UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound;
-
-        settings = [UIUserNotificationSettings settingsForTypes:types
-                                                     categories:nil];
-
-        [[UIApplication sharedApplication]
-         registerUserNotificationSettings:settings];
-    }
-}
-
-#pragma mark -
-#pragma mark LocalNotifications
-
-/**
- * List of all local notifications which have been added
- * but not yet removed from the notification center.
- */
-- (NSArray*) localNotifications
-{
-    NSArray* scheduledNotifications = self.scheduledLocalNotifications;
-    NSMutableArray* notifications = [[NSMutableArray alloc] init];
-
-    for (UILocalNotification* notification in scheduledNotifications)
-    {
-        if (notification) {
-            [notifications addObject:notification];
-        }
-    }
-
-    return notifications;
-}
-
-/**
- * List of all triggered local notifications which have been scheduled
- * and not yet removed the notification center.
- */
-- (NSArray*) triggeredLocalNotifications
-{
-    NSArray* notifications = self.localNotifications;
-    NSMutableArray* triggeredNotifications = [[NSMutableArray alloc] init];
-
-    for (UILocalNotification* notification in notifications)
-    {
-        if ([notification isTriggered]) {
-            [triggeredNotifications addObject:notification];
-        }
-    }
-
-    return triggeredNotifications;
-}
-
-/**
- * List of all local notifications IDs.
- */
-- (NSArray*) localNotificationIds
-{
-    NSArray* notifications = self.localNotifications;
-    NSMutableArray* ids = [[NSMutableArray alloc] init];
-
-    for (UILocalNotification* notification in notifications)
-    {
-        [ids addObject:notification.options.id];
-    }
-
-    return ids;
-}
-
-/**
- * List of all local notifications IDs from given type.
- *
- * @param type
- *      Notification life cycle type
- */
-- (NSArray*) localNotificationIdsByType:(APPLocalNotificationType)type
-{
-    NSArray* notifications = self.localNotifications;
-    NSMutableArray* ids = [[NSMutableArray alloc] init];
-
-    for (UILocalNotification* notification in notifications)
-    {
-        if (notification.type == type) {
-            [ids addObject:notification.options.id];
-        }
-    }
-
-    return ids;
-}
-
-/*
- * If local notification with ID exists.
- *
- * @param id
- *      Notification ID
- */
-- (BOOL) localNotificationExist:(NSNumber*)id
-{
-    return [self localNotificationWithId:id] != NULL;
-}
-
-/* If local notification with ID and type exists
- *
- * @param id
- *      Notification ID
- * @param type
- *      Notification life cycle type
- */
-- (BOOL) localNotificationExist:(NSNumber*)id type:(APPLocalNotificationType)type
-{
-    return [self localNotificationWithId:id andType:type] != NULL;
-}
-
-/**
- * Get local notification with ID.
- *
- * @param id
- *      Notification ID
- */
-- (UILocalNotification*) localNotificationWithId:(NSNumber*)id
-{
-    NSArray* notifications = self.localNotifications;
-
-    for (UILocalNotification* notification in notifications)
-    {
-        NSString* fid = [NSString stringWithFormat:@"%@", notification.options.id];
-        
-        if ([fid isEqualToString:[id stringValue]]) {
-            return notification;
-        }
-    }
-
-    return NULL;
-}
-
-/*
- * Get local notification with ID and type.
- *
- * @param id
- *      Notification ID
- * @param type
- *      Notification life cycle type
- */
-- (UILocalNotification*) localNotificationWithId:(NSNumber*)id andType:(APPLocalNotificationType)type
-{
-    UILocalNotification* notification = [self localNotificationWithId:id];
-
-    if (notification && notification.type == type)
-        return notification;
-
-    return NULL;
-}
-
-/**
- * List of properties from all notifications.
- */
-- (NSArray*) localNotificationOptions
-{
-    NSArray* notifications = self.localNotifications;
-    NSMutableArray* options = [[NSMutableArray alloc] init];
-
-    for (UILocalNotification* notification in notifications)
-    {
-        [options addObject:notification.options.userInfo];
-    }
-
-    return options;
-}
-
-/**
- * List of properties from all local notifications from given type.
- *
- * @param type
- *      Notification life cycle type
- */
-- (NSArray*) localNotificationOptionsByType:(APPLocalNotificationType)type
-{
-    NSArray* notifications = self.localNotifications;
-    NSMutableArray* options = [[NSMutableArray alloc] init];
-
-    for (UILocalNotification* notification in notifications)
-    {
-        if (notification.type == type) {
-            [options addObject:notification.options.userInfo];
-        }
-    }
-
-    return options;
-}
-
-/**
- * List of properties from given local notifications.
- *
- * @param ids
- *      Notification IDs
- */
-- (NSArray*) localNotificationOptionsById:(NSArray*)ids
-{
-    UILocalNotification* notification;
-    NSMutableArray* options = [[NSMutableArray alloc] init];
-
-    for (NSNumber* id in ids)
-    {
-        notification = [self localNotificationWithId:id];
-
-        if (notification) {
-            [options addObject:notification.options.userInfo];
-        }
-    }
-
-    return options;
-}
-
-/**
- * List of properties from given local notifications.
- *
- * @param type
- *      Notification life cycle type
- * @param ids
- *      Notification IDs
- */
-- (NSArray*) localNotificationOptionsByType:(APPLocalNotificationType)type andId:(NSArray*)ids
-{
-    UILocalNotification* notification;
-    NSMutableArray* options = [[NSMutableArray alloc] init];
-
-    for (NSNumber* id in ids)
-    {
-        notification = [self localNotificationWithId:id];
-
-        if (notification && notification.type == type) {
-            [options addObject:notification.options.userInfo];
-        }
-    }
-
-    return options;
-}
-
-/*
- * Clear all local notfications.
- */
-- (void) clearAllLocalNotifications
-{
-    NSArray* notifications = self.triggeredLocalNotifications;
-
-    for (UILocalNotification* notification in notifications) {
-        [self clearLocalNotification:notification];
-    }
-}
-
-/*
- * Clear single local notfication.
- *
- * @param notification
- *      The local notification object
- */
-- (void) clearLocalNotification:(UILocalNotification*)notification
-{
-    [self cancelLocalNotification:notification];
-
-    if ([notification isRepeating]) {
-        notification.fireDate = notification.options.fireDate;
-
-        [self scheduleLocalNotification:notification];
-    };
-}
-
-@end

+ 0 - 247
src/ios/UILocalNotification+APPLocalNotification.m

@@ -1,247 +0,0 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-#import "UILocalNotification+APPLocalNotification.h"
-#import "APPLocalNotificationOptions.h"
-#import <objc/runtime.h>
-
-static char optionsKey;
-
-NSInteger const APPLocalNotificationTypeScheduled = 1;
-NSInteger const APPLocalNotificationTypeTriggered = 2;
-
-@implementation UILocalNotification (APPLocalNotification)
-
-#pragma mark -
-#pragma mark Init
-
-/**
- * Initialize a local notification with the given options when calling on JS side:
- * notification.local.add(options)
- */
-- (id) initWithOptions:(NSDictionary*)dict
-{
-    self = [self init];
-
-    [self setUserInfo:dict];
-    [self __init];
-
-    return self;
-}
-
-/**
- * Applies the given options when calling on JS side:
- * notification.local.add(options)
-
- */
-- (void) __init
-{
-    APPLocalNotificationOptions* options = self.options;
-
-    self.fireDate = options.fireDate;
-    self.timeZone = [NSTimeZone defaultTimeZone];
-    self.applicationIconBadgeNumber = options.badgeNumber;
-    self.repeatInterval = options.repeatInterval;
-    self.alertBody = options.alertBody;
-    self.soundName = options.soundName;
-
-    if ([self wasInThePast]) {
-        self.fireDate = [NSDate date];
-    }
-}
-
-#pragma mark -
-#pragma mark Methods
-
-/**
- * The options provided by the plug-in.
- */
-- (APPLocalNotificationOptions*) options
-{
-    APPLocalNotificationOptions* options = [self getOptions];
-
-    if (!options) {
-        options = [[APPLocalNotificationOptions alloc]
-                   initWithDict:[self userInfo]];
-
-        [self setOptions:options];
-    }
-
-    return options;
-}
-
-/**
- * Get associated option object
- */
-- (APPLocalNotificationOptions*) getOptions
-{
-    return objc_getAssociatedObject(self, &optionsKey);
-}
-
-/**
- * Set associated option object
- */
-- (void) setOptions:(APPLocalNotificationOptions*)options
-{
-    objc_setAssociatedObject(self, &optionsKey,
-                             options, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-}
-
-/**
- * The repeating interval in seconds.
- */
-- (int) repeatIntervalInSeconds
-{
-    switch (self.repeatInterval) {
-        case NSCalendarUnitMinute:
-            return 60;
-
-        case NSCalendarUnitHour:
-            return 60000;
-
-        case NSCalendarUnitDay:
-        case NSCalendarUnitWeekOfYear:
-        case NSCalendarUnitMonth:
-        case NSCalendarUnitYear:
-            return 86400;
-
-        default:
-            return 1;
-    }
-}
-
-/**
- * Timeinterval since fire date.
- */
-- (double) timeIntervalSinceFireDate
-{
-    NSDate* now      = [NSDate date];
-    NSDate* fireDate = self.fireDate;
-
-    int timespan = [now timeIntervalSinceDate:fireDate];
-
-    return timespan;
-}
-
-/**
- * Timeinterval since last trigger date.
- */
-- (double) timeIntervalSinceLastTrigger
-{
-    int timespan = [self timeIntervalSinceFireDate];
-
-    if ([self isRepeating]) {
-        timespan = timespan % [self repeatIntervalInSeconds];
-    }
-
-    return timespan;
-}
-
-/**
- * Encode the user info dict to JSON.
- */
-- (NSString*) encodeToJSON
-{
-    NSString* json;
-    NSData* data;
-    NSMutableDictionary* obj = [self.userInfo mutableCopy];
-
-    [obj removeObjectForKey:@"updatedAt"];
-
-    if (obj == NULL || obj.count == 0)
-        return json;
-
-    data = [NSJSONSerialization dataWithJSONObject:obj
-                                           options:NSJSONWritingPrettyPrinted
-                                             error:NULL];
-
-    json = [[NSString alloc] initWithData:data
-                                 encoding:NSUTF8StringEncoding];
-
-    return [json stringByReplacingOccurrencesOfString:@"\n"
-                                           withString:@""];
-}
-
-#pragma mark -
-#pragma mark State
-
-/**
- * If the fire date was in the past.
- */
-- (BOOL) wasInThePast
-{
-    return [self timeIntervalSinceLastTrigger] > 0;
-}
-
-// If the notification was already scheduled
-- (BOOL) isScheduled
-{
-    return [self isRepeating] || ![self wasInThePast];
-}
-
-/**
- * If the notification was already triggered.
- */
-- (BOOL) isTriggered
-{
-    NSDate* now      = [NSDate date];
-    NSDate* fireDate = self.fireDate;
-
-    bool isLaterThanFireDate = !([now compare:fireDate] == NSOrderedAscending);
-
-    return isLaterThanFireDate;
-}
-
-/**
- * If the notification was updated.
- */
-- (BOOL) wasUpdated
-{
-    NSDate* now       = [NSDate date];
-    NSDate* updatedAt = [self.userInfo objectForKey:@"updatedAt"];
-
-    if (updatedAt == NULL)
-        return NO;
-
-    int timespan = [now timeIntervalSinceDate:updatedAt];
-
-    return timespan < 1;
-}
-
-/**
- * If it's a repeating notification.
- */
-- (BOOL) isRepeating
-{
-    return [self.options isRepeating];
-}
-
-/**
- * Process state type of the local notification.
- */
-- (APPLocalNotificationType) type
-{
-    return [self isTriggered] ? NotifcationTypeTriggered : NotifcationTypeScheduled;
-}
-
-@end

+ 6 - 28
src/ios/UILocalNotification+APPLocalNotification.h → src/ios/UNNotificationRequest+APPLocalNotification.h

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
+ * Apache 2.0 License
  *
- * @APPPLANT_LICENSE_HEADER_START@
+ * Copyright (c) Sebastian Katzer 2017
  *
  * This file contains Original Code and/or Modifications of Original Code
  * as defined in and that are subject to the Apache License
@@ -17,40 +17,18 @@
  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  * Please see the License for the specific language governing rights and
  * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
  */
 
-#import "APPLocalNotificationOptions.h"
+#import "APPNotificationOptions.h"
 
-typedef NS_ENUM(NSUInteger, APPLocalNotificationType) {
-    NotifcationTypeAll = 0,
-    NotifcationTypeScheduled = 1,
-    NotifcationTypeTriggered = 2
-};
+@import UserNotifications;
 
-@interface UILocalNotification (APPLocalNotification)
+@interface UNNotificationRequest (APPLocalNotification)
 
-// Initialize a new local notification
-- (id) initWithOptions:(NSDictionary*)dict;
 // The options provided by the plug-in
-- (APPLocalNotificationOptions*) options;
-// Timeinterval since last trigger date
-- (double) timeIntervalSinceLastTrigger;
-// Timeinterval since fire date
-- (double) timeIntervalSinceFireDate;
-// If the fire date was in the past
-- (BOOL) wasInThePast;
-// If the notification was already scheduled
-- (BOOL) isScheduled;
-// If the notification was already triggered
-- (BOOL) isTriggered;
+- (APPNotificationOptions*) options;
 // If the notification was updated
 - (BOOL) wasUpdated;
-// If it's a repeating notification
-- (BOOL) isRepeating;
-// Notifciation type
-- (APPLocalNotificationType) type;
 // Encode the user info dict to JSON
 - (NSString*) encodeToJSON;
 

+ 102 - 0
src/ios/UNNotificationRequest+APPLocalNotification.m

@@ -0,0 +1,102 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+#import "APPNotificationOptions.h"
+#import "UNNotificationRequest+APPLocalNotification.h"
+#import "APPNotificationContent.h"
+#import <objc/runtime.h>
+
+@import UserNotifications;
+
+static char optionsKey;
+
+@implementation UNNotificationRequest (APPLocalNotification)
+
+/**
+ * Get associated option object
+ */
+- (APPNotificationOptions*) getOptions
+{
+    return objc_getAssociatedObject(self, &optionsKey);
+}
+
+/**
+ * Set associated option object
+ */
+- (void) setOptions:(APPNotificationOptions*)options
+{
+    objc_setAssociatedObject(self, &optionsKey,
+                             options, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+/**
+ * The options provided by the plug-in.
+ */
+- (APPNotificationOptions*) options
+{
+    APPNotificationOptions* options = [self getOptions];
+
+    if (!options) {
+        options = [[APPNotificationOptions alloc]
+                   initWithDict:[self.content userInfo]];
+
+        [self setOptions:options];
+    }
+
+    return options;
+}
+
+/**
+ * If the notification was updated.
+ *
+ * @return [ BOOL ]
+ */
+- (BOOL) wasUpdated
+{
+    return [self.content userInfo][@"updatedAt"] != NULL;
+}
+
+/**
+ * Encode the user info dict to JSON.
+ */
+- (NSString*) encodeToJSON
+{
+    NSString* json;
+    NSData* data;
+    NSMutableDictionary* obj = [self.content.userInfo mutableCopy];
+
+    [obj removeObjectForKey:@"updatedAt"];
+
+    if (obj == NULL || obj.count == 0)
+        return json;
+
+    data = [NSJSONSerialization dataWithJSONObject:obj
+                                           options:NSJSONWritingPrettyPrinted
+                                             error:NULL];
+
+    json = [[NSString alloc] initWithData:data
+                                 encoding:NSUTF8StringEncoding];
+
+    return [json stringByReplacingOccurrencesOfString:@"\n"
+                                           withString:@""];
+}
+
+@end

+ 70 - 0
src/ios/UNUserNotificationCenter+APPLocalNotification.h

@@ -0,0 +1,70 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+#import "APPNotificationContent.h"
+
+@interface UNUserNotificationCenter (APPLocalNotification)
+
+extern NSString * const kAPPGeneralCategory;
+
+typedef NS_ENUM(NSUInteger, APPNotificationType) {
+    NotifcationTypeAll = 0,
+    NotifcationTypeScheduled = 1,
+    NotifcationTypeTriggered = 2,
+    NotifcationTypeUnknown = 3
+};
+
+#define APPNotificationType_DEFINED
+
+@property (readonly, getter=getNotifications) NSArray* localNotifications;
+@property (readonly, getter=getNotificationIds) NSArray* localNotificationIds;
+
+// Register general notification category to listen for dismiss actions
+- (void) registerGeneralNotificationCategory;
+// Add the specified category to the list of categories
+- (void) addNotificationCategory:(UNNotificationCategory*)category;
+
+// List of all notification IDs from given type
+- (NSArray*) getNotificationIdsByType:(APPNotificationType)type;
+
+// Find notification by ID
+- (UNNotificationRequest*) getNotificationWithId:(NSNumber*)id;
+// Find notification type by ID
+- (APPNotificationType) getTypeOfNotificationWithId:(NSNumber*)id;
+
+// Property list from all local notifications
+- (NSArray*) getNotificationOptions;
+// Property list from given local notifications
+- (NSArray*) getNotificationOptionsById:(NSArray*)ids;
+// Property list from all local notifications with type constraint
+- (NSArray*) getNotificationOptionsByType:(APPNotificationType)type;
+
+// Clear specified notfication
+- (void) clearNotification:(UNNotificationRequest*)notification;
+// Clear all notfications
+- (void) clearAllNotifications;
+
+// Cancel specified notfication
+- (void) cancelNotification:(UNNotificationRequest*)notification;
+// Cancel all notfications
+- (void) cancelAllNotifications;
+
+@end

+ 322 - 0
src/ios/UNUserNotificationCenter+APPLocalNotification.m

@@ -0,0 +1,322 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+#import "UNUserNotificationCenter+APPLocalNotification.h"
+#import "UNNotificationRequest+APPLocalNotification.h"
+
+@import UserNotifications;
+
+NSString * const kAPPGeneralCategory = @"GENERAL";
+
+@implementation UNUserNotificationCenter (APPLocalNotification)
+
+#pragma mark -
+#pragma mark NotificationCategory
+
+/**
+ * Register general notification category to listen for dismiss actions.
+ *
+ * @return [ Void ]
+ */
+- (void) registerGeneralNotificationCategory
+{
+    UNNotificationCategory* category;
+
+    category = [UNNotificationCategory
+                categoryWithIdentifier:kAPPGeneralCategory
+                actions:@[]
+                intentIdentifiers:@[]
+                options:UNNotificationCategoryOptionCustomDismissAction];
+
+    [self setNotificationCategories:[NSSet setWithObject:category]];
+}
+
+/**
+ * Add the specified category to the list of categories.
+ *
+ * @param [ UNNotificationCategory* ] category The category to add.
+ *
+ * @return [ Void ]
+ */
+- (void) addNotificationCategory:(UNNotificationCategory*)category
+{
+    if (!category)
+        return;
+
+    [self getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *set) {
+        NSMutableSet* categories = [NSMutableSet setWithSet:set];
+
+        for (UNNotificationCategory* item in categories) {
+            if ([category.identifier isEqualToString:item.identifier]) {
+                [categories removeObject:item];
+                break;
+            }
+        }
+
+        [categories addObject:category];
+        [self setNotificationCategories:categories];
+    }];
+}
+
+#pragma mark -
+#pragma mark LocalNotifications
+
+/**
+ * List of all delivered or still pending notifications.
+ */
+- (NSArray*) getNotifications
+{
+    NSMutableArray* notifications = [[NSMutableArray alloc] init];
+
+    [notifications addObjectsFromArray:[self getPendingNotifications]];
+    [notifications addObjectsFromArray:[self getDeliveredNotifications]];
+
+    return notifications;
+}
+
+/**
+ * List of all triggered notifications.
+ */
+- (NSArray*) getDeliveredNotifications
+{
+    NSMutableArray* notifications = [[NSMutableArray alloc] init];
+    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
+
+    [self getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> *delivered) {
+        for (UNNotification* notification in delivered) {
+            [notifications addObject:notification.request];
+        }
+        dispatch_semaphore_signal(sema);
+    }];
+
+    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
+
+    return notifications;
+}
+
+/**
+ * List of all pending notifications.
+ */
+- (NSArray*) getPendingNotifications
+{
+    NSMutableArray* notifications = [[NSMutableArray alloc] init];
+    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
+
+    [self getPendingNotificationRequestsWithCompletionHandler:^(NSArray<UNNotificationRequest *> *requests) {
+        [notifications addObjectsFromArray:requests];
+        dispatch_semaphore_signal(sema);
+    }];
+
+    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
+
+    return notifications;
+}
+
+/**
+ * List of all notifications from given type.
+ *
+ * @param type
+ *      Notification life cycle type
+ */
+- (NSArray*) getNotificationsByType:(APPNotificationType)type
+{
+    switch (type) {
+        case NotifcationTypeScheduled:
+            return [self getPendingNotifications];
+
+        case NotifcationTypeTriggered:
+            return [self getDeliveredNotifications];
+
+        default:
+            return [self getNotifications];
+    }
+}
+
+/**
+ * List of all local notifications IDs.
+ */
+- (NSArray*) getNotificationIds
+{
+    NSArray* notifications = [self getNotifications];
+    NSMutableArray* ids    = [[NSMutableArray alloc] init];
+
+    for (UNNotificationRequest* notification in notifications)
+    {
+        [ids addObject:notification.options.id];
+    }
+
+    return ids;
+}
+
+/**
+ * List of all notifications IDs from given type.
+ *
+ * @param type
+ *      Notification life cycle type
+ */
+- (NSArray*) getNotificationIdsByType:(APPNotificationType)type
+{
+    NSArray* notifications = [self getNotificationsByType:type];
+    NSMutableArray* ids    = [[NSMutableArray alloc] init];
+
+    for (UNNotificationRequest* notification in notifications)
+    {
+        [ids addObject:notification.options.id];
+    }
+
+    return ids;
+}
+
+/**
+ * Find notification by ID.
+ *
+ * @param id
+ *      Notification ID
+ */
+- (UNNotificationRequest*) getNotificationWithId:(NSNumber*)id
+{
+    NSArray* notifications = [self getNotifications];
+
+    for (UNNotificationRequest* notification in notifications)
+    {
+        NSString* fid = [NSString stringWithFormat:@"%@", notification.options.id];
+        
+        if ([fid isEqualToString:[id stringValue]]) {
+            return notification;
+        }
+    }
+    
+    return NULL;
+}
+
+/**
+ * Find notification type by ID
+ */
+- (APPNotificationType) getTypeOfNotificationWithId:(NSNumber*)id
+{
+    NSArray* ids = [self getNotificationIdsByType:NotifcationTypeTriggered];
+    
+    if ([ids containsObject:id])
+        return NotifcationTypeTriggered;
+
+    ids = [self getNotificationIdsByType:NotifcationTypeScheduled];
+    
+    if ([ids containsObject:id])
+        return NotifcationTypeScheduled;
+    
+    return NotifcationTypeUnknown;
+}
+
+/**
+ * List of properties from all notifications.
+ */
+- (NSArray*) getNotificationOptions
+{
+    return [self getNotificationOptionsByType:NotifcationTypeAll];
+}
+
+/**
+ * List of properties from all notifications of given type.
+ *
+ * @param type
+ *      Notification life cycle type
+ */
+- (NSArray*) getNotificationOptionsByType:(APPNotificationType)type
+{
+    NSArray* notifications  = [self getNotificationsByType:type];
+    NSMutableArray* options = [[NSMutableArray alloc] init];
+
+    for (UNNotificationRequest* notification in notifications)
+    {
+        [options addObject:notification.options.userInfo];
+    }
+
+    return options;
+}
+
+/**
+ * List of properties from given local notifications.
+ *
+ * @param ids
+ *      Notification IDs
+ */
+- (NSArray*) getNotificationOptionsById:(NSArray*)ids
+{
+    NSArray* notifications  = [self getNotifications];
+    NSMutableArray* options = [[NSMutableArray alloc] init];
+    
+    for (UNNotificationRequest* notification in notifications)
+    {
+        if ([ids containsObject:notification.options.id]) {
+            [options addObject:notification.options.userInfo];
+        }
+    }
+    
+    return options;
+}
+
+/*
+ * Clear all notfications.
+ */
+- (void) clearAllNotifications
+{
+    [self removeAllDeliveredNotifications];
+}
+
+/*
+ * Clear Specified notfication.
+ *
+ * @param notification
+ *      The notification object
+ */
+- (void) clearNotification:(UNNotificationRequest*)notification
+{
+    NSArray* ids = [[NSArray alloc]
+                    initWithObjects:notification.identifier, nil];
+
+    [self removeDeliveredNotificationsWithIdentifiers:ids];
+}
+
+/*
+ * Cancel all notfications.
+ */
+- (void) cancelAllNotifications
+{
+    [self removeAllPendingNotificationRequests];
+    [self removeAllDeliveredNotifications];
+}
+
+/*
+ * Cancel specified notfication.
+ *
+ * @param notification
+ *      The notification object
+ */
+- (void) cancelNotification:(UNNotificationRequest*)notification
+{
+    NSArray* ids = [[NSArray alloc]
+                    initWithObjects:notification.identifier, nil];
+
+    [self removeDeliveredNotificationsWithIdentifiers:ids];
+    [self removePendingNotificationRequestsWithIdentifiers:ids];
+}
+
+@end

+ 0 - 437
src/windows/LocalNotificationCore.js

@@ -1,437 +0,0 @@
-/*
-    Copyright 2013-2015 appPlant UG
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-*/
-
-
-var proxy = require('de.appplant.cordova.plugin.local-notification.LocalNotification.Proxy');
-
-var Notifications = Windows.UI.Notifications;
-
-
-proxy.core = {
-
-    /**
-     * Executes all queued events.
-     */
-    deviceready: function () {
-        var plugin = cordova.plugins.notification.local,
-            events = this.eventQueue;
-
-        this.isReady = true;
-
-        for (var i = 0; i < events.length; i++) {
-            plugin.fireEvent.apply(plugin, events[i]);
-        }
-
-        this.eventQueue = [];
-    },
-
-    /**
-     * Schedules new local notifications.
-     *
-     * @param {Object[]} notifications
-     *      Array of local notifications
-     * @param {String} event
-     *      'schedule' or 'update'
-     */
-    schedule: function (notifications) {
-        var triggerFn = function (notification) {
-            this.updateBadge(notification.badge);
-            this.fireEvent('trigger', notification);
-        };
-
-        for (var i = 0; i < notifications.length; i++) {
-            var options = notifications[i],
-                notification = this.build(options);
-
-            this.cancelLocalNotification(options.id);
-            this.scheduleLocalNotification(notification, options);
-            this.scheduleBackupNotification(notification, options);
-            this.fireEvent('schedule', options);
-            this.callOnTrigger(options, triggerFn);
-        }
-    },
-
-    /**
-     * Schedules a single local notification.
-     *
-     * @param {Windows.Data.Xml.Dom.XmlDocument} notification
-     *      The local notification
-     * @param {Object} options
-     *      Local notification properties
-     */
-    scheduleLocalNotification: function (notification, options) {
-        var interval = this.getRepeatInterval(options.every),
-            triggerTime = new Date((options.at * 1000)),
-            now = new Date().getTime(),
-            toast;
-
-        if (triggerTime <= now) {
-            triggerTime = new Date(now + 10);
-        }
-
-        try {
-            if (interval !== 0 && interval < 360001 && interval > 59999) {
-                toast = new Notifications.ScheduledToastNotification(
-                    notification, triggerTime, interval, 5);
-            } else {
-                toast = new Notifications.ScheduledToastNotification(
-                    notification, triggerTime);
-            }
-        } catch (e) {
-            console.error(e);
-            return;
-        }
-
-        toast.id = options.id;
-        toast.tag = 'Toast' + toast.id;
-
-        this.getToastNotifier().addToSchedule(toast);
-    },
-
-    /**
-     * Schedules a backup local notification 10 years later.
-     *
-     * @param {Object} notification
-     *      The local notification
-     */
-    scheduleBackupNotification: function (notification, options) {
-        var properties = Object.create(options);
-
-        properties.id = options.id + '-2';
-        properties.at = options.at + 315360000; // 10 years later
-
-        this.scheduleLocalNotification(notification, properties);
-    },
-
-    /**
-     * Updates the badge number of the active tile.
-     *
-     * @param {Number} badge
-     *      The badge number. Zero will clean the badge.
-     */
-    updateBadge: function (badge) {
-        var notifications = Windows.UI.Notifications,
-            type = notifications.BadgeTemplateType.badgeNumber,
-            xml = notifications.BadgeUpdateManager.getTemplateContent(type),
-            attrs = xml.getElementsByTagName('badge'),
-            notification = new notifications.BadgeNotification(xml);
-
-        attrs[0].setAttribute('value', badge);
-
-        notifications.BadgeUpdateManager.createBadgeUpdaterForApplication()
-            .update(notification);
-    },
-
-    /**
-     * Updates existing notifications specified by IDs in options.
-     *
-     * @param {Object[]} notifications
-     *      Array of local notifications
-     */
-    update: function (notifications) {
-        for (var i = 0; i < notifications.length; i++) {
-            var updates = notifications[i],
-                options = getAll(updates.id || '0')[0];
-
-            this.updateLocalNotification(options, updates);
-            this.fireEvent('update', options);
-        }
-    },
-
-    /**
-     * Updates a single local notification.
-     *
-     * @param {Object} notification
-     *      The local notification
-     * @param {Object} updates
-     *      Updated properties
-     */
-    updateLocalNotification: function (notification, updates) {
-        for (var key in updates) {
-            notification[key] = updates[key];
-        }
-
-        this.cancelLocalNotification(notification.id);
-        this.scheduleLocalNotification(notification);
-    },
-
-    /**
-     * Clears the specified notifications.
-     *
-     * @param {int[]} ids
-     *      List of local notification IDs
-     */
-    clear: function (ids) {
-        for (var i = 0; i < ids.length; i++) {
-            var id = ids[i],
-                notification = this.getAll([id])[0];
-
-            this.clearLocalNotification(id);
-            this.fireEvent('clear', notification);
-        }
-    },
-
-    /**
-     * Clears the local notification with the given ID.
-     *
-     * @param {String} id
-     *      Local notification ID
-     */
-    clearLocalNotification: function (id) {
-        var notification = this.getAll([id])[0];
-
-        try {
-            this.getToastHistory().remove('Toast' + id);
-        } catch (e) {/*Only Phones support the NotificationHistory*/ }
-
-        if (this.isRepeating(notification))
-            return;
-
-        if (this.isTriggered(id) && !this.isScheduled(id)) {
-            this.cancelLocalNotification(id);
-        }
-    },
-
-    /**
-     * Clears all notifications.
-     */
-    clearAll: function () {
-        var ids = this.getTriggeredIds();
-
-        for (var i = 0; i < ids.length; i++) {
-            this.clearLocalNotification(ids[i]);
-        }
-
-        try {
-            this.getToastHistory().clear();
-        } catch (e) {/*Only Phones support the NotificationHistory*/ }
-        this.fireEvent('clearall');
-    },
-
-    /**
-     * Cancels all specified notifications.
-     *
-     * @param {int[]} ids
-     *      List of local notification IDs
-     */
-    cancel: function (ids) {
-        for (var i = 0; i < ids.length; i++) {
-            var id = ids[i],
-                notification = this.getAll([id])[0];
-
-            this.cancelLocalNotification(ids[i]);
-            this.fireEvent('cancel', notification);
-        }
-    },
-
-    /**
-     * Cancels the local notification with the given ID.
-     *
-     * @param {String} id
-     *      Local notification ID
-     */
-    cancelLocalNotification: function (id) {
-        var notifier = this.getToastNotifier(),
-            history = this.getToastHistory(),
-            toasts = this.getScheduledToasts();
-
-        try {
-            history.remove('Toast' + id);
-        } catch (e) {/*Only Phones support the NotificationHistory*/ }
-
-        for (var i = 0; i < toasts.length; i++) {
-            var toast = toasts[i];
-
-            if (toast.id == id || toast.id == id + '-2') {
-                notifier.removeFromSchedule(toast);
-            }
-        }
-    },
-
-    /**
-     * Cancels all notifications.
-     */
-    cancelAll: function () {
-        var ids = this.getAllIds();
-
-        for (var i = 0; i < ids.length; i++) {
-            this.cancelLocalNotification(ids[i]);
-        }
-
-        try {
-            this.getToastHistory().clear();
-        } catch (e) {/*Only Phones support the NotificationHistory*/ }
-        this.fireEvent('cancelall');
-    },
-
-    /**
-     * Checks if a notification with an ID is present.
-     *
-     * @param {int} id
-     *      Local notification ID
-     */
-    isPresent: function (id) {
-        return !!this.findToastById(id);
-    },
-
-    /**
-     * Checks if a notification with an ID was scheduled.
-     *
-     * @param {int} id
-     *      Local notification ID
-     */
-    isScheduled: function (id) {
-        var toast = this.findToastById(id);
-
-        return toast && this.isToastScheduled(toast);
-    },
-
-    /**
-     * Checks if a notification with an ID was triggered.
-     *
-     * @param {int} id
-     *      Local notification ID
-     */
-    isTriggered: function (id) {
-        var toast = this.findToastById(id);
-
-        return toast && this.isToastTriggered(toast);
-    },
-
-    /**
-     * Lists all local notification IDs.
-     */
-    getAllIds: function () {
-        var toasts = this.getScheduledToasts(),
-            ids = [];
-
-        for (var i = 0; i < toasts.length; i++) {
-            var toast   = toasts[i],
-                toastId = this.getToastId(toast);
-
-            if (ids.indexOf(toastId) == -1) {
-                ids.push(toastId);
-            }
-        }
-
-        return ids;
-    },
-
-    /**
-     * Lists all scheduled notification IDs.
-     */
-    getScheduledIds: function () {
-        var toasts = this.getScheduledToasts(),
-            ids = [];
-
-        for (var i = 0; i < toasts.length; i++) {
-            var toast = toasts[i];
-
-            if (!this.isToastScheduled(toast))
-                continue;
-
-            ids.push(this.getToastId(toast));
-        }
-
-        return ids;
-    },
-
-    /**
-     * Lists all scheduled notification IDs.
-     */
-    getTriggeredIds: function () {
-        var toasts = this.getScheduledToasts(),
-            ids = [];
-
-        for (var i = 0; i < toasts.length; i++) {
-            var toast = toasts[i];
-
-            if (!this.isToastTriggered(toast))
-                continue;
-
-            ids.push(this.getToastId(toast));
-        }
-
-        return ids;
-    },
-
-    /**
-     * Property list for given notifications.
-     * If called without IDs, all notification will be returned.
-     *
-     * @param {int[]} ids
-     *      List of local notification IDs.
-     * @param {String?} type
-     *      Local notification life cycle type
-     */
-    getAll: function (ids, type) {
-        var toasts = this.getScheduledToasts(),
-            notifications = [];
-
-        if (!ids || ids.length === 0) {
-            ids = this.getAllIds();
-        }
-
-        for (var index = 0; index < ids.length; index++) {
-            var id = ids[index],
-                toast = this.findToastById(id);
-
-            if (!toast || type && this.getToastType(toast) != type)
-                continue;
-
-            var json = toast.content.lastChild.lastChild.innerText;
-
-            notifications.push(JSON.parse(json));
-        }
-
-        return notifications;
-    },
-
-    /**
-     * Property list for given notifications.
-     * If called without IDs, all notification will be returned.
-     *
-     * @param {int[]} ids
-     *      List of local notification IDs
-     */
-    getScheduled: function (ids) {
-        if (!ids || ids.length === 0) {
-            ids = this.getAllIds();
-        }
-
-        return this.getAll(ids, 'scheduled');
-    },
-
-    /**
-     * Property list for given notifications.
-     * If called without IDs, all notification will be returned.
-     *
-     * @param {int[]} ids
-     *      List of local notification IDs
-     */
-    getTriggered: function (ids) {
-        if (!ids || ids.length === 0) {
-            ids = this.getAllIds();
-        }
-
-        return this.getAll(ids, 'triggered');
-    },
-};

+ 431 - 202
src/windows/LocalNotificationProxy.js

@@ -1,311 +1,540 @@
 /*
-    Copyright 2013-2015 appPlant UG
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-*/
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+var LocalNotification = LocalNotificationProxy.LocalNotification,
+       ActivationKind = Windows.ApplicationModel.Activation.ActivationKind;
+
+var impl  = new LocalNotificationProxy.LocalNotificationProxy(),
+    queue = [],
+    ready = false;
+
+/**
+ * Set launchDetails object.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
+ */
+exports.launch = function (success, error, args) {
+    var plugin = cordova.plugins.notification.local;
+
+    if (args.length === 0 || plugin.launchDetails) return;
+
+    plugin.launchDetails = { id: args[0], action: args[1] };
+};
+
+/**
+ * To execute all queued events.
+ *
+ * @return [ Void ]
+ */
+exports.ready = function () {
+    ready = true;
+
+    for (var item of queue) {
+        exports.fireEvent.apply(exports, item);
+    }
+
+    queue = [];
+};
+
+/**
+ * Check permission to show notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
+ */
+exports.check = function (success, error) {
+    var granted = impl.hasPermission();
+
+    success(granted);
+};
 
 /**
- * Executes all queued events.
+ * Request permission to show notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
-exports.deviceready  = function () {
-    exports.core.deviceready();
+exports.request = function (success, error) {
+    exports.check(success, error);
 };
 
 /**
- * Schedule a new local notification.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {Object[]} notifications
- *      Array of local notifications
+ * Schedule notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
  */
-exports.schedule = function (success, error, notifications) {
-    exports.core.schedule(notifications, 'schedule');
+exports.schedule = function (success, error, args) {
+    var options = [];
+
+    for (var props of args) {
+        opts  = exports.parseOptions(props);
+        options.push(opts);
+    }
+
+    impl.schedule(options);
+
+    for (var toast of options) {
+        exports.fireEvent('add', toast);
+    }
 
     success();
 };
 
 /**
- * Update existing notifications specified by IDs in options.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {Object[]} notifications
- *      Array of local notifications
+ * Update notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
  */
-exports.update = function (success, error, notifications) {
-    exports.core.update(notifications);
+exports.update = function (success, error, args) {
+    var options = [];
+
+    for (var props of args) {
+        opts  = exports.parseOptions(props);
+        options.push(opts);
+    }
+
+    impl.update(options);
+
+    for (var toast of options) {
+        exports.fireEvent('update', toast);
+    }
 
     success();
 };
 
 /**
- * Clear the specified notification.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * Clear the notifications specified by id.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
  */
-exports.clear = function (success, error, ids) {
-    exports.core.clear(ids, true);
+exports.clear = function (success, error, args) {
+    var toasts = impl.clear(args) || [];
+
+    for (var toast of toasts) {
+        exports.fireEvent('clear', toast);
+    }
 
     success();
 };
 
 /**
- * Clear all previously sheduled notifications.
+ * Clear all notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
  *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
+ * @return [ Void ]
  */
 exports.clearAll = function (success, error) {
-    exports.core.clearAll();
-
+    impl.clearAll();
+    exports.fireEvent('clearall');
     success();
 };
 
 /**
- * Cancel the specified notifications.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * Cancel the notifications specified by id.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
  */
-exports.cancel = function (success, error, ids) {
-    exports.core.cancel(ids, true);
+exports.cancel = function (success, error, args) {
+    var toasts = impl.cancel(args) || [];
+
+    for (var toast of toasts) {
+        exports.fireEvent('cancel', toast);
+    }
 
     success();
 };
 
 /**
- * Remove all previously registered notifications.
+ * Cancel all notifications.
  *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
 exports.cancelAll = function (success, error) {
-    exports.core.cancelAll();
-
+    impl.cancelAll();
+    exports.fireEvent('cancelall');
     success();
 };
 
 /**
- * Check if a notification with an ID is present.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int} id
- *      Local notification ID
+ * Get the type of notification.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
  */
-exports.isPresent = function (success, error, args) {
-    var found = exports.core.isPresent(args[0]);
+exports.type = function (success, error, args) {
+    var type = impl.type(args[0]);
 
-    success(found);
+    success(type);
 };
 
 /**
- * Check if a notification with an ID is scheduled.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int} id
- *      Local notification ID
+ * List of all notification ids.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
-exports.isScheduled = function (success, error, args) {
-    var found = exports.core.isScheduled(args[0]);
+exports.ids = function (success, error) {
+    var ids = impl.ids() || [];
 
-    success(found);
+    success(Array.from(ids));
 };
 
 /**
- * Check if a notification with an ID was triggered.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int} id
- *      Local notification ID
+ * List of all scheduled notification ids.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
-exports.isTriggered = function (success, error, args) {
-    var found = exports.core.isTriggered(args[0]);
+exports.scheduledIds = function (success, error) {
+    var ids = impl.scheduledIds() || [];
 
-    success(found);
+    success(Array.from(ids));
 };
 
 /**
- * List all local notification IDs.
+ * List of all triggered notification ids.
  *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
-exports.getAllIds = function (success, error) {
-    var ids = exports.core.getAllIds();
+exports.triggeredIds = function (success, error) {
+    var ids = impl.triggeredIds() || [];
 
-    success(ids);
+    success(Array.from(ids));
 };
 
 /**
- * List all scheduled notification IDs.
+ * Get a single notification by id.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
  *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
+ * @return [ Void ]
  */
-exports.getScheduledIds = function (success, error) {
-    var ids = exports.core.getScheduledIds();
+exports.notification = function (success, error, args) {
+    var obj = impl.notification(args[0]);
 
-    success(ids);
+    success(exports.clone(obj));
 };
 
 /**
- * List all triggered notification IDs.
+ * List of (all) notifications.
  *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ * @param [ Array ]    args    Interface arguments
+ *
+ * @return [ Void ]
  */
-exports.getTriggeredIds = function (success, error) {
-    var ids = exports.core.getTriggeredIds();
+exports.notifications = function (success, error, args) {
+    var objs = impl.notifications(args) || [];
 
-    success(ids);
+    success(exports.cloneAll(objs));
 };
 
 /**
- * Propertys for given notification.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * List of all scheduled notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
-exports.getSingle = function (success, error, ids) {
-    var notification = exports.core.getAll(ids)[0];
+exports.scheduledNotifications = function (success, error) {
+    var objs = impl.scheduledNotifications() || [];
 
-    success(notification);
+    success(exports.cloneAll(objs));
 };
 
 /**
- * Propertys for given scheduled notification.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * List of all triggered notifications.
+ *
+ * @param [ Function ] success Success callback
+ * @param [ Function ] error   Error callback
+ *
+ * @return [ Void ]
  */
-exports.getSingleScheduled = function (success, error, ids) {
-    var notification = exports.core.getScheduled(ids)[0];
+exports.triggeredNotifications = function (success, error) {
+    var objs = impl.triggeredNotifications() || [];
 
-    success(notification);
+    success(exports.cloneAll(objs));
 };
 
 /**
- * Propertys for given triggered notification.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * Inform the user through the click event that a notification was clicked.
+ *
+ * @param [ String ] xml The launch identifier.
+ *
+ * @return [ Void ]
+ */
+exports.clicked = function (xml, input) {
+    var toast = LocalNotification.Options.parse(xml),
+        event = toast.action || 'click',
+        meta  = Object.assign({}, input);
+
+    if (input && input.size > 0) {
+        meta.text = input.first().current.value;
+    }
+
+    if (!ready) {
+        exports.launch(null, null, [toast.id, event]);
+    }
+
+    exports.fireEvent(event, toast, meta);
+};
+
+/**
+ * Invoke listeners for the given event.
+ *
+ * @param [ String ] event The name of the event.
+ * @param [ Object ] toast Optional notification object.
+ * @param [ Object ] data  Optional meta data about the event.
+ *
+ * @return [ Void ]
+ */
+exports.fireEvent = function (event, toast, data) {
+    var meta   = Object.assign({ event: event }, data),
+        plugin = cordova.plugins.notification.local.core;
+
+    if (!ready) {
+        queue.push(arguments);
+        return;
+    }
+
+    if (toast) {
+        plugin.fireEvent(event, exports.clone(toast), meta);
+    } else {
+        plugin.fireEvent(event, meta);
+    }
+};
+
+/**
+ * Clone the objects and delete internal properties.
+ *
+ * @param [ Array<Object> ] objs The objects to clone for.
+ *
+ * @return [ Array<Object> ]
+ */
+exports.cloneAll = function (objs) {
+    var clones = [];
+
+    for (var obj of objs) {
+        clones.push(exports.clone(obj));
+    }
+
+    return clones;
+};
+
+/**
+ * Clone the object and delete internal properties.
+ *
+ * @param [ Object ] obj The object to clone for.
+ *
+ * @return [ Object ]
+ */
+exports.clone = function (obj) {
+    var ignore = ['action'],
+        dclone = ['trigger'],
+        clone  = {};
+
+    if (obj === null) return null;
+
+    for (var prop in obj) {
+        if (ignore.includes(prop) || typeof obj[prop] === 'function')
+            continue;
+
+        try {
+            clone[prop] = dclone.includes(prop) ? exports.clone(obj[prop]) : obj[prop];
+        } catch (e) {
+            clone[prop] = null;
+        }
+    }
+
+    return clone;
+};
+
+/**
+ * Parse notification spec into an instance of prefered type.
+ *
+ * @param [ Object ] obj The notification options map.
+ *
+ * @return [ LocalNotification.Options ]
  */
-exports.getSingleTriggered = function (success, error, ids) {
-    var notification = exports.core.getTriggered(ids)[0];
+exports.parseOptions = function (obj) {
+    var opts   = new LocalNotification.Options(),
+        ignore = ['progressBar', 'actions', 'trigger'];
+
+    for (var prop in opts) {
+        if (!ignore.includes(prop) && obj[prop]) {
+            opts[prop] = obj[prop];
+        }
+    }
+
+    var progressBar  = exports.parseProgressBar(obj);
+    opts.progressBar = progressBar;
+
+    var trigger  = exports.parseTrigger(obj);
+    opts.trigger = trigger;
+
+    var actions  = exports.parseActions(obj);
+    opts.actions = actions;
 
-    success(notification);
+    return opts;
 };
 
 /**
- * Property list for given notifications.
- * If called without IDs, all notification will be returned.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * Parse trigger spec into instance of prefered type.
+ *
+ * @param [ Object ] obj The notification options map.
+ *
+ * @return [ LocalNotification.Trigger ]
  */
-exports.getAll = function (success, error, ids) {
-    var notifications = exports.core.getAll(ids);
+exports.parseTrigger = function (obj) {
+    var trigger = new LocalNotification.Trigger(),
+        spec    = obj.trigger, val;
+
+    if (!spec) return trigger;
+
+    for (var prop in trigger) {
+        val = spec[prop];
+        if (!val) continue;
+        trigger[prop] = prop == 'every' ? exports.parseEvery(val) : val;
+    }
 
-    success(notifications);
+    return trigger;
 };
 
 /**
- * Property list for given triggered notifications.
- * If called without IDs, all notification will be returned.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * Parse trigger.every spec into instance of prefered type.
+ *
+ * @param [ Object ] spec The trigger.every object.
+ *
+ * @return [ LocalNotification.Every|String ]
  */
-exports.getScheduled = function (success, error, ids) {
-    var notifications = exports.core.getScheduled(ids);
+exports.parseEvery = function (spec) {
+    var every = new LocalNotification.Every();
+
+    if (typeof spec !== 'object') return spec;
+
+    for (var prop in every) {
+        if (spec[prop]) every[prop] = parseInt(spec[prop]);
+    }
 
-    success(notifications);
+    return every;
 };
 
 /**
- * Property list for given triggered notifications.
- * If called without IDs, all notification will be returned.
- *
- * @param {Function} success
- *      Success callback
- * @param {Function} error
- *      Error callback
- * @param {int[]} ids
- *      List of local notification IDs
+ * Parse action specs into instances of prefered types.
+ *
+ * @param [ Object ] obj The notification options map.
+ *
+ * @return [ Array<LocalNotification.Action> ]
  */
-exports.getTriggered = function (success, error, ids) {
-    var notifications = exports.core.getTriggered(ids);
+exports.parseActions = function (obj) {
+    var actions = [], btn;
+
+    if (!obj.actions) return actions;
+
+    for (var action of obj.actions) {
+        if (!action.type || action.type == 'button') {
+            btn = new LocalNotification.Button();
+        } else
+        if (action.type == 'input') {
+            btn = new LocalNotification.Input();
+        }
+
+        for (var prop in btn) {
+            if (action[prop]) btn[prop] = action[prop];
+        }
+
+        actions.push(btn);
+    }
+
+    return actions;
+};
+
+/**
+ * Parse progressBar specs into instances of prefered types.
+ *
+ * @param [ Object ] obj The notification options map.
+ *
+ * @return [ LocalNotification.ProgressBar ]
+ */
+exports.parseProgressBar = function (obj) {
+    var bar  = new LocalNotification.ProgressBar(),
+        spec = obj.progressBar;
+
+    if (!spec) return bar;
+
+    for (var prop in bar) {
+        if (spec[prop]) bar[prop] = spec[prop];
+    }
 
-    success(notifications);
+    return bar;
 };
 
+// Handle onclick event
+document.addEventListener('activated', function (e) {
+    if (e.kind == ActivationKind.toastNotification) {
+        exports.clicked(e.raw.argument, e.raw.userInput);
+    }
+}, false);
 
 cordova.commandProxy.add('LocalNotification', exports);

BIN
src/windows/LocalNotificationProxy/.vs/LocalNotificationProxy/v15/.suo


+ 40 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy.sln

@@ -0,0 +1,40 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26430.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalNotificationProxy", "LocalNotificationProxy\LocalNotificationProxy.csproj", "{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Debug|ARM = Debug|ARM
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|Any CPU = Release|Any CPU
+		Release|ARM = Release|ARM
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|ARM.ActiveCfg = Debug|ARM
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|ARM.Build.0 = Debug|ARM
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|x64.ActiveCfg = Debug|x64
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|x64.Build.0 = Debug|x64
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|x86.ActiveCfg = Debug|x86
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Debug|x86.Build.0 = Debug|x86
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|ARM.ActiveCfg = Release|ARM
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|ARM.Build.0 = Release|ARM
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|x64.ActiveCfg = Release|x64
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|x64.Build.0 = Release|x64
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|x86.ActiveCfg = Release|x86
+		{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}.Release|x86.Build.0 = Release|x86
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

+ 199 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Builder.cs

@@ -0,0 +1,199 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    using Microsoft.Toolkit.Uwp.Notifications;
+    using Windows.UI.Notifications;
+
+    internal class Builder
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Builder"/> class.
+        /// </summary>
+        /// <param name="options">Notification properties to set.</param>
+        public Builder(Options options)
+        {
+            this.Content = new Notification(options);
+        }
+
+        /// <summary>
+        /// Gets the content.
+        /// </summary>
+        public Notification Content { get; private set; }
+
+        /// <summary>
+        /// Gets the options.
+        /// </summary>
+        private Options Options { get => this.Content.Options; }
+
+        /// <summary>
+        /// Gets the trigger.
+        /// </summary>
+        private Trigger Trigger { get => this.Options.Trigger; }
+
+        /// <summary>
+        /// Build a toast notification specified by the options.
+        /// </summary>
+        /// <returns>A fully configured toast notification instance.</returns>
+        public ScheduledToastNotification Build()
+        {
+            var toast = this.InitToast();
+
+            this.AddProgressBarToToast(toast);
+            this.AddAttachmentsToToast(toast);
+            this.AddActionsToToast(toast);
+
+            return this.ConvertToastToNotification(toast);
+        }
+
+        /// <summary>
+        /// If there is at least one more toast variant to build.
+        /// </summary>
+        /// <returns>True if there are more toasts to build.</returns>
+        public bool HasNext() => this.Trigger.Count >= this.Trigger.Occurrence;
+
+        /// <summary>
+        /// Moves the flag to the next toast variant.
+        /// </summary>
+        public void MoveNext() => this.Trigger.Occurrence += this.HasNext() ? 1 : 0;
+
+        /// <summary>
+        /// Gets the initialize skeleton for a toast notification.
+        /// </summary>
+        /// <returns>Basic skeleton with sound, image and text.</returns>
+        private ToastContent InitToast()
+        {
+            return new ToastContent()
+            {
+                Launch = this.Content.GetXml(),
+                Audio = this.Content.Sound,
+
+                Visual = new ToastVisual()
+                {
+                    BindingGeneric = new ToastBindingGeneric()
+                    {
+                        Children =
+                        {
+                            new AdaptiveText()
+                            {
+                                Text = this.Options.Title
+                            },
+
+                            new AdaptiveText()
+                            {
+                                Text = this.Options.Text
+                            }
+                        },
+
+                        AppLogoOverride = this.Content.Icon
+                    }
+                },
+
+                Actions = new ToastActionsCustom()
+                {
+                    Buttons = { },
+                    Inputs = { }
+                }
+            };
+        }
+
+        /// <summary>
+        /// Adds optional progress bar to the toast.
+        /// </summary>
+        /// <param name="toast">Tho toast to extend for.</param>
+        private void AddProgressBarToToast(ToastContent toast)
+        {
+            var progressBar = this.Content.ProgressBar;
+
+            if (progressBar != null)
+            {
+                toast.Visual.BindingGeneric.Children.Add(progressBar);
+            }
+        }
+
+        /// <summary>
+        /// Adds attachments to the toast.
+        /// </summary>
+        /// <param name="toast">Tho toast to extend for.</param>
+        private void AddAttachmentsToToast(ToastContent toast)
+        {
+            foreach (var image in this.Content.Attachments)
+            {
+                toast.Visual.BindingGeneric.Children.Add(image);
+            }
+        }
+
+        /// <summary>
+        /// Adds buttons and input fields to the toast.
+        /// </summary>
+        /// <param name="toast">Tho toast to extend for.</param>
+        private void AddActionsToToast(ToastContent toast)
+        {
+            foreach (var btn in this.Content.Inputs)
+            {
+                (toast.Actions as ToastActionsCustom).Inputs.Add(btn);
+            }
+
+            foreach (var btn in this.Content.Buttons)
+            {
+                (toast.Actions as ToastActionsCustom).Buttons.Add(btn);
+            }
+        }
+
+        /// <summary>
+        /// Converts the toast into a notification.
+        /// </summary>
+        /// <param name="toast">The toast to convert.</param>
+        /// <returns>A notification ready to schedule.</returns>
+        private ScheduledToastNotification ConvertToastToNotification(ToastContent toast)
+        {
+            var xml = toast.GetXml();
+            var at = this.Content.Date;
+            ScheduledToastNotification notification;
+
+            if (!at.HasValue)
+            {
+                return null;
+            }
+
+            try
+            {
+                notification = new ScheduledToastNotification(xml, at.Value);
+            }
+            catch
+            {
+                return null;
+            }
+
+            notification.Id = this.Content.Id;
+            notification.Tag = this.Options.Id.ToString();
+            notification.SuppressPopup = this.Options.Silent;
+
+            if (this.Trigger.Occurrence > 1)
+            {
+                notification.Group = notification.Tag;
+            }
+
+            return notification;
+        }
+    }
+}

+ 41 - 44
src/android/notification/ClearReceiver.java → src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Button.cs

@@ -1,44 +1,41 @@
-/*
- * Copyright (c) 2013-2015 by appPlant UG. All rights reserved.
- *
- * @APPPLANT_LICENSE_HEADER_START@
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apache License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://opensource.org/licenses/Apache-2.0/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- *
- * @APPPLANT_LICENSE_HEADER_END@
- */
-
-package de.appplant.cordova.plugin.notification;
-
-/**
- * The clear intent receiver is triggered when the user clears a
- * notification manually. It un-persists the cleared notification from the
- * shared preferences.
- */
-public class ClearReceiver extends AbstractClearReceiver {
-
-    /**
-     * Called when a local notification was cleared from outside of the app.
-     *
-     * @param notification
-     *      Wrapper around the local notification
-     */
-    @Override
-    public void onClear (Notification notification) {
-        notification.clear();
-    }
-
-}
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    public sealed class Button : IAction
+    {
+        /// <summary>
+        /// Gets or sets the ID.
+        /// </summary>
+        public string ID { get; set; }
+
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        public string Title { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to launch the app.
+        /// </summary>
+        public bool Launch { get; set; } = true;
+    }
+}

+ 88 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Every.cs

@@ -0,0 +1,88 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    using System;
+
+    public sealed class Every
+    {
+        private static readonly string[] Intervals = { null, "minute", "hour", "day", "month", "year" };
+
+        /// <summary>
+        /// Gets or sets the minute.
+        /// </summary>
+        public int? Minute { get; set; }
+
+        /// <summary>
+        /// Gets or sets the hour.
+        /// </summary>
+        public int? Hour { get; set; }
+
+        /// <summary>
+        /// Gets or sets the day.
+        /// </summary>
+        public int? Day { get; set; }
+
+        /// <summary>
+        /// Gets or sets the month.
+        /// </summary>
+        public int? Month { get; set; }
+
+        /// <summary>
+        /// Gets or sets the year.
+        /// </summary>
+        public int? Year { get; set; }
+
+        /// <summary>
+        /// Gets the interval as a string representation.
+        /// </summary>
+        /// <returns>year, month, ...</returns>
+        internal string Interval { get => Intervals[Array.IndexOf(this.ToArray(), null) + 1]; }
+
+        /// <summary>
+        /// Converts the date time components into a datetime object.
+        /// </summary>
+        /// <param name="now">The relative date to calculate the date from.</param>
+        /// <returns>A datetime object</returns>
+        internal DateTime ToDateTime(DateTime now)
+        {
+            var p = this.ToArray();
+
+            p[0] = this.Minute.GetValueOrDefault();
+            p[1] = this.Hour.GetValueOrDefault();
+            p[2] = this.Day.GetValueOrDefault(now.Day);
+            p[3] = this.Month.GetValueOrDefault(now.Month);
+            p[4] = this.Year.GetValueOrDefault(now.Year);
+
+            return new DateTime(p[4].Value, p[3].Value, p[2].Value, p[1].Value, p[0].Value, 0);
+        }
+
+        /// <summary>
+        /// Gets an array of all date parts to construct a datetime instance.
+        /// </summary>
+        /// <returns>[min, hour, day, month, year]</returns>
+        private int?[] ToArray()
+        {
+            return new int?[] { this.Minute, this.Hour, this.Day, this.Month, this.Year };
+        }
+    }
+}

+ 41 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/IAction.cs

@@ -0,0 +1,41 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    public interface IAction
+    {
+        /// <summary>
+        /// Gets or sets the ID.
+        /// </summary>
+        string ID { get; set; }
+
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        string Title { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to launch the app.
+        /// </summary>
+        bool Launch { get; set; }
+    }
+}

+ 56 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Input.cs

@@ -0,0 +1,56 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    public sealed class Input : IAction
+    {
+        /// <summary>
+        /// Gets or sets the ID.
+        /// </summary>
+        public string ID { get; set; }
+
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        public string Title { get; set; }
+
+        /// <summary>
+        /// Gets or sets the title of the submit button.
+        /// </summary>
+        public string SubmitTitle { get; set; }
+
+        /// <summary>
+        /// Gets or sets placeholder text.
+        /// </summary>
+        public string EmptyText { get; set; }
+
+        /// <summary>
+        /// Gets or sets the default text.
+        /// </summary>
+        public string DefaultValue { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to launch the app.
+        /// </summary>
+        public bool Launch { get; set; } = true;
+    }
+}

+ 432 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Manager.cs

@@ -0,0 +1,432 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    using System.Collections.Generic;
+    using System.Runtime.InteropServices.WindowsRuntime;
+    using Windows.UI.Notifications;
+
+    internal class Manager
+    {
+        /// <summary>
+        /// Gets a value indicating whether the permission to schedule notifications is enabled.
+        /// </summary>
+        /// <returns>True if settings are enabled</returns>
+        public bool Enabled => ToastNotifier.Setting == NotificationSetting.Enabled;
+
+        /// <summary>
+        /// Gets the default toast notifier.
+        /// </summary>
+        internal static ToastNotifier ToastNotifier => ToastNotificationManager.CreateToastNotifier();
+
+        /// <summary>
+        /// Schedule notifications.
+        /// </summary>
+        /// <param name="notifications">List of key-value properties</param>
+        public void Schedule([ReadOnlyArray] Options[] notifications)
+        {
+            foreach (Options options in notifications)
+            {
+                var builder = new Builder(options);
+
+                do
+                {
+                    var toast = builder.Build();
+
+                    if (toast == null)
+                    {
+                        break;
+                    }
+
+                    ToastNotifier.AddToSchedule(toast);
+                    builder.MoveNext();
+                }
+                while (builder.HasNext());
+            }
+        }
+
+        /// <summary>
+        /// Update notifications.
+        /// </summary>
+        /// <param name="notifications">List of key-value properties</param>
+        public void Update([ReadOnlyArray] Options[] notifications)
+        {
+            foreach (Options options in notifications)
+            {
+                var bar = options.ProgressBar;
+
+                if (!bar.Enabled)
+                {
+                    continue;
+                }
+
+                var data = new NotificationData { SequenceNumber = 0 };
+
+                data.Values["progress.value"] = bar.Value.ToString();
+                data.Values["progress.status"] = bar.Status.ToString();
+                data.Values["progress.description"] = bar.Description.ToString();
+
+                ToastNotifier.Update(data, options.Id.ToString());
+            }
+        }
+
+        /// <summary>
+        /// Clear the notifications specified by id.
+        /// </summary>
+        /// <param name="ids">The IDs of the notification to clear.</param>
+        /// <returns>The cleared notifications.</returns>
+        public List<Options> Clear(int[] ids)
+        {
+            var triggered = ToastNotificationManager.History.GetHistory();
+            var tags = new List<int>(ids);
+            var toasts = new List<Options>();
+
+            foreach (var toast in triggered)
+            {
+                var id = int.Parse(toast.Tag);
+
+                if (tags.Contains(id))
+                {
+                    tags.Remove(id);
+                    toasts.Add(new Notification(toast).Options);
+                    ToastNotificationManager.History.Remove(toast.Tag);
+                }
+
+                if (tags.Count == 0)
+                {
+                    break;
+                }
+            }
+
+            return toasts;
+        }
+
+        /// <summary>
+        /// Clear all notifications.
+        /// </summary>
+        public void ClearAll()
+        {
+            ToastNotificationManager.History.Clear();
+        }
+
+        /// <summary>
+        /// Cancel the notifications specified by id.
+        /// </summary>
+        /// <param name="ids">The IDs of the notification to clear.</param>
+        /// <returns>The cleared notifications.</returns>
+        public List<Options> Cancel(int[] ids)
+        {
+            var scheduled = ToastNotifier.GetScheduledToastNotifications();
+            var toRemove = new List<int>(ids);
+            var toClear = new List<int>(ids);
+            var toasts = new List<Options>();
+
+            foreach (var toast in scheduled)
+            {
+                var id = int.Parse(toast.Tag);
+
+                if (toRemove.Contains(id))
+                {
+                    toClear.Remove(id);
+                    ToastNotifier.RemoveFromSchedule(toast);
+
+                    if (!this.IsPhantom(toast))
+                    {
+                        toasts.Add(new Notification(toast).Options);
+                    }
+                }
+            }
+
+            if (toClear.Count > 0)
+            {
+                toasts.AddRange(this.Clear(toClear.ToArray()));
+            }
+
+            return toasts;
+        }
+
+        /// <summary>
+        /// Cancel all notifications.
+        /// </summary>
+        public void CancelAll()
+        {
+            var toasts = ToastNotifier.GetScheduledToastNotifications();
+
+            foreach (var toast in toasts)
+            {
+                ToastNotifier.RemoveFromSchedule(toast);
+            }
+
+            this.ClearAll();
+        }
+
+        /// <summary>
+        /// Gets all notifications by id.
+        /// </summary>
+        /// <returns>List of ids</returns>
+        public List<int> GetIds()
+        {
+            return this.GetIdsByType(Notification.Type.All);
+        }
+
+        /// <summary>
+        /// Gets all notifications by id for given type.
+        /// </summary>
+        /// <param name="type">The type of notification.</param>
+        /// <returns>List of ids</returns>
+        public List<int> GetIdsByType(Notification.Type type)
+        {
+            var ids = new List<int>();
+
+            if (type == Notification.Type.All || type == Notification.Type.Scheduled)
+            {
+                var toasts = ToastNotifier.GetScheduledToastNotifications();
+
+                foreach (var toast in toasts)
+                {
+                    if (!this.IsPhantom(toast))
+                    {
+                        ids.Add(int.Parse(toast.Tag));
+                    }
+                }
+            }
+
+            if (type == Notification.Type.All || type == Notification.Type.Triggered)
+            {
+                var toasts = ToastNotificationManager.History.GetHistory();
+
+                foreach (var toast in toasts)
+                {
+                    if (!this.IsPhantom(toast))
+                    {
+                        ids.Add(int.Parse(toast.Tag));
+                    }
+                }
+            }
+
+            return ids;
+        }
+
+        /// <summary>
+        /// Gets all notifications.
+        /// </summary>
+        /// <returns>A list of all triggered and scheduled notifications.</returns>
+        public List<Notification> GetAll()
+        {
+            return this.GetByType(Notification.Type.All);
+        }
+
+        /// <summary>
+        /// Gets all notifications of given type.
+        /// </summary>
+        /// <param name="type">The type of notification.</param>
+        /// <returns>A list of notifications.</returns>
+        public List<Notification> GetByType(Notification.Type type)
+        {
+            var notifications = new List<Notification>();
+
+            if (type == Notification.Type.All || type == Notification.Type.Scheduled)
+            {
+                var toasts = ToastNotifier.GetScheduledToastNotifications();
+
+                foreach (var toast in toasts)
+                {
+                    if (!this.IsPhantom(toast))
+                    {
+                        notifications.Add(new Notification(toast));
+                    }
+                }
+            }
+
+            if (type == Notification.Type.All || type == Notification.Type.Triggered)
+            {
+                var toasts = ToastNotificationManager.History.GetHistory();
+
+                foreach (var toast in toasts)
+                {
+                    if (!this.IsPhantom(toast))
+                    {
+                        notifications.Add(new Notification(toast));
+                    }
+                }
+            }
+
+            return notifications;
+        }
+
+        /// <summary>
+        /// Gets all notifications.
+        /// </summary>
+        /// <returns>A list of notification options instances.</returns>
+        public List<Options> GetOptions()
+        {
+            return this.GetOptionsByType(Notification.Type.All);
+        }
+
+        /// <summary>
+        /// Gets notifications specified by ID.
+        /// </summary>
+        /// <param name="ids">Optional list of IDs to find.</param>
+        /// <returns>A list of notification options instances.</returns>
+        public List<Options> GetOptions(int[] ids)
+        {
+            var options = new List<Options>();
+
+            foreach (var toast in this.Get(ids))
+            {
+                options.Add(toast.Options);
+            }
+
+            return options;
+        }
+
+        /// <summary>
+        /// Gets all notifications for given type.
+        /// </summary>
+        /// <param name="type">The type of notification.</param>
+        /// <returns>A list of notification options instances.</returns>
+        public List<Options> GetOptionsByType(Notification.Type type)
+        {
+            var options = new List<Options>();
+            var toasts = this.GetByType(type);
+
+            foreach (var toast in toasts)
+            {
+                options.Add(toast.Options);
+            }
+
+            return options;
+        }
+
+        /// <summary>
+        /// Gets the notifications specified by ID.
+        /// </summary>
+        /// <param name="ids">List of IDs to find.</param>
+        /// <returns>List of found notifications.</returns>
+        public List<Notification> Get(int[] ids)
+        {
+            var toasts = new List<Notification>();
+
+            foreach (var id in ids)
+            {
+                var toast = this.Get(id);
+
+                if (toast != null)
+                {
+                    toasts.Add(toast);
+                }
+            }
+
+            return toasts;
+        }
+
+        /// <summary>
+        /// Gets the notification by ID.
+        /// </summary>
+        /// <param name="id">The ID of the notification to find.</param>
+        /// <returns>The found instance or null.</returns>
+        public Notification Get(int id)
+        {
+            return this.Get(id.ToString());
+        }
+
+        /// <summary>
+        /// Gets the notification by ID.
+        /// </summary>
+        /// <param name="id">The ID of the notification to find.</param>
+        /// <returns>The found instance or null.</returns>
+        public Notification Get(string id)
+        {
+            var scheduled = ToastNotifier.GetScheduledToastNotifications();
+
+            foreach (var toast in scheduled)
+            {
+                if (toast.Tag == id)
+                {
+                    return new Notification(toast);
+                }
+            }
+
+            var triggered = ToastNotificationManager.History.GetHistory();
+
+            foreach (var toast in triggered)
+            {
+                if (toast.Tag == id)
+                {
+                    return new Notification(toast);
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the type (scheduled, triggered or unknown).
+        /// </summary>
+        /// <param name="id">The ID of the notification to find for.</param>
+        /// <returns>The type of the notification or unknown type.</returns>
+        public Notification.Type GetType(string id)
+        {
+            var scheduled = ToastNotifier.GetScheduledToastNotifications();
+
+            foreach (var toast in scheduled)
+            {
+                if (toast.Tag == id)
+                {
+                    return Notification.Type.Scheduled;
+                }
+            }
+
+            var triggered = ToastNotificationManager.History.GetHistory();
+
+            foreach (var toast in triggered)
+            {
+                if (toast.Tag == id)
+                {
+                    return Notification.Type.Triggered;
+                }
+            }
+
+            return Notification.Type.Unknown;
+        }
+
+        /// <summary>
+        /// If the specified toast is only for internal reason.
+        /// </summary>
+        /// <param name="toast">The toast notification to test for.</param>
+        /// <returns>True if its an internal ("phantom") one.</returns>
+        private bool IsPhantom(ToastNotification toast)
+        {
+            return toast.Group.Length > 0;
+        }
+
+        /// <summary>
+        /// If the specified toast is only for internal reason.
+        /// </summary>
+        /// <param name="toast">The toast notification to test for.</param>
+        /// <returns>True if its an internal ("phantom") one.</returns>
+        private bool IsPhantom(ScheduledToastNotification toast)
+        {
+            return toast.Group.Length > 0;
+        }
+    }
+}

+ 308 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Notification.cs

@@ -0,0 +1,308 @@
+namespace LocalNotificationProxy.LocalNotification
+{
+    using System;
+    using System.Collections.Generic;
+    using Microsoft.Toolkit.Uwp.Notifications;
+    using Windows.UI.Notifications;
+
+    internal class Notification
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Notification"/> class.
+        /// </summary>
+        /// <param name="options">The options hash map from JS side.</param>
+        public Notification(Options options)
+        {
+            this.Options = options;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Notification"/> class.
+        /// </summary>
+        /// <param name="xml">The options as a xml string.</param>
+        public Notification(string xml)
+        {
+            this.Options = Options.Parse(xml);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Notification"/> class.
+        /// </summary>
+        /// <param name="toast">The options as a toast object.</param>
+        public Notification(ScheduledToastNotification toast)
+        {
+            this.Options = Options.Parse(toast.Content.GetXml());
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Notification"/> class.
+        /// </summary>
+        /// <param name="toast">The options as a toast object.</param>
+        public Notification(ToastNotification toast)
+        {
+            var xml = toast.Content.DocumentElement.GetAttribute("launch");
+            this.Options = Options.Parse(xml);
+        }
+
+        public enum Type
+        {
+            All, Scheduled, Triggered, Unknown
+        }
+
+        /// <summary>
+        /// Gets the wrapped notification options.
+        /// </summary>
+        public Options Options { get; private set; }
+
+        /// <summary>
+        /// Gets the unique identifier for the toast.
+        /// </summary>
+        public string Id => $"{this.Options.Id}#{this.Options.Trigger.Occurrence}";
+
+        /// <summary>
+        /// Gets a ToastAudio object based on the specified sound uri.
+        /// </summary>
+        public ToastAudio Sound
+        {
+            get
+            {
+                var sound = new ToastAudio();
+                var path = this.Options.Sound;
+
+                if (path == null || path.Length == 0 || path.Equals("false"))
+                {
+                    sound.Silent = true;
+                }
+                else
+                if (path.StartsWith("file:///") || path.StartsWith("http"))
+                {
+                    sound.Src = new Uri(path, UriKind.Absolute);
+                }
+                else
+                if (path.StartsWith("file://"))
+                {
+                    sound.Src = new Uri(path.Replace("file:/", "ms-appx:///www"));
+                }
+                else
+                if (path.StartsWith("res://"))
+                {
+                    sound.Src = new Uri(path.Replace("res://", "ms-winsoundevent:notification."));
+                }
+                else
+                if (path.StartsWith("app://"))
+                {
+                    sound.Src = new Uri(path.Replace("app:/", "ms-appdata://"));
+                }
+
+                return sound;
+            }
+        }
+
+        /// <summary>
+        /// Gets a GenericAppLogo object based on the specified icon uri.
+        /// </summary>
+        public ToastGenericAppLogo Icon
+        {
+            get
+            {
+                var image = new ToastGenericAppLogo();
+                var path = this.Options.Icon;
+
+                if (path == null || path.StartsWith("res://logo"))
+                {
+                    image.Source = string.Empty;
+                }
+                else
+                if (path.StartsWith("file:///") || path.StartsWith("http"))
+                {
+                    image.Source = path;
+                }
+                else
+                if (path.StartsWith("file://"))
+                {
+                    image.Source = path.Replace("file:/", "ms-appx:///www");
+                }
+                else
+                if (path.StartsWith("res://"))
+                {
+                    image.Source = path.Replace("res://", "ms-appx:///images");
+                }
+                else
+                if (path.StartsWith("app://"))
+                {
+                    image.Source = path.Replace("app:/", "ms-appdata://local");
+                }
+                else
+                {
+                    image.Source = string.Empty;
+                }
+
+                if (image.Source.EndsWith("?crop=none"))
+                {
+                    image.HintCrop = ToastGenericAppLogoCrop.None;
+                }
+                else
+                if (image.Source.EndsWith("?crop=cirlce"))
+                {
+                    image.HintCrop = ToastGenericAppLogoCrop.Circle;
+                }
+
+                return image;
+            }
+        }
+
+        /// <summary>
+        /// Gets the parsed image attachments.
+        /// </summary>
+        public List<AdaptiveImage> Attachments
+        {
+            get
+            {
+                var images = new List<AdaptiveImage>();
+
+                if (this.Options.Attachments == null)
+                {
+                    return images;
+                }
+
+                foreach (string path in this.Options.Attachments)
+                {
+                    var image = new AdaptiveImage();
+
+                    if (path.StartsWith("file:///") || path.StartsWith("http"))
+                    {
+                        image.Source = path;
+                    }
+                    else
+                    if (path.StartsWith("file://"))
+                    {
+                        image.Source = path.Replace("file:/", "ms-appx:///www");
+                    }
+                    else
+                    if (path.StartsWith("res://"))
+                    {
+                        image.Source = path.Replace("res://", "ms-appx:///images");
+                    }
+                    else
+                    if (path.StartsWith("app://"))
+                    {
+                        image.Source = path.Replace("app:/", "ms-appdata://local");
+                    }
+
+                    if (image.Source != null)
+                    {
+                        images.Add(image);
+                    }
+                }
+
+                return images;
+            }
+        }
+
+        /// <summary>
+        /// Gets all toast buttons.
+        /// </summary>
+        public List<ToastButton> Buttons
+        {
+            get
+            {
+                var buttons = new List<ToastButton>();
+
+                foreach (var action in this.Options.Actions)
+                {
+                    if (action is Button)
+                    {
+                        buttons.Add(new ToastButton(action.Title, this.Options.GetXml(action.ID))
+                        {
+                            ActivationType = action.Launch ? ToastActivationType.Foreground : ToastActivationType.Background
+                        });
+                    }
+                    else if (action is Input && (action as Input).SubmitTitle != null)
+                    {
+                        var input = action as Input;
+
+                        buttons.Add(new ToastButton(input.SubmitTitle, this.Options.GetXml(input.ID))
+                        {
+                            ActivationType = input.Launch ? ToastActivationType.Foreground : ToastActivationType.Background,
+                            TextBoxId = input.ID
+                        });
+                    }
+                }
+
+                return buttons;
+            }
+        }
+
+        /// <summary>
+        /// Gets all toast inputs.
+        /// </summary>
+        public List<ToastTextBox> Inputs
+        {
+            get
+            {
+                var inputs = new List<ToastTextBox>();
+
+                foreach (var action in this.Options.Actions)
+                {
+                    if (!(action is Input))
+                    {
+                        continue;
+                    }
+
+                    inputs.Add(new ToastTextBox(action.ID)
+                    {
+                        Title = action.Title,
+                        PlaceholderContent = (action as Input).EmptyText,
+                        DefaultInput = (action as Input).DefaultValue
+                    });
+                }
+
+                return inputs;
+            }
+        }
+
+        /// <summary>
+        /// Gets the progress bar widget.
+        /// </summary>
+        public AdaptiveProgressBar ProgressBar
+        {
+            get
+            {
+                var bar = this.Options.ProgressBar;
+
+                if (!bar.Enabled)
+                {
+                    return null;
+                }
+
+                return new AdaptiveProgressBar()
+                {
+                    Title = bar.Title,
+                    Value = new BindableProgressBarValue("progress.value"),
+                    ValueStringOverride = new BindableString("progress.description"),
+                    Status = new BindableString("progress.status")
+                };
+            }
+        }
+
+        /// <summary>
+        /// Gets the date when to trigger the notification.
+        /// </summary>
+        public DateTime? Date
+        {
+            get
+            {
+                return this.Options.Trigger.Date;
+            }
+        }
+
+        /// <summary>
+        /// Gets the instance as an serialized xml element.
+        /// </summary>
+        /// <returns>Element with all property values set as attributes.</returns>
+        public string GetXml()
+        {
+            return this.Options.GetXml();
+        }
+    }
+}

+ 218 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Options.cs

@@ -0,0 +1,218 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    using Windows.Data.Xml.Dom;
+
+    public sealed class Options
+    {
+        /// <summary>
+        /// Gets notification event.
+        /// </summary>
+        public string Action { get; private set; } = "click";
+
+        /// <summary>
+        /// Gets or sets notification ID.
+        /// </summary>
+        public int Id { get; set; } = 0;
+
+        /// <summary>
+        /// Gets or sets notification title.
+        /// </summary>
+        public string Title { get; set; }
+
+        /// <summary>
+        /// Gets or sets notification text.
+        /// </summary>
+        public string Text { get; set; }
+
+        /// <summary>
+        /// Gets or sets app badge number.
+        /// </summary>
+        public int Badge { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification sound.
+        /// </summary>
+        public string Sound { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification image.
+        /// </summary>
+        public string Icon { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the popup shall be visible.
+        /// </summary>
+        public bool Silent { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets the notification trigger.
+        /// </summary>
+        public Trigger Trigger { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification user data.
+        /// </summary>
+        public string Data { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification attachments.
+        /// </summary>
+        public string[] Attachments { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification actions.
+        /// </summary>
+        public IAction[] Actions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification progress bar.
+        /// </summary>
+        public ProgressBar ProgressBar { get; set; }
+
+        /// <summary>
+        /// Deserializes the XML string into an instance of Options.
+        /// </summary>
+        /// <param name="identifier">The serialized instance of Options as an xml string.</param>
+        /// <returns>An instance where all properties have been assigned.</returns>
+        public static Options Parse(string identifier)
+        {
+            var doc = new XmlDocument();
+            doc.LoadXml(identifier);
+
+            var options = new Options();
+            var node = doc.DocumentElement;
+
+            options.Id = int.Parse(node.GetAttribute("id"));
+            options.Badge = int.Parse(node.GetAttribute("badge"));
+            options.Trigger = Trigger.Parse(node.GetAttribute("trigger"));
+
+            if (node.GetAttributeNode("text") != null)
+            {
+                options.Text = node.GetAttribute("text");
+            }
+
+            if (node.GetAttributeNode("title") != null)
+            {
+                options.Title = node.GetAttribute("title");
+            }
+
+            if (node.GetAttributeNode("sound") != null)
+            {
+                options.Sound = node.GetAttribute("sound");
+            }
+
+            if (node.GetAttributeNode("image") != null)
+            {
+                options.Icon = node.GetAttribute("image");
+            }
+
+            if (node.GetAttributeNode("data") != null)
+            {
+                options.Data = node.GetAttribute("data");
+            }
+
+            if (node.GetAttributeNode("attachments") != null)
+            {
+                options.Attachments = node.GetAttribute("attachments").Split(',');
+            }
+
+            if (node.GetAttributeNode("silent") != null)
+            {
+                options.Silent = true;
+            }
+
+            if (node.GetAttributeNode("action") != null)
+            {
+                options.Action = node.GetAttribute("action");
+            }
+
+            return options;
+        }
+
+        /// <summary>
+        /// Gets the instance as an serialized xml element.
+        /// </summary>
+        /// <returns>Element with all property values set as attributes.</returns>
+        public string GetXml()
+        {
+            return this.GetXml(null);
+        }
+
+        /// <summary>
+        /// Gets the instance as an serialized xml element.
+        /// </summary>
+        /// <param name="action">Optional (internal) event name.</param>
+        /// <returns>Element with all property values set as attributes.</returns>
+        internal string GetXml(string action)
+        {
+            var node = new XmlDocument().CreateElement("options");
+
+            node.SetAttribute("id", this.Id.ToString());
+            node.SetAttribute("badge", this.Badge.ToString());
+            node.SetAttribute("trigger", this.Trigger.GetXml());
+
+            if (this.Title != null)
+            {
+                node.SetAttribute("title", this.Title);
+            }
+
+            if (this.Text != null)
+            {
+                node.SetAttribute("text", this.Text);
+            }
+
+            if (this.Sound != null)
+            {
+                node.SetAttribute("sound", this.Sound);
+            }
+
+            if (this.Icon != null)
+            {
+                node.SetAttribute("image", this.Icon);
+            }
+
+            if (this.Data != null)
+            {
+                node.SetAttribute("data", this.Data);
+            }
+
+            if (this.Attachments != null)
+            {
+                node.SetAttribute("attachments", string.Join(",", this.Attachments));
+            }
+
+            if (this.Silent)
+            {
+                node.SetAttribute("silent", "1");
+            }
+
+            if (action != null)
+            {
+                node.SetAttribute("action", action);
+            }
+
+            return node.GetXml();
+        }
+    }
+}

+ 51 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/ProgressBar.cs

@@ -0,0 +1,51 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    public sealed class ProgressBar
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether the notification has a progress bar.
+        /// </summary>
+        public bool Enabled { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        public string Title { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the value.
+        /// </summary>
+        public double Value { get; set; } = 0;
+
+        /// <summary>
+        /// Gets or sets the status.
+        /// </summary>
+        public string Status { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the description.
+        /// </summary>
+        public string Description { get; set; } = string.Empty;
+    }
+}

+ 357 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotification/Trigger.cs

@@ -0,0 +1,357 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy.LocalNotification
+{
+    using System;
+    using Windows.Data.Xml.Dom;
+
+    public sealed class Trigger
+    {
+        private DateTime? triggerDate;
+
+        /// <summary>
+        /// Gets the trigger type.
+        /// </summary>
+        public string Type { get; } = "calendar";
+
+        /// <summary>
+        /// Gets or sets the trigger date.
+        /// </summary>
+        public long At { get; set; } = 0;
+
+        /// <summary>
+        /// Gets or sets the relative trigger date from now.
+        /// </summary>
+        public int In { get; set; } = 0;
+
+        /// <summary>
+        /// Gets or sets the trigger count.
+        /// </summary>
+        public int Count { get; set; } = 1;
+
+        /// <summary>
+        /// Gets the trigger occurrence.
+        /// </summary>
+        public int Occurrence { get; internal set; } = 1;
+
+        /// <summary>
+        /// Gets or sets the trigger interval.
+        /// </summary>
+        public object Every { get; set; }
+
+        /// <summary>
+        /// Gets or sets the trigger unit.
+        /// </summary>
+        public string Unit { get; set; } = "second";
+
+        /// <summary>
+        /// Gets the date when to trigger the notification.
+        /// </summary>
+        internal DateTime? Date
+        {
+            get
+            {
+                if (!this.triggerDate.HasValue)
+                {
+                    this.triggerDate = this.Every is Every ? this.GetRelDate() : this.GetFixDate();
+                }
+
+                if (!this.triggerDate.HasValue)
+                {
+                    return null;
+                }
+
+                var date = this.GetNextTriggerDate();
+                var minDate = DateTime.Now.AddSeconds(0.2);
+
+                if (!date.HasValue)
+                {
+                    return null;
+                }
+
+                if (date >= minDate)
+                {
+                    return date;
+                }
+
+                if ((minDate - date).Value.TotalMinutes <= 1)
+                {
+                    return minDate;
+                }
+
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Deserializes the XML string into an instance of Trigger.
+        /// </summary>
+        /// <param name="xml">The serialized instance of Options as an xml string.</param>
+        /// <returns>An instance where all properties have been assigned.</returns>
+        internal static Trigger Parse(string xml)
+        {
+            var doc = new XmlDocument();
+            doc.LoadXml(xml);
+
+            var trigger = new Trigger();
+            var node = doc.DocumentElement;
+
+            trigger.At = int.Parse(node.GetAttribute("at"));
+            trigger.In = int.Parse(node.GetAttribute("in"));
+            trigger.Unit = node.GetAttribute("unit");
+            trigger.Count = int.Parse(node.GetAttribute("count"));
+            trigger.Occurrence = int.Parse(node.GetAttribute("occurrence"));
+
+            if (node.GetAttributeNode("every") != null)
+            {
+                trigger.Every = node.GetAttribute("every");
+            }
+
+            return trigger;
+        }
+
+        /// <summary>
+        /// Gets the instance as an serialized xml element.
+        /// </summary>
+        /// <returns>Element with all property values set as attributes.</returns>
+        internal string GetXml()
+        {
+            var node = new XmlDocument().CreateElement("trigger");
+
+            node.SetAttribute("at", this.At.ToString());
+            node.SetAttribute("in", this.In.ToString());
+            node.SetAttribute("unit", this.Unit);
+            node.SetAttribute("count", this.Count.ToString());
+            node.SetAttribute("occurrence", this.Occurrence.ToString());
+
+            if (this.Every != null && !(this.Every is Every))
+            {
+                node.SetAttribute("every", this.Every.ToString());
+            }
+
+            return node.GetXml();
+        }
+
+        /// <summary>
+        /// Adds the interval to the specified date.
+        /// </summary>
+        /// <param name="date">The date where to add the interval of ticks</param>
+        /// <param name="interval">minute, hour, day, ...</param>
+        /// <param name="ticks">The number of minutes, hours, days, ...</param>
+        /// <returns>A new datetime instance</returns>
+        private DateTime? AddInterval(DateTime date, string interval, int ticks)
+        {
+            switch (interval)
+            {
+                case null:
+                case "":
+                    return null;
+                case "second":
+                    return DateTime.Now.AddSeconds(ticks);
+                case "minute":
+                    return DateTime.Now.AddMinutes(ticks);
+                case "hour":
+                    return DateTime.Now.AddHours(ticks);
+                case "day":
+                    return DateTime.Now.AddDays(ticks);
+                case "week":
+                    return DateTime.Now.AddDays(ticks * 7);
+                case "month":
+                    return DateTime.Now.AddMonths(ticks);
+                case "quarter":
+                    return DateTime.Now.AddMonths(ticks * 3);
+                case "year":
+                    return DateTime.Now.AddYears(ticks);
+                default:
+                    return null;
+            }
+        }
+
+        /// <summary>
+        /// Gets the date when to trigger the notification.
+        /// </summary>
+        /// <returns>The fix date specified by trigger.at or trigger.in</returns>
+        private DateTime? GetFixDate()
+        {
+            if (this.At == 0)
+            {
+                return this.AddInterval(DateTime.Now, this.Unit, this.In);
+            }
+
+            return DateTimeOffset.FromUnixTimeMilliseconds(this.At * 1000).LocalDateTime;
+        }
+
+        /// <summary>
+        /// Gets the date when to trigger the notification.
+        /// </summary>
+        /// <returns>The first matching date specified by trigger.every</returns>
+        private DateTime? GetRelDate()
+        {
+            return this.GetRelDate(DateTime.Now);
+        }
+
+        /// <summary>
+        /// Gets the date when to trigger the notification.
+        /// </summary>
+        /// <param name="now">The relative date to calculate the date from.</param>
+        /// <returns>The first matching date specified by trigger.every</returns>
+        private DateTime? GetRelDate(DateTime now)
+        {
+            var every = this.Every as Every;
+            var date = every.ToDateTime(now);
+
+            if (date >= now)
+            {
+                return date;
+            }
+
+            if (every.Interval == null || date.Year < now.Year)
+            {
+                return null;
+            }
+
+            if (date.Month < now.Month)
+            {
+                switch (every.Interval)
+                {
+                    case "minute":
+                    case "hour":
+                    case "day":
+                        if (!every.Year.HasValue)
+                        {
+                            return date.AddYears(now.Year - date.Year + 1);
+                        }
+
+                        break;
+                    case "year":
+                        return date.AddYears(now.Year - date.Year + 1);
+                }
+            }
+            else if (date.Day < now.Day)
+            {
+                switch (every.Interval)
+                {
+                    case "minute":
+                    case "hour":
+                        if (!every.Month.HasValue)
+                        {
+                            return date.AddMonths(now.Month - date.Month + 1);
+                        }
+                        else if (!every.Year.HasValue)
+                        {
+                            return date.AddYears(now.Year - date.Year + 1);
+                        }
+
+                        break;
+                    case "month":
+                        return date.AddMonths(now.Month - date.Month + 1);
+                    case "year":
+                        return date.AddYears(now.Year - date.Year + 1);
+                }
+            }
+            else if (date.Hour < now.Hour)
+            {
+                switch (every.Interval)
+                {
+                    case "minute":
+                        if (!every.Day.HasValue)
+                        {
+                            return date.AddDays(now.Day - date.Day + 1);
+                        }
+                        else if (!every.Month.HasValue)
+                        {
+                            return date.AddMonths(now.Month - date.Month + 1);
+                        }
+
+                        break;
+                    case "hour":
+                        return date.AddHours(now.Hour - date.Hour);
+                    case "day":
+                        return date.AddDays(now.Day - date.Day + 1);
+                    case "month":
+                        return date.AddMonths(now.Month - date.Month + 1);
+                    case "year":
+                        return date.AddYears(now.Year - date.Year + 1);
+                }
+            }
+            else if (date.Minute < now.Minute)
+            {
+                switch (every.Interval)
+                {
+                    case "minute":
+                        return date.AddMinutes(now.Minute - date.Minute + 1);
+                    case "hour":
+                        return date.AddHours(now.Hour - date.Hour + 1);
+                    case "day":
+                        return date.AddDays(now.Day - date.Day + 1);
+                    case "month":
+                        return date.AddMonths(now.Month - date.Month + 1);
+                    case "year":
+                        return date.AddYears(now.Year - date.Year + 1);
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Calculates the next trigger date by adding (interval * occurence)
+        /// </summary>
+        /// <returns>The next valid trigger date</returns>
+        private DateTime? GetNextTriggerDate()
+        {
+            var date = this.triggerDate.Value;
+            var multiple = this.Occurrence - 1;
+
+            if (multiple == 0)
+            {
+                return date;
+            }
+
+            var every = this.Every is Every ? (this.Every as Every).Interval : this.Every;
+            DateTime? nextDate;
+
+            if (every is double)
+            {
+                var ticks = Convert.ToInt32(every);
+
+                if (ticks == 0)
+                {
+                    return null;
+                }
+
+                nextDate = this.AddInterval(date, this.Unit, multiple * ticks);
+            }
+            else
+            {
+                nextDate = this.AddInterval(date, every as string, multiple);
+
+                if (nextDate.HasValue && this.Every is Every)
+                {
+                    nextDate = this.GetRelDate(nextDate.Value);
+                }
+            }
+
+            return nextDate;
+        }
+    }
+}

+ 181 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotificationProxy.cs

@@ -0,0 +1,181 @@
+/*
+ * Apache 2.0 License
+ *
+ * Copyright (c) Sebastian Katzer 2017
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apache License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ */
+
+namespace LocalNotificationProxy
+{
+    using System.Runtime.InteropServices.WindowsRuntime;
+    using LocalNotification;
+
+    public sealed class LocalNotificationProxy
+    {
+        /// <summary>
+        /// Manager wrapps the native SDK methods.
+        /// </summary>
+        private Manager manager = new Manager();
+
+        /// <summary>
+        /// Check permission to schedule notifications.
+        /// </summary>
+        /// <returns>True if settings are enabled</returns>
+        public bool HasPermission()
+        {
+            return this.manager.Enabled;
+        }
+
+        /// <summary>
+        /// Schedule notifications.
+        /// </summary>
+        /// <param name="notifications">List of key-value properties</param>
+        public void Schedule([ReadOnlyArray] Options[] notifications)
+        {
+            this.manager.Schedule(notifications);
+        }
+
+        /// <summary>
+        /// Update notifications.
+        /// </summary>
+        /// <param name="notifications">List of key-value properties</param>
+        public void Update([ReadOnlyArray] Options[] notifications)
+        {
+            this.manager.Update(notifications);
+        }
+
+        /// <summary>
+        /// Clear the notifications specified by id.
+        /// </summary>
+        /// <param name="ids">The IDs of the notification to clear.</param>
+        /// <returns>The cleared notifications.</returns>
+        public Options[] Clear([ReadOnlyArray] int[] ids)
+        {
+            return this.manager.Clear(ids).ToArray();
+        }
+
+        /// <summary>
+        /// Clear all notifications.
+        /// </summary>
+        public void ClearAll()
+        {
+            this.manager.ClearAll();
+        }
+
+        /// <summary>
+        /// Cancel the notifications specified by id.
+        /// </summary>
+        /// <param name="ids">The IDs of the notification to clear.</param>
+        /// <returns>The cleared notifications.</returns>
+        public Options[] Cancel([ReadOnlyArray] int[] ids)
+        {
+            return this.manager.Cancel(ids).ToArray();
+        }
+
+        /// <summary>
+        /// Cancel all notifications.
+        /// </summary>
+        public void CancelAll()
+        {
+            this.manager.CancelAll();
+        }
+
+        /// <summary>
+        /// Gets the type of the notification specified by ID.
+        /// </summary>
+        /// <param name="id">The ID of the notification to find for.</param>
+        /// <returns>The type (scheduled, triggered or unknown).</returns>
+        public string Type(int id)
+        {
+            return this.manager.GetType(id.ToString()).ToString().ToLower();
+        }
+
+        /// <summary>
+        /// List of all notifiation by id.
+        /// </summary>
+        /// <returns>List of numbers</returns>
+        public int[] Ids()
+        {
+            return this.manager.GetIds().ToArray();
+        }
+
+        /// <summary>
+        /// List of all scheduled notifiation by id.
+        /// </summary>
+        /// <returns>List of numbers</returns>
+        public int[] ScheduledIds()
+        {
+            return this.manager.GetIdsByType(Notification.Type.Scheduled).ToArray();
+        }
+
+        /// <summary>
+        /// List of all triggered notifiation by id.
+        /// </summary>
+        /// <returns>List of numbers</returns>
+        public int[] TriggeredIds()
+        {
+            return this.manager.GetIdsByType(Notification.Type.Triggered).ToArray();
+        }
+
+#pragma warning disable SA1300 // Element must begin with upper-case letter
+        /// <summary>
+        /// Gets a single notifiation specified by id.
+        /// </summary>
+        /// <param name="id">The ID of the notification to find.</param>
+        /// <returns>List of options instances</returns>
+        public Options notification(int id)
+        {
+            var toast = this.manager.Get(id);
+
+            return toast != null ? toast.Options : null;
+        }
+#pragma warning restore SA1300 // Element must begin with upper-case letter
+
+        /// <summary>
+        /// List of (all) notifiation specified by id.
+        /// </summary>
+        /// <param name="ids">Optional list of IDs to find.</param>
+        /// <returns>List of options instances</returns>
+        public Options[] Notifications([ReadOnlyArray] int[] ids)
+        {
+            if (ids == null || ids.Length == 0)
+            {
+                return this.manager.GetOptions().ToArray();
+            }
+
+            return this.manager.GetOptions(ids).ToArray();
+        }
+
+        /// <summary>
+        /// List of all scheduled notifiation.
+        /// </summary>
+        /// <returns>List of options instances</returns>
+        public Options[] ScheduledNotifications()
+        {
+            return this.manager.GetOptionsByType(Notification.Type.Scheduled).ToArray();
+        }
+
+        /// <summary>
+        /// List of all triggered notifiation.
+        /// </summary>
+        /// <returns>List of options instances</returns>
+        public Options[] TriggeredNotifications()
+        {
+            return this.manager.GetOptionsByType(Notification.Type.Triggered).ToArray();
+        }
+    }
+}

+ 151 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/LocalNotificationProxy.csproj

@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{F5B32CBA-8DAB-43E5-AE4F-98B3B1281FF1}</ProjectGuid>
+    <OutputType>winmdobj</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>LocalNotificationProxy</RootNamespace>
+    <AssemblyName>LocalNotificationProxy</AssemblyName>
+    <DefaultLanguage>de-DE</DefaultLanguage>
+    <TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
+    <TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.15063.0</TargetPlatformVersion>
+    <TargetPlatformMinVersion>10.0.15063.0</TargetPlatformMinVersion>
+    <MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <AllowCrossPlatformRetargeting>false</AllowCrossPlatformRetargeting>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
+    <PlatformTarget>x86</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <OutputPath>bin\x86\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <NoWarn>;2008</NoWarn>
+    <DebugType>full</DebugType>
+    <PlatformTarget>x86</PlatformTarget>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <ErrorReport>prompt</ErrorReport>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
+    <PlatformTarget>x86</PlatformTarget>
+    <OutputPath>bin\x86\Release\</OutputPath>
+    <DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <Optimize>true</Optimize>
+    <NoWarn>;2008</NoWarn>
+    <DebugType>pdbonly</DebugType>
+    <PlatformTarget>x86</PlatformTarget>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <ErrorReport>prompt</ErrorReport>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
+    <PlatformTarget>ARM</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <OutputPath>bin\ARM\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <NoWarn>;2008</NoWarn>
+    <DebugType>full</DebugType>
+    <PlatformTarget>ARM</PlatformTarget>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <ErrorReport>prompt</ErrorReport>
+    <AllowUnsafeBlocks>false</AllowUnsafeBlocks>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
+    <PlatformTarget>ARM</PlatformTarget>
+    <OutputPath>bin\ARM\Release\</OutputPath>
+    <DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <Optimize>true</Optimize>
+    <NoWarn>;2008</NoWarn>
+    <DebugType>pdbonly</DebugType>
+    <PlatformTarget>ARM</PlatformTarget>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <ErrorReport>prompt</ErrorReport>
+    <AllowUnsafeBlocks>false</AllowUnsafeBlocks>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+    <PlatformTarget>x64</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <OutputPath>bin\x64\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <NoWarn>;2008</NoWarn>
+    <DebugType>full</DebugType>
+    <PlatformTarget>x64</PlatformTarget>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <ErrorReport>prompt</ErrorReport>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+    <PlatformTarget>x64</PlatformTarget>
+    <OutputPath>bin\x64\Release\</OutputPath>
+    <DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
+    <Optimize>true</Optimize>
+    <NoWarn>;2008</NoWarn>
+    <DebugType>pdbonly</DebugType>
+    <PlatformTarget>x64</PlatformTarget>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <ErrorReport>prompt</ErrorReport>
+  </PropertyGroup>
+  <PropertyGroup>
+    <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="LocalNotificationProxy.cs" />
+    <Compile Include="LocalNotification\Builder.cs" />
+    <Compile Include="LocalNotification\Button.cs" />
+    <Compile Include="LocalNotification\Every.cs" />
+    <Compile Include="LocalNotification\Notification.cs" />
+    <Compile Include="LocalNotification\IAction.cs" />
+    <Compile Include="LocalNotification\Input.cs" />
+    <Compile Include="LocalNotification\Manager.cs" />
+    <Compile Include="LocalNotification\Options.cs" />
+    <Compile Include="LocalNotification\ProgressBar.cs" />
+    <Compile Include="LocalNotification\Trigger.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NETCore">
+      <Version>5.0.0</Version>
+    </PackageReference>
+    <PackageReference Include="Microsoft.NETCore.Portable.Compatibility">
+      <Version>1.0.1</Version>
+    </PackageReference>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Microsoft.Toolkit.Uwp.Notifications.UWP\Microsoft.Toolkit.Uwp.Notifications.UWP.csproj">
+      <Project>{fb381278-f4ad-4703-a12a-c43ee0b231bd}</Project>
+      <Name>Microsoft.Toolkit.Uwp.Notifications.UWP</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
+    <VisualStudioVersion>14.0</VisualStudioVersion>
+  </PropertyGroup>
+  <Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 29 - 0
src/windows/LocalNotificationProxy/LocalNotificationProxy/Properties/AssemblyInfo.cs

@@ -0,0 +1,29 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("LocalNotificationProxy")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("LocalNotificationProxy")]
+[assembly: AssemblyCopyright("Copyright ©  2017")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: ComVisible(false)]

+ 0 - 443
src/windows/LocalNotificationUtil.js

@@ -1,443 +0,0 @@
-/*
-    Copyright 2013-2015 appPlant UG
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-*/
-
-
-exports = require('de.appplant.cordova.plugin.local-notification.LocalNotification.Proxy').core;
-
-var channel = require('cordova/channel');
-
-
-/***********
- * MEMBERS *
- ***********/
-
-// True if App is running, false if suspended
-exports.isInBackground = true;
-
-// Indicates if the device is ready (to receive events)
-exports.isReady = false;
-
-// Queues all events before deviceready
-exports.eventQueue = [];
-
-/********
- * UTIL *
- ********/
-
-/**
- * The repeating interval in milliseconds.
- *
- * @param {String} interval
- *      A number or a placeholder like `minute`.
- *
- * @return {Number}
- *      Interval in milliseconds
- */
-exports.getRepeatInterval = function (every) {
-
-    if (!every)
-        return 0;
-
-    if (every == 'minute')
-        return 60000;
-
-    if (every == 'hour')
-        return 360000;
-
-    if (!NaN(every))
-        return parseInt(every) * 60000;
-
-    return 0;
-};
-
-/**
- * If the notification is repeating.
- *
- * @param {Object} notification
- *      Local notification object
- *
- * @return Boolean
- */
-exports.isRepeating = function (notification) {
-    return this.getRepeatInterval(notification.every) !== 0;
-};
-
-/**
- * Parses sound file path.
- *
- * @param {String} path
- *      Relative path to sound resource
- *
- * @return {String} XML Tag for Sound-File
- */
-exports.parseSound = function (path) {
-	if (!path.match(/^file/))
-		return '';
-
-	var uri = this.parseUri(path),
-		audio = "<audio src=" + uri + " loop='false'/>";
-
-	return audio;
-};
-
-/**
- * Parses image file path.
- *
- * @param {String} path
- *      Relative path to image resource
- *
- * @return {String} XML-Tag for Image-File
- */
-exports.parseImage = function (path) {
-    if (!path.match(/^file/))
-        return '';
-
-    var uri = this.parseUri(path),
-        image = "<image id='1' src=" + uri + " />";
-
-    return image;
-};
-
-/**
- * Parses file path to URI.
- *
- * @param {String} path
- *      Relative path to a resource
- *
- * @return {String} URI to File
- */
-exports.parseUri = function (path) {
-    var pkg = Windows.ApplicationModel.Package.current,
-        pkgId = pkg.id,
-        pkgName = pkgId.name;
-
-	var uri = "'ms-appx://" + pkgName + "/www" + path.slice(6, path.length) + "'";
-
-	return uri;
-};
-
-/**
- * Builds the xml payload for a local notification based on its options.
- *
- * @param {Object} options
- *      Local notification properties
- *
- * @return Windows.Data.Xml.Dom.XmlDocument
- */
-exports.build = function (options) {
-    var template = this.buildToastTemplate(options),
-        notification = new Windows.Data.Xml.Dom.XmlDocument();
-
-    try {
-        notification.loadXml(template);
-    } catch (e) {
-        console.error(
-            'LocalNotification#schedule',
-            'Error loading the xml, check for invalid characters.');
-    }
-
-    // Launch Attribute to enable onClick event
-    var launchAttr = notification.createAttribute('launch'),
-        toastNode = notification.selectSingleNode('/toast');
-
-    launchAttr.value = options.id.toString();
-    toastNode.attributes.setNamedItem(launchAttr);
-
-    return notification;
-};
-
-/**
- * Builds the toast template with the right style depend on the options.
- *
- * @param {Object} options
- *      Local notification properties
- *
- * @return String
- */
-exports.buildToastTemplate = function (options) {
-	var title = options.title,
-		message = options.text || '',
-		json = JSON.stringify(options),
-		sound = '';
-
-	if (options.sound && options.sound !== '') {
-		sound = this.parseSound(options.sound);
-	}
-
-	var templateName = "ToastText",
-		imageNode;
-	if (options.icon && options.icon !== '') {
-		imageNode = this.parseImage(options.icon);
-		// template with Image
-		if (imageNode !== '') {
-			templateName = "ToastImageAndText";
-		}
-	} else {
-		imageNode = "";
-	}
-
-	var bindingNode;
-	if (title && title !== '') {
-		bindingNode = "<binding template='" + templateName + "02'>" +
-							imageNode +
-							"<text id='1'>" + title + "</text>" +
-							"<text id='2'>" + message + "</text>" +
-						"</binding>";
-	} else {
-		bindingNode = "<binding template='" + templateName + "01'>" +
-							imageNode +
-							"<text id='1'>" + message + "</text>" +
-						"</binding>";
-	}
-	return "<toast>" +
-				"<visual>" +
-						bindingNode +
-				"</visual>" +
-				sound +
-				"<json>" + json + "</json>" +
-			"</toast>";
-};
-
-/**
- * Short-hand method for the toast notification history.
- */
-exports.getToastHistory = function () {
-    return Windows.UI.Notifications.ToastNotificationManager.history;
-};
-
-/**
- * Gets a toast notifier instance.
- *
- * @return Object
- */
-exports.getToastNotifier = function () {
-    return Windows.UI.Notifications.ToastNotificationManager
-            .createToastNotifier();
-};
-
-/**
- * List of all scheduled toast notifiers.
- *
- * @return Array
- */
-exports.getScheduledToasts = function () {
-    return this.getToastNotifier().getScheduledToastNotifications();
-};
-
-/**
- * Gets the Id from the toast notifier.
- *
- * @param {Object} toast
- *      A toast notifier object
- *
- * @return String
- */
-exports.getToastId = function (toast) {
-    var id = toast.id;
-
-    if (id.match(/-2$/))
-        return id.match(/^[^-]+/)[0];
-
-    return id;
-};
-
-/**
- * Gets the notification life cycle type
- * (scheduled or triggered)
- *
- * @param {Object} toast
- *      A toast notifier object
- *
- * @return String
- */
-exports.getToastType = function (toast) {
-    return this.isToastTriggered(toast) ? 'triggered' : 'scheduled';
-};
-
-/**
- * If the toast is already scheduled.
- *
- * @param {Object} toast
- *      A toast notifier object
- *
- * @return Boolean
- */
-exports.isToastScheduled = function (toast) {
-    return !this.isToastTriggered(toast);
-};
-
-/**
- * If the toast is already triggered.
- *
- * @param {Object} toast
- *      A toast notifier object
- *
- * @return Boolean
- */
-exports.isToastTriggered = function (toast) {
-    var id = this.getToastId(toast),
-        notification = this.getAll([id])[0],
-        fireDate = new Date((notification.at) * 1000);
-
-    if (this.isRepeating(notification))
-        return false;
-
-    return fireDate <= new Date();
-};
-
-/**
- * Finds the toast by it's ID.
- *
- * @param {String} id
- *      Local notification ID
- *
- * @param Object
- */
-exports.findToastById = function (id) {
-    var toasts = this.getScheduledToasts();
-
-    for (var i = 0; i < toasts.length; i++) {
-        var toast = toasts[i];
-
-        if (this.getToastId(toast) == id)
-            return toast;
-    }
-
-    return null;
-};
-
-/**
- * Sets trigger event for local notification.
- *
- * @param {Object} notification
- *      Local notification object
- * @param {Function} callback
- *      Callback function
- */
-exports.callOnTrigger = function (notification, callback) {
-    var triggerTime = new Date((notification.at * 1000)),
-        interval = triggerTime - new Date();
-
-    if (interval <= 0) {
-        callback.call(this, notification);
-        return;
-    }
-
-    WinJS.Promise.timeout(interval).then(function () {
-        if (exports.isPresent(notification.id)) {
-            callback.call(exports, notification);
-        }
-    });
-};
-
-/**
- * Sets trigger event for all scheduled local notification.
- *
- * @param {Function} callback
- *      Callback function
- */
-exports.callOnTriggerForScheduled = function (callback) {
-    var notifications = this.getScheduled();
-
-    for (var i = 0; i < notifications.length; i++) {
-        this.callOnTrigger(notifications[i], callback);
-    }
-};
-
-/**
- * The application state - background or foreground.
- *
- * @return String
- */
-exports.getApplicationState = function () {
-    return this.isInBackground ? 'background' : 'foreground';
-};
-
-/**
- * Fires the event about a local notification.
- *
- * @param {String} event
- *      The event
- * @param {Object} notification
- *      The notification
- */
-exports.fireEvent = function (event, notification) {
-    var plugin = cordova.plugins.notification.local.core,
-        state = this.getApplicationState(),
-        args;
-
-    if (notification) {
-        args = [event, notification, state];
-    } else {
-        args = [event, state];
-    }
-
-    if (this.isReady && plugin) {
-        plugin.fireEvent.apply(plugin, args);
-    } else {
-        this.eventQueue.push(args);
-    }
-};
-
-
-/**************
- * LIFE CYCLE *
- **************/
-
-// Called before 'deviceready' event
-channel.onCordovaReady.subscribe(function () {
-    // Register trigger handler for each scheduled notification
-    exports.callOnTriggerForScheduled(function (notification) {
-        this.updateBadge(notification.badge);
-        this.fireEvent('trigger', notification);
-    });
-});
-
-// Handle onclick event
-document.addEventListener('activated', function (e) {
-    var id = e.args,
-        notification = exports.getAll([id])[0];
-
-    if (!notification)
-        return;
-
-    exports.clearLocalNotification(id);
-
-    var repeating = exports.isRepeating(notification);
-
-    exports.fireEvent('click', notification);
-    exports.fireEvent(repeating ? 'clear' : 'cancel', notification);
-}, false);
-
-// App is running in background
-document.addEventListener('pause', function () {
-    exports.isInBackground = true;
-}, false);
-
-// App is running in foreground
-document.addEventListener('resume', function () {
-    exports.isInBackground = false;
-}, false);
-
-// App is running in foreground
-document.addEventListener('deviceready', function () {
-    exports.isInBackground = false;
-}, false);

+ 46 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveGroup.cs

@@ -0,0 +1,46 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// Groups semantically identify that the content in the group must either be displayed as a whole, or not displayed if it cannot fit. Groups also allow creating multiple columns. Supported on Tiles since RTM. Supported on Toasts since Anniversary Update.
+    /// </summary>
+    public sealed class AdaptiveGroup : ITileBindingContentAdaptiveChild, IAdaptiveChild, IToastBindingGenericChild
+    {
+        /// <summary>
+        /// The only valid children of groups are <see cref="AdaptiveSubgroup"/>. Each subgroup is displayed as a separate vertical column. Note that you must include at least one subgroup in your group, otherwise an <see cref="InvalidOperationException"/> will be thrown when you try to retrieve the XML for the notification.
+        /// </summary>
+        public IList<AdaptiveSubgroup> Children { get; private set; } = new List<AdaptiveSubgroup>();
+
+        internal Element_AdaptiveGroup ConvertToElement()
+        {
+            if (Children.Count == 0)
+            {
+                throw new InvalidOperationException("Groups must have at least one child subgroup. The Children property had zero items in it.");
+            }
+
+            Element_AdaptiveGroup group = new Element_AdaptiveGroup();
+
+            foreach (var subgroup in Children)
+            {
+                group.Children.Add(subgroup.ConvertToElement());
+            }
+
+            return group;
+        }
+    }
+}

+ 49 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveHelper.cs

@@ -0,0 +1,49 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive
+{
+    internal static class AdaptiveHelper
+    {
+        internal static object ConvertToElement(object obj)
+        {
+            if (obj is AdaptiveText)
+            {
+                return (obj as AdaptiveText).ConvertToElement();
+            }
+
+            if (obj is AdaptiveImage)
+            {
+                return (obj as AdaptiveImage).ConvertToElement();
+            }
+
+            if (obj is AdaptiveGroup)
+            {
+                return (obj as AdaptiveGroup).ConvertToElement();
+            }
+
+            if (obj is AdaptiveSubgroup)
+            {
+                return (obj as AdaptiveSubgroup).ConvertToElement();
+            }
+
+            if (obj is AdaptiveProgressBar)
+            {
+                return (obj as AdaptiveProgressBar).ConvertToElement();
+            }
+
+            throw new NotImplementedException("Unknown object: " + obj.GetType());
+        }
+    }
+}

+ 89 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveImage.cs

@@ -0,0 +1,89 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// An inline image.
+    /// </summary>
+    public sealed class AdaptiveImage
+        : IBaseImage,
+        IToastBindingGenericChild,
+        ITileBindingContentAdaptiveChild,
+        IAdaptiveChild,
+        IAdaptiveSubgroupChild
+    {
+        /// <summary>
+        /// Control the desired cropping of the image. Supported on Tiles since RTM. Supported on Toast since Anniversary Update.
+        /// </summary>
+        public AdaptiveImageCrop HintCrop { get; set; }
+
+        /// <summary>
+        /// By default, images have an 8px margin around them. You can remove this margin by setting this property to true. Supported on Tiles since RTM. Supported on Toast since Anniversary Update.
+        /// </summary>
+        public bool? HintRemoveMargin { get; set; }
+
+        /// <summary>
+        /// The horizontal alignment of the image. For Toast, this is only supported when inside an <see cref="AdaptiveSubgroup"/>.
+        /// </summary>
+        public AdaptiveImageAlign HintAlign { get; set; }
+
+        private string _source;
+
+        /// <summary>
+        /// Required. The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+        /// </summary>
+        public string Source
+        {
+            get { return _source; }
+            set { BaseImageHelper.SetSource(ref _source, value); }
+        }
+
+        /// <summary>
+        /// A description of the image, for users of assistive technologies.
+        /// </summary>
+        public string AlternateText { get; set; }
+
+        /// <summary>
+        /// Set to true to allow Windows to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+        /// </summary>
+        public bool? AddImageQuery { get; set; }
+
+        /// <summary>
+        /// Returns the image's source string.
+        /// </summary>
+        /// <returns>The image's source string.</returns>
+        public override string ToString()
+        {
+            if (Source == null)
+            {
+                return "Source is null";
+            }
+
+            return Source;
+        }
+
+        internal Element_AdaptiveImage ConvertToElement()
+        {
+            Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this);
+
+            image.Crop = HintCrop;
+            image.RemoveMargin = HintRemoveMargin;
+            image.Align = HintAlign;
+            image.Placement = AdaptiveImagePlacement.Inline;
+
+            return image;
+        }
+    }
+}

+ 72 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveImageEnums.cs

@@ -0,0 +1,72 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// Specifies the horizontal alignment for an image.
+    /// </summary>
+    public enum AdaptiveImageAlign
+    {
+        /// <summary>
+        /// Default value, alignment behavior determined by renderer.
+        /// </summary>
+        Default,
+
+        /// <summary>
+        /// Image stretches to fill available width (and potentially available height too, depending on where the image is).
+        /// </summary>
+        [EnumString("stretch")]
+        Stretch,
+
+        /// <summary>
+        /// Align the image to the left, displaying the image at its native resolution.
+        /// </summary>
+        [EnumString("left")]
+        Left,
+
+        /// <summary>
+        /// Align the image in the center horizontally, displaying the image at its native resolution.
+        /// </summary>
+        [EnumString("center")]
+        Center,
+
+        /// <summary>
+        /// Align the image to the right, displaying the image at its native resolution.
+        /// </summary>
+        [EnumString("right")]
+        Right
+    }
+
+    /// <summary>
+    /// Specify the desired cropping of the image.
+    /// </summary>
+    public enum AdaptiveImageCrop
+    {
+        /// <summary>
+        /// Default value, cropping behavior determined by renderer.
+        /// </summary>
+        Default,
+
+        /// <summary>
+        /// Image is not cropped.
+        /// </summary>
+        [EnumString("none")]
+        None,
+
+        /// <summary>
+        /// Image is cropped to a circle shape.
+        /// </summary>
+        [EnumString("circle")]
+        Circle
+    }
+}

+ 108 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveProgressBar.cs

@@ -0,0 +1,108 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+#if WINRT
+using System.Collections.Generic;
+#endif
+using System;
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// New in Creators Update: A progress bar. Only supported on toasts on Desktop, build 15007 or newer.
+    /// </summary>
+    public sealed class AdaptiveProgressBar : IToastBindingGenericChild
+    {
+#if WINRT
+        /// <summary>
+        /// Gets a dictionary of the current data bindings, where you can assign new bindings.
+        /// </summary>
+        public IDictionary<AdaptiveProgressBarBindableProperty, string> Bindings { get; private set; } = new Dictionary<AdaptiveProgressBarBindableProperty, string>();
+#endif
+
+        /// <summary>
+        /// Gets or sets an optional title string. Supports data binding.
+        /// </summary>
+        public
+#if WINRT
+            string
+#else
+            BindableString
+#endif
+            Title { get; set; }
+
+        /// <summary>
+        /// Gets or sets the value of the progress bar. Supports data binding. Defaults to 0.
+        /// </summary>
+        public
+#if WINRT
+            AdaptiveProgressBarValue
+#else
+            BindableProgressBarValue
+#endif
+            Value { get; set; } = AdaptiveProgressBarValue.FromValue(0);
+
+        /// <summary>
+        /// Gets or sets an optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed.
+        /// </summary>
+        public
+#if WINRT
+            string
+#else
+            BindableString
+#endif
+            ValueStringOverride { get; set; }
+
+        /// <summary>
+        /// Required. Gets or sets a status string, which is displayed underneath the progress bar. This string should reflect the status of the operation, like "Downloading..." or "Installing..."
+        /// </summary>
+        public
+#if WINRT
+            string
+#else
+            BindableString
+#endif
+            Status { get; set; }
+
+        internal Element_AdaptiveProgressBar ConvertToElement()
+        {
+            // If Value not provided, we use 0
+            var val = Value;
+            if (val == null)
+            {
+                val = AdaptiveProgressBarValue.FromValue(0);
+            }
+
+            var answer = new Element_AdaptiveProgressBar();
+
+#if WINRT
+            answer.Title = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.Title, Title);
+            answer.Value = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.Value, val.ToXmlString());
+            answer.ValueStringOverride = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.ValueStringOverride, ValueStringOverride);
+            answer.Status = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.Status, Status);
+#else
+            answer.Title = Title?.ToXmlString();
+            answer.Value = val.ToXmlString();
+            answer.ValueStringOverride = ValueStringOverride?.ToXmlString();
+            answer.Status = Status?.ToXmlString();
+#endif
+
+            if (answer.Status == null)
+            {
+                throw new NullReferenceException("Status property is required.");
+            }
+
+            return answer;
+        }
+    }
+}

+ 43 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveProgressBarBindableProperty.cs

@@ -0,0 +1,43 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    // Note that this code is only compiled for WinRT. It is not compiled in any of the other projects.
+#if WINRT
+    /// <summary>
+    /// An enumeration of the properties that support data binding on <see cref="AdaptiveProgressBar"/> .
+    /// </summary>
+    public enum AdaptiveProgressBarBindableProperty
+    {
+        /// <summary>
+        /// An optional title string
+        /// </summary>
+        Title,
+
+        /// <summary>
+        /// The value of the progress bar.
+        /// </summary>
+        Value,
+
+        /// <summary>
+        /// An optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed.
+        /// </summary>
+        ValueStringOverride,
+
+        /// <summary>
+        /// An optional status string, which is displayed underneath the progress bar. If provided, this string should reflect the status of the download, like "Downloading..." or "Installing...".
+        /// </summary>
+        Status
+    }
+#endif
+}

+ 81 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveProgressBarValue.cs

@@ -0,0 +1,81 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// A class that represents the progress bar's value.
+    /// </summary>
+    public sealed class AdaptiveProgressBarValue
+    {
+        /// <summary>
+        /// Gets or sets the value (0-1) representing the percent complete.
+        /// </summary>
+        public double Value { get; set; }
+
+        /// <summary>
+        /// Gets or sets whether the progress bar is indeterminate.
+        /// </summary>
+        public bool IsIndeterminate { get; set; }
+
+        /// <summary>
+        /// Private constructor
+        /// </summary>
+        private AdaptiveProgressBarValue()
+        {
+        }
+
+        internal string ToXmlString()
+        {
+            if (IsIndeterminate)
+            {
+                return "indeterminate";
+            }
+
+            return Value.ToString();
+        }
+
+        /// <summary>
+        /// Returns an indeterminate progress bar value.
+        /// </summary>
+        public static AdaptiveProgressBarValue Indeterminate
+        {
+            get
+            {
+                return new AdaptiveProgressBarValue()
+                {
+                    IsIndeterminate = true
+                };
+            }
+        }
+
+        /// <summary>
+        /// Returns a progress bar value using the specified value (0-1) representing the percent complete.
+        /// </summary>
+        /// <param name="d">The value, 0-1, inclusive.</param>
+        /// <returns>A progress bar value.</returns>
+        public static AdaptiveProgressBarValue FromValue(double d)
+        {
+            if (d < 0 || d > 1)
+            {
+                throw new ArgumentOutOfRangeException("d", "Value must be between 0 and 1, inclusive.");
+            }
+
+            return new AdaptiveProgressBarValue()
+            {
+                Value = d
+            };
+        }
+    }
+}

+ 85 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveSubgroup.cs

@@ -0,0 +1,85 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// Subgroups are vertical columns that can contain text and images. Supported on Tiles since RTM. Supported on Toasts since Anniversary Update.
+    /// </summary>
+    public sealed class AdaptiveSubgroup
+    {
+        /// <summary>
+        /// <see cref="AdaptiveText"/> and <see cref="AdaptiveImage"/> are valid children of subgroups.
+        /// </summary>
+        public IList<IAdaptiveSubgroupChild> Children { get; private set; } = new List<IAdaptiveSubgroupChild>();
+
+        private int? _hintWeight;
+
+        /// <summary>
+        /// Control the width of this subgroup column by specifying the weight, relative to the other subgroups.
+        /// </summary>
+        public int? HintWeight
+        {
+            get
+            {
+                return _hintWeight;
+            }
+
+            set
+            {
+                Element_AdaptiveSubgroup.CheckWeight(value);
+
+                _hintWeight = value;
+            }
+        }
+
+        /// <summary>
+        /// Control the vertical alignment of this subgroup's content.
+        /// </summary>
+        public AdaptiveSubgroupTextStacking HintTextStacking { get; set; } = Element_AdaptiveSubgroup.DEFAULT_TEXT_STACKING;
+
+        internal Element_AdaptiveSubgroup ConvertToElement()
+        {
+            var subgroup = new Element_AdaptiveSubgroup()
+            {
+                Weight = HintWeight,
+                TextStacking = HintTextStacking
+            };
+
+            foreach (var child in Children)
+            {
+                subgroup.Children.Add(ConvertToSubgroupChildElement(child));
+            }
+
+            return subgroup;
+        }
+
+        private static IElement_AdaptiveSubgroupChild ConvertToSubgroupChildElement(IAdaptiveSubgroupChild child)
+        {
+            if (child is AdaptiveText)
+            {
+                return (child as AdaptiveText).ConvertToElement();
+            }
+
+            if (child is AdaptiveImage)
+            {
+                return (child as AdaptiveImage).ConvertToElement();
+            }
+
+            throw new NotImplementedException("Unknown child: " + child.GetType());
+        }
+    }
+}

+ 43 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveSubgroupEnums.cs

@@ -0,0 +1,43 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// TextStacking specifies the vertical alignment of content.
+    /// </summary>
+    public enum AdaptiveSubgroupTextStacking
+    {
+        /// <summary>
+        /// Renderer automatically selects the default vertical alignment.
+        /// </summary>
+        Default,
+
+        /// <summary>
+        /// Vertical align to the top.
+        /// </summary>
+        [EnumString("top")]
+        Top,
+
+        /// <summary>
+        /// Vertical align to the center.
+        /// </summary>
+        [EnumString("center")]
+        Center,
+
+        /// <summary>
+        /// Vertical align to the bottom.
+        /// </summary>
+        [EnumString("bottom")]
+        Bottom
+    }
+}

+ 143 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveText.cs

@@ -0,0 +1,143 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+#if WINRT
+using System.Collections.Generic;
+#endif
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// An adaptive text element.
+    /// </summary>
+    public sealed class AdaptiveText
+        : IAdaptiveChild,
+        IAdaptiveSubgroupChild,
+        ITileBindingContentAdaptiveChild,
+        IToastBindingGenericChild
+    {
+#if WINRT
+        /// <summary>
+        /// Gets a dictionary of the current data bindings, where you can assign new bindings.
+        /// </summary>
+        public IDictionary<AdaptiveTextBindableProperty, string> Bindings { get; private set; } = new Dictionary<AdaptiveTextBindableProperty, string>();
+#endif
+
+        /// <summary>
+        /// The text to display. Data binding support added in Creators Update, only works for toast top-level text elements.
+        /// </summary>
+        public
+#if WINRT
+            string
+#else
+            BindableString
+#endif
+            Text { get; set; }
+
+        /// <summary>
+        /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+        /// </summary>
+        public string Language { get; set; }
+
+        /// <summary>
+        /// The style controls the text's font size, weight, and opacity. Note that for Toast, the style will only take effect if the text is inside an <see cref="AdaptiveSubgroup"/>.
+        /// </summary>
+        public AdaptiveTextStyle HintStyle { get; set; }
+
+        /// <summary>
+        /// Set this to true to enable text wrapping. For Tiles, this is false by default. For Toasts, this is true on top-level text elements, and false inside an <see cref="AdaptiveSubgroup"/>. Note that for Toast, setting wrap will only take effect if the text is inside an <see cref="AdaptiveSubgroup"/> (you can use HintMaxLines = 1 to prevent top-level text elements from wrapping).
+        /// </summary>
+        public bool? HintWrap { get; set; }
+
+        private int? _hintMaxLines;
+
+        /// <summary>
+        /// The maximum number of lines the text element is allowed to display. For Tiles, this is infinity by default. For Toasts, top-level text elements will have varying max line amounts (and in the Anniversary Update you can change the max lines). Text on a Toast inside an <see cref="AdaptiveSubgroup"/> will behave identically to Tiles (default to infinity).
+        /// </summary>
+        public int? HintMaxLines
+        {
+            get
+            {
+                return _hintMaxLines;
+            }
+
+            set
+            {
+                if (value != null)
+                {
+                    Element_AdaptiveText.CheckMaxLinesValue(value.Value);
+                }
+
+                _hintMaxLines = value;
+            }
+        }
+
+        private int? _hintMinLines;
+
+        /// <summary>
+        /// The minimum number of lines the text element must display. Note that for Toast, this property will only take effect if the text is inside an <see cref="AdaptiveSubgroup"/>.
+        /// </summary>
+        public int? HintMinLines
+        {
+            get
+            {
+                return _hintMinLines;
+            }
+
+            set
+            {
+                if (value != null)
+                {
+                    Element_AdaptiveText.CheckMinLinesValue(value.Value);
+                }
+
+                _hintMinLines = value;
+            }
+        }
+
+        /// <summary>
+        /// The horizontal alignment of the text. Note that for Toast, this property will only take effect if the text is inside an <see cref="AdaptiveSubgroup"/>.
+        /// </summary>
+        public AdaptiveTextAlign HintAlign { get; set; }
+
+        internal Element_AdaptiveText ConvertToElement()
+        {
+            var answer = new Element_AdaptiveText()
+            {
+                Lang = Language,
+                Style = HintStyle,
+                Wrap = HintWrap,
+                MaxLines = HintMaxLines,
+                MinLines = HintMinLines,
+                Align = HintAlign
+            };
+
+#if WINRT
+            answer.Text = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveTextBindableProperty.Text, Text);
+#else
+            answer.Text = Text?.ToXmlString();
+#endif
+
+            return answer;
+        }
+
+        /// <summary>
+        /// Returns the value of the Text property.
+        /// </summary>
+        /// <returns>The value of the Text property.</returns>
+        public override string ToString()
+        {
+            return Text;
+        }
+    }
+}

+ 28 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveTextBindableProperty.cs

@@ -0,0 +1,28 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    // Note that this code is only compiled for WinRT. It is not compiled in any of the other projects.
+#if WINRT
+    /// <summary>
+    /// An enumeration of the properties that support data binding on <see cref="AdaptiveText"/> .
+    /// </summary>
+    public enum AdaptiveTextBindableProperty
+    {
+        /// <summary>
+        /// The text to display. Added in Creators Update only for toast top-level elements.
+        /// </summary>
+        Text
+    }
+#endif
+}

+ 173 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/AdaptiveTextEnums.cs

@@ -0,0 +1,173 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// Text style controls font size, weight, and opacity.
+    /// </summary>
+    public enum AdaptiveTextStyle
+    {
+        /// <summary>
+        /// Style is determined by the renderer.
+        /// </summary>
+        Default,
+
+        /// <summary>
+        /// Default value. Paragraph font size, normal weight and opacity.
+        /// </summary>
+        [EnumString("caption")]
+        Caption,
+
+        /// <summary>
+        /// Same as Caption but with subtle opacity.
+        /// </summary>
+        [EnumString("captionSubtle")]
+        CaptionSubtle,
+
+        /// <summary>
+        /// H5 font size.
+        /// </summary>
+        [EnumString("body")]
+        Body,
+
+        /// <summary>
+        /// Same as Body but with subtle opacity.
+        /// </summary>
+        [EnumString("bodySubtle")]
+        BodySubtle,
+
+        /// <summary>
+        /// H5 font size, bold weight. Essentially the bold version of Body.
+        /// </summary>
+        [EnumString("base")]
+        Base,
+
+        /// <summary>
+        /// Same as Base but with subtle opacity.
+        /// </summary>
+        [EnumString("baseSubtle")]
+        BaseSubtle,
+
+        /// <summary>
+        /// H4 font size.
+        /// </summary>
+        [EnumString("subtitle")]
+        Subtitle,
+
+        /// <summary>
+        /// Same as Subtitle but with subtle opacity.
+        /// </summary>
+        [EnumString("subtitleSubtle")]
+        SubtitleSubtle,
+
+        /// <summary>
+        /// H3 font size.
+        /// </summary>
+        [EnumString("title")]
+        Title,
+
+        /// <summary>
+        /// Same as Title but with subtle opacity.
+        /// </summary>
+        [EnumString("titleSubtle")]
+        TitleSubtle,
+
+        /// <summary>
+        /// Same as Title but with top/bottom padding removed.
+        /// </summary>
+        [EnumString("titleNumeral")]
+        TitleNumeral,
+
+        /// <summary>
+        /// H2 font size.
+        /// </summary>
+        [EnumString("subheader")]
+        Subheader,
+
+        /// <summary>
+        /// Same as Subheader but with subtle opacity.
+        /// </summary>
+        [EnumString("subheaderSubtle")]
+        SubheaderSubtle,
+
+        /// <summary>
+        /// Same as Subheader but with top/bottom padding removed.
+        /// </summary>
+        [EnumString("subheaderNumeral")]
+        SubheaderNumeral,
+
+        /// <summary>
+        /// H1 font size.
+        /// </summary>
+        [EnumString("header")]
+        Header,
+
+        /// <summary>
+        /// Same as Header but with subtle opacity.
+        /// </summary>
+        [EnumString("headerSubtle")]
+        HeaderSubtle,
+
+        /// <summary>
+        /// Same as Header but with top/bottom padding removed.
+        /// </summary>
+        [EnumString("headerNumeral")]
+        HeaderNumeral
+    }
+
+    /// <summary>
+    /// Controls the horizontal alignment of text.
+    /// </summary>
+    public enum AdaptiveTextAlign
+    {
+        /// <summary>
+        /// Alignment is automatically determined by
+        /// </summary>
+        Default,
+
+        /// <summary>
+        /// The system automatically decides the alignment based on the language and culture.
+        /// </summary>
+        [EnumString("auto")]
+        Auto,
+
+        /// <summary>
+        /// Horizontally align the text to the left.
+        /// </summary>
+        [EnumString("left")]
+        Left,
+
+        /// <summary>
+        /// Horizontally align the text in the center.
+        /// </summary>
+        [EnumString("center")]
+        Center,
+
+        /// <summary>
+        /// Horizontally align the text to the right.
+        /// </summary>
+        [EnumString("right")]
+        Right
+    }
+
+    internal enum AdaptiveTextPlacement
+    {
+        /// <summary>
+        /// Default value
+        /// </summary>
+        Inline,
+
+        [EnumString("attribution")]
+        Attribution
+    }
+}

+ 45 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BaseImageHelper.cs

@@ -0,0 +1,45 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    internal static class BaseImageHelper
+    {
+        internal static void SetSource(ref string destination, string value)
+        {
+            if (value == null)
+            {
+                throw new ArgumentNullException(nameof(value));
+            }
+
+            destination = value;
+        }
+
+        internal static Element_AdaptiveImage CreateBaseElement(IBaseImage curr)
+        {
+            if (curr.Source == null)
+            {
+                throw new NullReferenceException("Source property is required.");
+            }
+
+            return new Element_AdaptiveImage()
+            {
+                Src = curr.Source,
+                Alt = curr.AlternateText,
+                AddImageQuery = curr.AddImageQuery
+            };
+        }
+    }
+}

+ 28 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BaseTextHelper.cs

@@ -0,0 +1,28 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements;
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    internal class BaseTextHelper
+    {
+        internal static Element_AdaptiveText CreateBaseElement(IBaseText curr)
+        {
+            return new Element_AdaptiveText()
+            {
+                Text = curr.Text,
+                Lang = curr.Language
+            };
+        }
+    }
+}

+ 98 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BindableValues/BindableProgressBarValue.cs

@@ -0,0 +1,98 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    // Note that this code is NOT compiled for WinRT.
+    // WinRT uses a different binding system since it doesn't support implicit type converters.
+#if !WINRT
+    /// <summary>
+    /// A binding value for doubles.
+    /// </summary>
+    public sealed class BindableProgressBarValue
+    {
+        /// <summary>
+        /// Raw value is used for the implicit converter case, where dev provided a raw double. We store the raw value,
+        /// so that later on when generating the XML, we can provide this value rather than binding syntax.
+        /// </summary>
+        internal AdaptiveProgressBarValue RawValue { get; private set; }
+
+        internal bool RawIsIndeterminate { get; private set; }
+
+        /// <summary>
+        /// The name that maps to your binding data value.
+        /// </summary>
+        public string BindingName { get; set; }
+
+        /// <summary>
+        /// Initializes a new binding for a double value, with the required binding value name. Do NOT include surrounding {} brackets.
+        /// </summary>
+        /// <param name="bindingName">The name that maps to your binding data value.</param>
+        public BindableProgressBarValue(string bindingName)
+        {
+            BindingName = bindingName;
+        }
+
+        /// <summary>
+        /// Private constructor used by the implicit converter to assign the raw value.
+        /// </summary>
+        private BindableProgressBarValue()
+        {
+        }
+
+        internal string ToXmlString()
+        {
+            if (BindingName != null)
+            {
+                return "{" + BindingName + "}";
+            }
+
+            if (RawValue != null)
+            {
+                return RawValue.ToXmlString();
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Creates a <see cref="BindableProgressBarValue"/> that has a raw value assigned.
+        /// </summary>
+        /// <param name="v">The raw value</param>
+        public static implicit operator BindableProgressBarValue(AdaptiveProgressBarValue v)
+        {
+            return new BindableProgressBarValue()
+            {
+                RawValue = v
+            };
+        }
+
+        /// <summary>
+        /// Returns the raw value of the <see cref="BindableProgressBarValue"/>.
+        /// </summary>
+        /// <param name="b">The <see cref="BindableProgressBarValue"/> to obtain the raw value from.</param>
+        public static implicit operator AdaptiveProgressBarValue(BindableProgressBarValue b)
+        {
+            return b.RawValue;
+        }
+
+        /// <summary>
+        /// Creates an <see cref="BindableProgressBarValue"/> that has tbe raw double value.
+        /// </summary>
+        /// <param name="d">The raw value</param>
+        public static implicit operator BindableProgressBarValue(double d)
+        {
+            return AdaptiveProgressBarValue.FromValue(d);
+        }
+    }
+#endif
+}

+ 78 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/BindableValues/BindableString.cs

@@ -0,0 +1,78 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    // Note that this code is NOT compiled for WinRT.
+    // WinRT uses a different binding system since it doesn't support implicit type converters.
+#if !WINRT
+    /// <summary>
+    /// A binding value for strings.
+    /// </summary>
+    public sealed class BindableString
+    {
+        internal string RawValue { get; private set; }
+
+        /// <summary>
+        /// The name that maps to your binding data value.
+        /// </summary>
+        public string BindingName { get; set; }
+
+        /// <summary>
+        /// Initializes a new binding for a string value, with the required binding name. Do NOT include surrounding {} brackets.
+        /// </summary>
+        /// <param name="bindingName">The name that maps to your data binding value.</param>
+        public BindableString(string bindingName)
+        {
+            BindingName = bindingName;
+        }
+
+        /// <summary>
+        /// Private constructor used by the implicit converter to assign the raw value.
+        /// </summary>
+        private BindableString()
+        {
+        }
+
+        internal string ToXmlString()
+        {
+            if (BindingName != null)
+            {
+                return "{" + BindingName + "}";
+            }
+
+            return RawValue;
+        }
+
+        /// <summary>
+        /// Creates a <see cref="BindableString"/> that has a raw value assigned.
+        /// </summary>
+        /// <param name="d">The raw value</param>
+        public static implicit operator BindableString(string d)
+        {
+            return new BindableString()
+            {
+                RawValue = d
+            };
+        }
+
+        /// <summary>
+        /// Returns the raw value of the <see cref="BindableString"/>.
+        /// </summary>
+        /// <param name="b">The <see cref="BindableString"/> to obtain the raw value from.</param>
+        public static implicit operator string(BindableString b)
+        {
+            return b.RawValue;
+        }
+    }
+#endif
+}

+ 37 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveGroup.cs

@@ -0,0 +1,37 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System.Collections.Generic;
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements
+{
+    [NotificationXmlElement("group")]
+    internal sealed class Element_AdaptiveGroup : IElement_TileBindingChild, IElement_ToastBindingChild, IElementWithDescendants
+    {
+        public IList<Element_AdaptiveSubgroup> Children { get; private set; } = new List<Element_AdaptiveSubgroup>();
+
+        public IEnumerable<object> Descendants()
+        {
+            foreach (Element_AdaptiveSubgroup subgroup in Children)
+            {
+                // Return the subgroup
+                yield return subgroup;
+
+                // And also return its descendants
+                foreach (object descendant in subgroup.Descendants())
+                {
+                    yield return descendant;
+                }
+            }
+        }
+    }
+}

+ 67 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveImage.cs

@@ -0,0 +1,67 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements
+{
+    [NotificationXmlElement("image")]
+    internal sealed class Element_AdaptiveImage : IElement_TileBindingChild, IElement_ToastBindingChild, IElement_AdaptiveSubgroupChild
+    {
+        internal const AdaptiveImagePlacement DEFAULT_PLACEMENT = AdaptiveImagePlacement.Inline;
+        internal const AdaptiveImageCrop DEFAULT_CROP = AdaptiveImageCrop.Default;
+        internal const AdaptiveImageAlign DEFAULT_ALIGN = AdaptiveImageAlign.Default;
+
+        [NotificationXmlAttribute("id")]
+        public int? Id { get; set; }
+
+        [NotificationXmlAttribute("src")]
+        public string Src { get; set; }
+
+        [NotificationXmlAttribute("alt")]
+        public string Alt { get; set; }
+
+        [NotificationXmlAttribute("addImageQuery")]
+        public bool? AddImageQuery { get; set; }
+
+        [NotificationXmlAttribute("placement", DEFAULT_PLACEMENT)]
+        public AdaptiveImagePlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+
+        [NotificationXmlAttribute("hint-align", DEFAULT_ALIGN)]
+        public AdaptiveImageAlign Align { get; set; } = DEFAULT_ALIGN;
+
+        [NotificationXmlAttribute("hint-crop", DEFAULT_CROP)]
+        public AdaptiveImageCrop Crop { get; set; } = DEFAULT_CROP;
+
+        [NotificationXmlAttribute("hint-removeMargin")]
+        public bool? RemoveMargin { get; set; }
+
+        private int? _overlay;
+
+        [NotificationXmlAttribute("hint-overlay")]
+        public int? Overlay
+        {
+            get
+            {
+                return _overlay;
+            }
+
+            set
+            {
+                if (value != null)
+                {
+                    Element_TileBinding.CheckOverlayValue(value.Value);
+                }
+
+                _overlay = value;
+            }
+        }
+    }
+}

+ 32 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveImageEnums.cs

@@ -0,0 +1,32 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements
+{
+    internal enum AdaptiveImagePlacement
+    {
+        [EnumString("inline")]
+        Inline,
+
+        [EnumString("background")]
+        Background,
+
+        [EnumString("peek")]
+        Peek,
+
+        [EnumString("hero")]
+        Hero,
+
+        [EnumString("appLogoOverride")]
+        AppLogoOverride
+    }
+}

+ 30 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveProgressBar.cs

@@ -0,0 +1,30 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements
+{
+    [NotificationXmlElement("progress")]
+    internal sealed class Element_AdaptiveProgressBar : IElement_ToastBindingChild
+    {
+        [NotificationXmlAttribute("value")]
+        public string Value { get; set; }
+
+        [NotificationXmlAttribute("title")]
+        public string Title { get; set; }
+
+        [NotificationXmlAttribute("valueStringOverride")]
+        public string ValueStringOverride { get; set; }
+
+        [NotificationXmlAttribute("status")]
+        public string Status { get; set; }
+    }
+}

+ 67 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveSubgroup.cs

@@ -0,0 +1,67 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements
+{
+    [NotificationXmlElement("subgroup")]
+    internal sealed class Element_AdaptiveSubgroup : IElementWithDescendants
+    {
+        internal const AdaptiveSubgroupTextStacking DEFAULT_TEXT_STACKING = AdaptiveSubgroupTextStacking.Default;
+
+        [NotificationXmlAttribute("hint-textStacking", DEFAULT_TEXT_STACKING)]
+        public AdaptiveSubgroupTextStacking TextStacking { get; set; } = DEFAULT_TEXT_STACKING;
+
+        private int? _weight;
+
+        [NotificationXmlAttribute("hint-weight")]
+        public int? Weight
+        {
+            get
+            {
+                return _weight;
+            }
+
+            set
+            {
+                CheckWeight(value);
+
+                _weight = value;
+            }
+        }
+
+        internal static void CheckWeight(int? weight)
+        {
+            if (weight != null && weight.Value < 1)
+            {
+                throw new ArgumentOutOfRangeException("Weight must be between 1 and int.MaxValue, inclusive (or null)");
+            }
+        }
+
+        public IList<IElement_AdaptiveSubgroupChild> Children { get; private set; } = new List<IElement_AdaptiveSubgroupChild>();
+
+        public IEnumerable<object> Descendants()
+        {
+            foreach (IElement_AdaptiveSubgroupChild child in Children)
+            {
+                // Return each child (we know there's no further descendants)
+                yield return child;
+            }
+        }
+    }
+
+    internal interface IElement_AdaptiveSubgroupChild
+    {
+    }
+}

+ 103 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/Elements/Element_AdaptiveText.cs

@@ -0,0 +1,103 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+using System;
+
+namespace Microsoft.Toolkit.Uwp.Notifications.Adaptive.Elements
+{
+    [NotificationXmlElement("text")]
+    internal sealed class Element_AdaptiveText : IElement_TileBindingChild, IElement_AdaptiveSubgroupChild, IElement_ToastBindingChild
+    {
+        internal const AdaptiveTextStyle DEFAULT_STYLE = AdaptiveTextStyle.Default;
+        internal const AdaptiveTextAlign DEFAULT_ALIGN = AdaptiveTextAlign.Default;
+        internal const AdaptiveTextPlacement DEFAULT_PLACEMENT = AdaptiveTextPlacement.Inline;
+
+        [NotificationXmlContent]
+        public string Text { get; set; }
+
+        [NotificationXmlAttribute("id")]
+        public int? Id { get; set; }
+
+        [NotificationXmlAttribute("lang")]
+        public string Lang { get; set; }
+
+        [NotificationXmlAttribute("hint-align", DEFAULT_ALIGN)]
+        public AdaptiveTextAlign Align { get; set; } = DEFAULT_ALIGN;
+
+        private int? _maxLines;
+
+        [NotificationXmlAttribute("hint-maxLines")]
+        public int? MaxLines
+        {
+            get
+            {
+                return _maxLines;
+            }
+
+            set
+            {
+                if (value != null)
+                {
+                    CheckMaxLinesValue(value.Value);
+                }
+
+                _maxLines = value;
+            }
+        }
+
+        internal static void CheckMaxLinesValue(int value)
+        {
+            if (value < 1)
+            {
+                throw new ArgumentOutOfRangeException("MaxLines must be between 1 and int.MaxValue, inclusive.");
+            }
+        }
+
+        private int? _minLines;
+
+        [NotificationXmlAttribute("hint-minLines")]
+        public int? MinLines
+        {
+            get
+            {
+                return _minLines;
+            }
+
+            set
+            {
+                if (value != null)
+                {
+                    CheckMinLinesValue(value.Value);
+                }
+
+                _minLines = value;
+            }
+        }
+
+        internal static void CheckMinLinesValue(int value)
+        {
+            if (value < 1)
+            {
+                throw new ArgumentOutOfRangeException("MinLines must be between 1 and int.MaxValue, inclusive.");
+            }
+        }
+
+        [NotificationXmlAttribute("hint-style", DEFAULT_STYLE)]
+        public AdaptiveTextStyle Style { get; set; } = DEFAULT_STYLE;
+
+        [NotificationXmlAttribute("hint-wrap")]
+        public bool? Wrap { get; set; }
+
+        [NotificationXmlAttribute("placement", DEFAULT_PLACEMENT)]
+        public AdaptiveTextPlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+    }
+}

+ 22 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/IAdaptiveChild.cs

@@ -0,0 +1,22 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// Elements that can be direct children of adaptive content, including (<see cref="AdaptiveText"/>, <see cref="AdaptiveImage"/>, and <see cref="AdaptiveGroup"/>).
+    /// </summary>
+    public interface IAdaptiveChild
+    {
+        // Blank interface simply for compile-enforcing the child types in the list.
+    }
+}

+ 22 - 0
src/windows/Microsoft.Toolkit.Uwp.Notifications.Shared/Adaptive/IAdaptiveSubgroupChild.cs

@@ -0,0 +1,22 @@
+// ******************************************************************
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
+// ******************************************************************
+
+namespace Microsoft.Toolkit.Uwp.Notifications
+{
+    /// <summary>
+    /// Elements that can be direct children of an <see cref="AdaptiveSubgroup"/>, including  (<see cref="AdaptiveText"/> and <see cref="AdaptiveImage"/>).
+    /// </summary>
+    public interface IAdaptiveSubgroupChild
+    {
+        // Blank interface simply for compile-enforcing the child types in the list.
+    }
+}

Неке датотеке нису приказане због велике количине промена