first commit

main
jcgitadmin 2024-10-19 21:17:47 +08:00
commit 68e2a3455b
178 changed files with 33411 additions and 0 deletions

9
.editorconfig 100644
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

33
.gitignore vendored 100644
View File

@ -0,0 +1,33 @@
.DS_Store
.thumbs.db
node_modules
# Quasar core related directories
.quasar
/dist
/quasar.config.*.temporary.compiled*
# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
# local .env files
.env.local*

3
.npmrc 100644
View File

@ -0,0 +1,3 @@
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false

13
.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,13 @@
{
"recommendations": [
"editorconfig.editorconfig",
"vue.volar",
"wayou.vscode-todo-highlight"
],
"unwantedRecommendations": [
"octref.vetur",
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

4
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,4 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true
}

45
README.md 100644
View File

@ -0,0 +1,45 @@
# 云朵小店 (yunduoxd)
云朵小店店主端
## Install the dependencies
```bash
yarn
# or
npm install
```
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
quasar dev
```
### Build the app for production
```bash
quasar build
```
### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
## 解决http图片无法加载问题
修改`jingcai-buyer-app/src-cordova/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java` initWebViewSettings方法中添加如下
```
private void initWebViewSettings() {
webView.setInitialScale(0);
webView.setVerticalScrollBarEnabled(false);
// Enable JavaScript
final WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
// **********这里开始添加如下三行代码
//允许混合内容 解决部分手机 加载不出https请求里面的http下的图片
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
...
}
```

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2014-2015 Andrew Stevens <andy@andxyz.com>,
Modern Alchemits <office@modalog.at>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,37 @@
# Cache Clear
This is a WebView cache plugin for Cordova 5.4.0 supporting Android (>=2.3.3) and iOS(>=6.0)
It allows the app to use javascript to initiate a cordova webview cache clear
There are two methods:
`clear(successCallback, errorCallback)`
`cleartemp()`
#### Manual Installation
You may use `cordova-cli` as follows:
```shell
cordova plugin add https://github.com/andxyz/cordova-plugin-cache.git
```
#### Usage
```javascript
document.addEventListener('deviceready', onDeviceReady);
function onDeviceReady() {
var success = function(status) {
alert('Message: ' + status);
}
var error = function(status) {
alert('Error: ' + status);
}
window.cache.clear(success, error);
window.cache.cleartemp();
}
```

View File

@ -0,0 +1,12 @@
{
"name": "cordova-plugin-cache",
"version": "1.0.5",
"description": "Cordova Clear Cache Plugin",
"cordova": {
"id": "cordova-plugin-cache",
"platforms": [
"android",
"ios"
]
}
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="cordova-plugin-cache"
version="1.0.5">
<name>Clear Cache</name>
<description>
&lt;p&gt;This is a WebView cache plugin for Cordova 5.4.0 supporting Android (>=2.3.3) and iOS(>=6.0). It allows to clear the cordova webview cache.&lt;/p&gt;
</description>
<js-module src="www/Cache.js" name="Cache">
<clobbers target="cache" /><!-- will be available under window.cache -->
</js-module>
<!-- android -->
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="Cache" >
<param name="android-package" value="org.apache.cordova.plugin.cache.Cache"/>
</feature>
</config-file>
<source-file src="src/android/Cache.java" target-dir="src/com/apache/cordova/plugin/cache" />
</platform>
<!-- ios -->
<platform name="ios">
<config-file target="config.xml" parent="/*">
<feature name="Cache">
<param name="ios-package" value="Cache" />
</feature>
</config-file>
<header-file src="src/ios/Cache.h" />
<source-file src="src/ios/Cache.m" />
</platform>
</plugin>

View File

@ -0,0 +1,125 @@
/*
The MIT License (MIT)
Copyright © 2014-2015 Andrew Stevens <andy@andxyz.com>, Modern Alchemits <office@modalog.at>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of
the Software.
THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package org.apache.cordova.plugin.cache;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.File;
import android.annotation.TargetApi;
import android.app.Activity;
import android.util.Log;
@TargetApi(19)
public class Cache extends CordovaPlugin {
private static final String LOG_TAG = "Cache";
private CallbackContext callbackContext;
/**
* Constructor.
*/
public Cache() {
}
@Override
public boolean execute (String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
/*try
{
*/
if ( action.equals("clear") ) {
Log.v(LOG_TAG, "Cordova Android Cache.clear() called.");
this.callbackContext = callbackContext;
final Cache self = this;
cordova.getActivity().runOnUiThread( new Runnable() {
public void run() {
try {
// clear the cache
self.webView.clearCache(true);
// clear the data
self.clearApplicationData();
// send success result to cordova
PluginResult result = new PluginResult(PluginResult.Status.OK);
result.setKeepCallback(false);
self.callbackContext.sendPluginResult(result);
} catch ( Exception e ) {
String msg = "Error while clearing webview cache.";
Log.e(LOG_TAG, msg );
// return error answer to cordova
PluginResult result = new PluginResult(PluginResult.Status.ERROR, msg);
result.setKeepCallback(false);
self.callbackContext.sendPluginResult(result);
}
}
});
return true;
}
return false;
/*
}
catch (JSONException e)
{
// TODO: signal JSON problem to JS
//callbackContext.error("Problem with JSON");
return false;
}
*/
}
// http://www.hrupin.com/2011/11/how-to-clear-user-data-in-your-android-application-programmatically
private void clearApplicationData() {
File cache = this.cordova.getActivity().getCacheDir();
File appDir = new File(cache.getParent());
Log.i(LOG_TAG, "Absolute path: " + appDir.getAbsolutePath());
if (appDir.exists()) {
String[] children = appDir.list();
for (String s : children) {
if (!s.equals("lib")) {
deleteDir(new File(appDir, s));
Log.i(LOG_TAG, "File /data/data/APP_PACKAGE/" + s + " DELETED");
}
}
}
}
private static boolean deleteDir(File dir) {
Log.i(LOG_TAG, "Deleting: " + dir.getAbsolutePath());
if (dir != null && dir.isDirectory()) {
String[] children = dir.list();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
return dir.delete();
}
}

View File

@ -0,0 +1,35 @@
/*
Copyright © 2014-2015 Andrew Stevens <andy@andxyz.com>,
Modern Alchemits <office@modalog.at>
Licensed under MIT.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "AppDelegate.h"
#import <Cordova/CDVPlugin.h>
#import <Foundation/Foundation.h>
@interface Cache : CDVPlugin {
NSString* _callbackId;
}
- (void)clear:(CDVInvokedUrlCommand *)command;
- (void)cleartemp:(CDVInvokedUrlCommand *)command;
// retain command for async repsonses
@property(nonatomic, strong) CDVInvokedUrlCommand *command;
@end

View File

@ -0,0 +1,134 @@
/*
Copyright © 2014-2015 Andrew Stevens <andy@andxyz.com>,
Modern Alchemits <office@modalog.at>
Licensed under MIT.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "Cache.h"
@implementation Cache
@synthesize command;
- (void)clear:(CDVInvokedUrlCommand *)command
{
NSLog(@"Cordova Cache Plugin method clear() called.");
_callbackId = command.callbackId;
// Plugin arguments are not used at the moment.
// NSArray* arguments = command.arguments;
[self.commandDelegate runInBackground:^{
// clear cache
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}];
[self success:@"NSURLCache"];
}
- (void)cleartemp:(CDVInvokedUrlCommand *)command
{
NSLog(@"Cordova Cache Plugin method cleartemp() called.");
_callbackId = command.callbackId;
// empty the tmp directory
NSFileManager *fileMgr = [[NSFileManager alloc] init];
// setup loop vars
NSError *err = nil;
BOOL hasErrors = NO;
NSString *tempDirectoryPath = NSTemporaryDirectory();
NSDirectoryEnumerator *directoryEnumerator = [fileMgr enumeratorAtPath:tempDirectoryPath];
NSString *fileName = nil;
BOOL result;
// clear contents of NSTemporaryDirectory
while ((fileName = [directoryEnumerator nextObject])) {
NSString *filePath = [tempDirectoryPath stringByAppendingPathComponent:fileName];
result = [fileMgr removeItemAtPath:filePath error:&err];
if (!result && err) {
NSLog(@"Failed to delete: %@ (error: %@)", filePath, err);
hasErrors = YES;
}
}
// send result
if (hasErrors) {
[self error:@"NSTemporaryDirectory"];
}
else {
[self success:@"NSTemporaryDirectory"];
}
}
- (void)success:(NSString *)message
{
NSString *resultMsg = [NSString stringWithFormat:@"Cordova Cache Plugin cleared the cache: (%@).", message];
NSLog(@"%@", resultMsg);
// create cordova result
CDVPluginResult *pluginResult = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK
messageAsString:[resultMsg stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
// send cordova result
[self.commandDelegate sendPluginResult:pluginResult callbackId:_callbackId];
}
- (void)error:(NSString *)message
{
NSString *resultMsg = [NSString stringWithFormat:@"Cordova Cache Plugin error clearing: (%@).", message];
NSLog(@"%@", resultMsg);
// create cordova result
CDVPluginResult *pluginResult = [CDVPluginResult
resultWithStatus:CDVCommandStatus_ERROR
messageAsString:[resultMsg stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
// send cordova result
[self.commandDelegate sendPluginResult:pluginResult callbackId:_callbackId];
}
- (void)onPause
{
NSLog(@"Cordova Cache Plugin onPause() fired");
// [self clear];
// [self cleartemp];
}
- (void)onResume
{
NSLog(@"Cordova Cache Plugin onResume() fired");
// [self clear];
// [self cleartemp];
}
- (void)pluginInitialize
{
if (UIApplicationDidEnterBackgroundNotification && UIApplicationWillEnterForegroundNotification) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onPause)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onResume)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
}
@end

View File

@ -0,0 +1,39 @@
//////////////////////////////////////////
// Cache.js
//
// The MIT License (MIT)
// Copyright © 2014-2015 Andrew Stevens <andy@andxyz.com>,
// Modern Alchemits <office@modalog.at>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//////////////////////////////////////////
var exec = require('cordova/exec');
var Cache = {
clear: function(success, error) {
exec(success, error, "Cache", "clear", [])
},
cleartemp: function(success, error) {
exec(success, error, "Cache", "cleartemp", [])
}
}
module.exports = Cache;

View File

@ -0,0 +1,5 @@
/src/windows.old
/src/windows/Microsoft.Toolkit.Uwp.Notifications.*
/src/windows/**/.vs
/src/windows/**/bin
/src/windows/**/obj

View File

@ -0,0 +1,144 @@
ChangeLog
---------
#### Version 0.9.0-beta.4
- Platform enhancements
- Android 8-10 device support
- Android 10 SDK support (using androidx libraries rather than support libraries)
- Note: If you are not building with API 29 on Android, you can use https://www.npmjs.com/package/cordova-plugin-androidx for backwards compatibility.
- Enhancements (Android)
- Adjusted high priority notifications to fire at more exact time.
- use setAlarmClock() rather than setExactAndAllowWhileIdle().
- New `autoLaunch` attribute.
- Notification launches application if closed (Android <= 9).
- App has the option to run some logic and schedule (or not schedule) an immediate alarm.
- Note, this will be overridden if fullScreenIntent is true. Doing that will use the fullScreenIntent behavior and not always autoLaunch.
- Also note, this feature can cause alarms to not always fire on time.
- New `alarmVolume` attribute. Can force application to increase device notification volume prior to playing sound.
- New `resetDelay` attribute. Delay to reset alarmVolume on the device back to its original settings
- New `wakeLockTimeout` attribute. Can be used to extend the wakelock to keep the device awake longer (in case an autoLaunch application trigger takes a while).
- New `triggerInApp` attribute.
- If set to true, notification will not fire. Instead it will fire a trigger event that can be listened to in the app.
- This allows you to evaluate the notification in the application, and if you decide to fire it, you can remove the trigger, remove triggerInApp, and schedule it. (It should fire immediately).
- This was previously coupled with autoLaunch, but I split it out for more flexibility.
- Listening to the event can be done as follows:
`window.cordova.plugins.notification.local.on('trigger', (notification) => {
// do something with notification
});`
- Note: this functionality will be skipped (alarms will fire immediately with no trigger method) if any of the following are true:
- Android 8+ is asleep and the app is not running (even if autoLaunch is true). As a timely execution of the app code can't be guaranteed in this state, the notification will fire immediately.
- The app is not running and autoLaunch is false (any Android version). If the app is not running, we can't execute its code, so fire immediately.
- New `fullScreenIntent` attribute.
- If set to true, will use fullScreenIntent in AlarmManager to launch application.
- Setting this to true will negate autoLaunch, and is the only way to automatically launch the app on Android 10+.
- Note: OS/manufacturer has some options for how to deal with this configuration. It will not always launch the activity, but typically will launch it if the device is asleep and show a heads-up notification if it is not.
- **Android Channel Support**
- New `channelName` attribute for the name of the notification channel to use
- New `channelId` attribute. If passed in, a notification channel will be created (using volume and vibration settings to determine importance)
- Android: Support for excluding an application from battery optimization settings.
- Android: Support for allowing an application permissions to override Do Not Disturb.
---
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
#### Version 0.8.3 (03.01.2016)
- Platform enhancements
- Support for the `Crosswalk Engine`
- Support for `cordova-ios@4` and the `WKWebView Engine`
- Support for `cordova-windows@4` and `Windows 10` without using hooks
- Enhancements
- New `color` attribute for Android (Thanks to @Eusebius1920)
- New `quarter` intervall for iOS & Android
- `smallIcon` is optional (Android)
- `update` checks for permission like _schedule_
- Decreased time-frame for trigger event (iOS)
- Force `every:` to be a string on iOS
- Bug fixes
- Fixed #634 option to skip permission check
- Fixed #588 crash when basename & extension can't be extracted (Android)
- Fixed #732 loop between update and trigger (Android)
- Fixed #710 crash due to >500 notifications (Android)
- Fixed #682 crash while resuming app from notification (Android 6)
- Fixed #612 cannot update icon or sound (Android)
- Fixed crashing get(ID) if notification doesn't exist
- Fixed #569 getScheduled returns two items per notification
- Fixed #700 notifications appears on bootup
#### Version 0.8.2 (08.11.2015)
- Submitted to npm
- Initial support for the `windows` platform
- Re-add autoCancel option on Android
- Warn about unknown properties
- Fix crash on iOS 9
- Fixed webView-Problems with cordova-android 4.0
- Fix get* with single id
- Fix issue when passing data in milliseconds
- Update device plugin id
- Several other fixes
#### Version 0.8.1 (08.03.2015)
- Fix incompatibility with cordova version 3.5-3.0
- Fire `clear` instead of `cancel` event when clicked on repeating notifications
- Do not fire `clear` or `cancel` event when clicked on persistent notifications
### Version 0.8.0 (05.03.2015)
- Support for iOS 8, Android 2 (SDK >= 7) and Android 5
- Windows Phone 8.1 will be added soon
- New interfaces to ask for / register permissions required to schedule local notifications
- `hasPermission()` and `registerPermission()`
- _schedule()_ will register the permission automatically and schedule the notification if granted.
- New interface to update already scheduled|triggered local notifications
- `update()`
- New interfaces to clear the notification center
- `clear()` and `clearAll()`
- New interfaces to query for local notifications, their properties, their IDs and their existence depend on their state
- `isPresent()`, `isScheduled()`, `isTriggered()`
- `getIds()`, `getAllIds()`, `getScheduledIds()`, `getTriggeredIds()`
- `get()`, `getAll()`, `getScheduled()`, `getTriggered()`
- Schedule multiple local notifications at once
- `schedule( [{...},{...}] )`
- Update multiple local notifications at once
- `update( [{...},{...}] )`
- Clear multiple local notifications at once
- `clear( [1, 2] )`
- Cancel multiple local notifications at once
- `cancel( [1, 2] )`
- New URI format to specify sound and image resources
- `http(s):` for remote resources _(Android)_
- `file:` for local resources relative to the _www_ folder
- `res:` for native resources
- New events
- `schedule`, `update`, `clear`, `clearall` and `cancelall`
- Enhanced event informations
- Listener will get called with the local notification object instead of only the ID
- Multiple listener for one event
- `on(event, callback, scope)`
- Unregister event listener
- `un(event, callback)`
- New Android specific properties
- `led` properties
- `sound` and `image` accepts remote resources
- Callback function and scope for all interface methods
- `schedule( notification, callback, scope )`
- Renamed `add()` to `schedule()`
- `autoCancel` property has been removed
- Use `ongoing: true` for persistent local notifications on Android
- Renamed repeat intervals
- `second`, `minute`, `hour`, `day`, `week`, `month` and `year`
- Renamed some local notification properties
- `date`, `json`, `message` and `repeat`
- Scheduling local notifications with the deprecated properties is still possible
- [Kitchen Sink sample app](https://github.com/katzer/cordova-plugin-local-notifications/tree/example)
- [Wiki](https://github.com/katzer/cordova-plugin-local-notifications/wiki)

View File

@ -0,0 +1,42 @@
**WARNING: IF YOU IGNORE THIS TEMPLATE, WE'LL IGNORE YOUR ISSUE. YOU MUST FILL THIS IN!**
Provide a general summary of the issue.
## Your Environment
* Plugin version:
* Platform:
* OS version:
* Device manufacturer / model:
* Cordova version (```cordova -v```):
* Cordova platform version (```cordova platform ls```):
* Plugin config
* Ionic Version (if using Ionic)
## Expected Behavior
_Tell us what should happen_
## Actual Behavior
_Tell us what happens instead_
## Steps to Reproduce
_Reproduce this issue; include code to reproduce, if relevant_
1. ...
2. ...
3. ...
4. ....
## Context
_What were you trying to do?_
## Debug logs
_Include iOS / Android logs_
* ios XCode logs
* Android: $ adb logcat

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
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.
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.

View File

@ -0,0 +1,582 @@
<p align="left"><b><a href="https://github.com/katzer/cordova-plugin-local-notifications/tree/example-x">SAMPLE APP</a> :point_right:</b></p>
<p align="center">
<img src="images/logo.png">
</p>
<p align="center">
<a href="https://www.npmjs.com/package/cordova-plugin-local-notification">
<img src="https://badge.fury.io/js/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>
<a href="https://opensource.org/licenses/Apache-2.0">
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License" />
</a>
</p>
<br>
> 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.
<br>
<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>
### Notification components
- Header area
- Content area
- Action area
### How notifications may be noticed
- 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
- Android 4.4+
- iOS 10+
- Windows 10
<br>
<br>
## Important Notice
Please make sure that you always read the tagged README for the version you're using.
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 Oreo is limited yet.
- v0.9 and v0.8 aren't compatible with each other (Wont fix)
Please report bugs or missing features!
## Basics
The plugin creates the object `cordova.plugins.notification.local` and is accessible after *deviceready* has been fired.
```js
cordova.plugins.notification.local.schedule({
title: 'My first notification',
text: 'Thats pretty easy...',
foreground: true
});
```
<p align="center">
<img src="images/ios-basic.png">
</p>
The plugin allows to schedule multiple notifications at once.
```js
cordova.plugins.notification.local.schedule([
{ id: 1, title: 'My first notification' },
{ id: 2, title: 'My first notification' }
]);
```
## Properties
A notification does have a set of configurable properties. Not all of them are supported across all platforms.
| Property | Property | Property | Property | Property | Property | Property | Property | Property |
| :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ |
| id | data | timeoutAfter | summary | led | clock | channelName | actions | alarmVolume |
| text | icon | attachments | smallIcon | color | defaults | launch | groupSummary | resetDelay |
| title | silent | progressBar | sticky | vibrate | priority | mediaSession | foreground | autoLaunch |
| sound | trigger | group | autoClear | lockscreen | number | badge | wakeup | channelId |
| iconType | wakeLockTimeout | triggerInApp | fullScreenIntent
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
});
```
## Actions
The plugin knows two types of actions: _button_ and _input_.
```js
cordova.plugins.notification.local.schedule({
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>
### 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',
}, ... ]
});
```
<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.
```js
cordova.plugins.notification.local.addActions('yes-no', [
{ id: 'yes', title: 'Yes' },
{ id: 'no', title: 'No' }
]);
```
Once you have defined an action group, you can reference it when scheduling notifications:
```js
cordova.plugins.notification.local.schedule({
title: 'Justin Rhyss',
text: 'Do you want to go see a movie tonight?',
actions: 'yes-no'
});
```
### 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
}
});
```
### Properties
The properties depend on the trigger type. Not all of them are supported across all platforms.
| Type | Property | Type | Value | Android | iOS | Windows |
| :----------- | :------------ | :------ | :--------------- | :------ | :-- | :------ |
| Fix |
| | at | Date | | x | x | x |
| Timespan |
| | in | Int | | x | x | x |
| | unit | String | `second` | x | x | x |
| | unit | String | `minute` | x | x | x |
| | unit | String | `hour` | x | x | x |
| | unit | String | `day` | x | x | x |
| | unit | String | `week` | x | x | x |
| | unit | String | `month` | x | x | x |
| | unit | String | `quarter` | x | x | x |
| | unit | String | `year` | x | x | x |
| Repeat |
| | count | Int | | x | | x |
| | every | String | `minute` | x | x | x |
| | every | String | `hour` | x | x | x |
| | every | String | `day` | x | x | x |
| | every | String | `week` | x | x | x |
| | every | String | `month` | x | x | x |
| | every | String | `quarter` | x | | x |
| | every | String | `year` | x | x | x |
| | before | Date | | x | | x |
| | firstAt | Date | | x | | x |
| Match |
| | count | Int | | x | | x |
| | every | Object | `minute` | x | x | x |
| | every | Object | `hour` | x | x | x |
| | every | Object | `day` | x | x | x |
| | every | Object | `weekday` | x | x | x |
| | every | Object | `weekdayOrdinal` | | x |
| | every | Object | `week` | | x |
| | every | Object | `weekOfMonth` | x | x | x |
| | every | Object | `month` | x | x | x |
| | every | Object | `quarter` | | x |
| | every | Object | `year` | x | x | x |
| | before | Date | | x | | x |
| | after | Date | | x | | x |
| Location |
| | center | Array | `[lat, long]` | | x |
| | radius | Int | | | x |
| | notifyOnEntry | Boolean | | | x |
| | notifyOnExit | Boolean | | | x |
| | single | Boolean | | | x |
## 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 too 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 });
```
On Android 8, special permissions are required to exit "do not disturb mode" (in case alarmVolume is defined).
You can check these by using:
```js
cordova.plugins.notification.local.hasDoNotDisturbPermissions(function (granted) { ... })
```
... and you can request them by using:
```js
cordova.plugins.notification.local.requestDoNotDisturbPermissions(function (granted) { ... })
```
The only downside to not having these permissions granted is that alarmVolume and vibrate may not be
honored on Android 8+ devices if the device is currently on silent when the notification fires (silent, not vibrate).
In this situation, the notification will fire silently but still appear in the notification bar.
Also on Android 8, it is helpful for alarms that autolaunch the app with an event, if the app can
ignore battery saving mode (otherwise alarms won't trigger reliably). You can check to see if the app is whitelisted for this with the following method.
```js
cordova.plugins.notification.local.isIgnoringBatteryOptimizations(function (granted) { ... })
```
... and you can request to be whitelisted by using:
```js
cordova.plugins.notification.local.requestIgnoreBatteryOptimizations(function (granted) { ... })
```
The request method here will work one of two ways.
1. If you have the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission defined in the manifest, it will use ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS to explicitly ignore battery optimizations for this app. This is the best overall user experience, but the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission seems to be frowned upon and can get your app banned. This plugin does not have this permission in plugin.xml for this reason, so you will need to use the cordova-custom-config plugin to add it to your config.xml
2. If you do not have REQUEST_IGNORE_BATTERY_OPTIMIZATIONS requested, it will launch ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS to show a list of all applications. You will want to put some sort of instructions prior to this to walk the user through this. Also, this action doesn't exist on all Android devices (is missing on Samsung phones), which will make this method simply return false if it can't start the activity.
## 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);
```
__Note:__ You have to provide the exact same callback to `cordova.plugins.notification.local.un` as you provided to `cordova.plugins.notification.local.on` to make unsubscribing work.
Hence you should define your callback as a separate function, not inline. If you want to use `this` inside of your callback, you also have to provide `this` as `scope` to `cordova.plugins.notification.local.on`.
### 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);
```
It might be possible that the underlying framework like __Ionic__ is not compatible with the launch process defined by cordova. With the result that the plugin fires the click event on app start before the app is able to listen for the events.
Therefore its possible to fire the queued events manually by defining a global variable.
```js
window.skipLocalNotificationReady = true
```
Once the app and Ionic is ready, you can fire the queued events manually.
```js
cordova.plugins.notification.local.fireQueuedEvents();
```
## Methods
All methods work asynchronous and accept callback methods.
See the sample app for how to use them.
| Method | Method | Method | Method | Method | Method |
| :------- | :---------------- | :-------------- | :------------- | :------------ | :--------------- |
| schedule | cancelAll | isTriggered | get | removeActions | un |
| update | hasPermission | getType | getAll | hasActions | fireQueuedEvents |
| clear | requestPermission | getIds | getScheduled | getDefaults | requestDoNotDisturbPermissions |
| clearAll | isPresent | getScheduledIds | getTriggered | setDefaults | hasDoNotDisturbPermissions |
| cancel | isScheduled | getTriggeredIds | addActions | on |
## 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-notifications.git
Or install from local source:
$ cordova plugin add <path> --nofetch --nosave --link
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
## License
This software is released under the [Apache 2.0 License][apache2_license].
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
[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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

View File

@ -0,0 +1,41 @@
{
"name": "cordova-plugin-local-notification",
"version": "0.9.0-beta.4",
"description": "Schedules and queries for local notifications",
"cordova": {
"id": "cordova-plugin-local-notification",
"platforms": [
"android",
"ios",
"windows"
]
},
"engines": [
{
"name": "cordova",
"version": ">=3.6.0"
},
{
"name": "cordova-android",
"version": ">=6.0.0"
},
{
"name": "cordova-windows",
"version": ">=4.2.0"
},
{
"name": "android-sdk",
"version": ">=26"
},
{
"name": "apple-ios",
"version": ">=10.0.0"
}
],
"author": "Sebastián Katzer",
"license": "Apache 2.0",
"bugs": {
"url": "https://github.com/katzer/cordova-plugin-local-notifications/issues"
},
"homepage": "https://github.com/katzer/cordova-plugin-local-notifications#readme"
}

View File

@ -0,0 +1,260 @@
<?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="cordova-plugin-local-notification"
version="0.9.0-beta.4">
<name>LocalNotification</name>
<description>Schedules and queries for local notifications</description>
<repo>https://github.com/katzer/cordova-plugin-local-notifications.git</repo>
<keywords>appplant, notification, local notification, user notification</keywords>
<license>Apache 2.0</license>
<author>Sebastián Katzer</author>
<!-- cordova -->
<engines>
<engine name="cordova" version=">=7.1.0" />
<engine name="cordova-plugman" version=">=4.3.0" />
<engine name="cordova-android" version=">=6.3.0" />
<engine name="cordova-windows" version=">=4.2.0" />
<!-- <engine name="android-sdk" version=">=26" /> -->
<engine name="apple-ios" version=">=10.0.0" />
</engines>
<!-- dependencies -->
<dependency id="cordova-plugin-device" />
<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" />
</js-module>
<!-- ios -->
<platform name="ios">
<config-file target="config.xml" parent="/*">
<feature name="LocalNotification">
<param name="ios-package" value="APPLocalNotification" onload="true" />
<param name="onload" value="true" />
</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/APPNotificationCategory.h" />
<source-file src="src/ios/APPNotificationCategory.m" />
<header-file src="src/ios/APPNotificationContent.h" />
<source-file src="src/ios/APPNotificationContent.m" />
<header-file src="src/ios/APPNotificationOptions.h" />
<source-file src="src/ios/APPNotificationOptions.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">
<preference name="ANDROID_SUPPORT_V4_VERSION" default="26.+" />
<framework src="com.android.support:support-v4:$ANDROID_SUPPORT_V4_VERSION" />
<framework src="src/android/build/localnotification.gradle" custom="true" type="gradleReference"/>
<config-file target="res/xml/config.xml" parent="/*">
<feature name="LocalNotification">
<param name="android-package" value="de.appplant.cordova.plugin.localnotification.LocalNotification"/>
</feature>
</config-file>
<config-file target="AndroidManifest.xml" parent="application">
<provider
android:name="de.appplant.cordova.plugin.notification.util.AssetProvider"
android:authorities="${applicationId}.localnotifications.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"
android:exported="false" />
<receiver
android:name="de.appplant.cordova.plugin.localnotification.ClearReceiver"
android:exported="false" />
<service
android:name="de.appplant.cordova.plugin.localnotification.ClickReceiver"
android:exported="false" />
<receiver
android:name="de.appplant.cordova.plugin.localnotification.RestoreReceiver"
android:directBootAware="true"
android:exported="false" >
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</config-file>
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
</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" />
<source-file
src="src/android/TriggerReceiver.java"
target-dir="src/de/appplant/cordova/plugin/localnotification" />
<source-file
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/action/ActionGroup.java"
target-dir="src/de/appplant/cordova/plugin/notification/action" />
<source-file
src="src/android/notification/receiver/AbstractClearReceiver.java"
target-dir="src/de/appplant/cordova/plugin/notification/receiver" />
<source-file
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/AbstractNotificationReceiver.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/trigger/DateTrigger.java"
target-dir="src/de/appplant/cordova/plugin/notification/trigger" />
<source-file
src="src/android/notification/trigger/IntervalTrigger.java"
target-dir="src/de/appplant/cordova/plugin/notification/trigger" />
<source-file
src="src/android/notification/trigger/MatchTrigger.java"
target-dir="src/de/appplant/cordova/plugin/notification/trigger" />
<source-file
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/util/LaunchUtils.java"
target-dir="src/de/appplant/cordova/plugin/notification/util" />
<source-file
src="src/android/notification/Builder.java"
target-dir="src/de/appplant/cordova/plugin/notification" />
<source-file
src="src/android/notification/Manager.java"
target-dir="src/de/appplant/cordova/plugin/notification" />
<source-file
src="src/android/notification/Notification.java"
target-dir="src/de/appplant/cordova/plugin/notification" />
<source-file
src="src/android/notification/NotificationVolumeManager.java"
target-dir="src/de/appplant/cordova/plugin/notification" />
<source-file
src="src/android/notification/Options.java"
target-dir="src/de/appplant/cordova/plugin/notification" />
<source-file
src="src/android/notification/Request.java"
target-dir="src/de/appplant/cordova/plugin/notification" />
</platform>
<!-- windows -->
<platform name="windows">
<framework src="src/windows/lib.UW/x86/LocalNotificationProxy.winmd" target-dir="x86" arch="x86" custom="true"/>
<framework src="src/windows/lib.UW/x64/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"/>
<framework src="src/windows/lib.UW/x86/Microsoft.Toolkit.Uwp.Notifications.dll" target-dir="x86" arch="x86" custom="true"/>
<framework src="src/windows/lib.UW/x64/Microsoft.Toolkit.Uwp.Notifications.dll" target-dir="x64" arch="x64" custom="true"/>
<framework src="src/windows/lib.UW/ARM/Microsoft.Toolkit.Uwp.Notifications.dll" target-dir="ARM" arch="ARM" custom="true"/>
<js-module src="src/windows/LocalNotificationProxy.js" name="LocalNotification.Proxy" >
<merges target="" />
</js-module>
</platform>
</plugin>

View File

@ -0,0 +1,61 @@
/*
* 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 de.appplant.cordova.plugin.notification.Notification;
import de.appplant.cordova.plugin.notification.receiver.AbstractClearReceiver;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.isAppRunning;
import static de.appplant.cordova.plugin.notification.Request.EXTRA_LAST;
/**
* 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.
* @param bundle The bundled extras.
*/
@Override
public void onClear (Notification notification, Bundle bundle) {
boolean isLast = bundle.getBoolean(EXTRA_LAST, false);
if (isLast) {
notification.cancel();
} else {
notification.clear();
}
if (isAppRunning()) {
fireEvent("clear", notification);
}
}
}

View File

@ -0,0 +1,109 @@
/*
* 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 androidx.core.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 de.appplant.cordova.plugin.notification.util.LaunchUtils;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent;
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();
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.getCharSequence(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;
LaunchUtils.launchApp(getApplicationContext());
}
/**
* If the notification was the last scheduled one by request.
*/
private boolean isLast() {
return getIntent().getBooleanExtra(EXTRA_LAST, false);
}
}

View File

@ -0,0 +1,780 @@
/*
* 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.
*/
// codebeat:disable[TOO_MANY_FUNCTIONS]
package de.appplant.cordova.plugin.localnotification;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.KeyguardManager;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Pair;
import android.view.View;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import javax.security.auth.callback.Callback;
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 android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
import static android.content.Context.POWER_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
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
* notifications. When a local notification is scheduled the alarm manager takes
* care of firing the event. When the event is processed, a notification is put
* in the Android notification center and status bar.
*/
@SuppressWarnings({ "Convert2Diamond", "Convert2Lambda" })
public class LocalNotification extends CordovaPlugin {
// Reference to the web view for static access
private static WeakReference<CordovaWebView> webView = null;
// Indicates if the device is ready (to receive events)
private static Boolean deviceready = false;
// Queues all events before deviceready
private static ArrayList<String> eventQueue = new ArrayList<String>();
// Launch details
private static Pair<Integer, String> launchDetails;
private static int REQUEST_PERMISSIONS_CALL = 10;
private static int REQUEST_IGNORE_BATTERY_CALL = 20;
private CallbackContext callbackContext;
/**
* 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.
*/
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
LocalNotification.webView = new WeakReference<CordovaWebView>(webView);
}
/**
* Called when the activity will start interacting with the user.
*
* @param multitasking Flag indicating if multitasking is turned on for app.
*/
@Override
public void onResume(boolean multitasking) {
super.onResume(multitasking);
deviceready();
}
/**
* The final call you receive before your activity is destroyed.
*/
@Override
public void onDestroy() {
deviceready = false;
}
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of
* work, use: cordova.getThreadPool().execute(runnable);
*
* 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.
*/
@Override
public boolean execute(final String action, final JSONArray args, final CallbackContext command)
throws JSONException {
if (action.equals("launch")) {
launch(command);
return true;
}
cordova.getThreadPool().execute(new Runnable() {
public void run() {
if (action.equals("ready")) {
deviceready();
} else if (action.equals("check")) {
check(command);
} else if (action.equals("request")) {
request(command);
} else if (action.equals("actions")) {
actions(args, command);
} else if (action.equals("schedule")) {
schedule(args, command);
} else if (action.equals("update")) {
update(args, command);
} else if (action.equals("cancel")) {
cancel(args, command);
} else if (action.equals("cancelAll")) {
cancelAll(command);
} else if (action.equals("clear")) {
clear(args, command);
} else if (action.equals("clearAll")) {
clearAll(command);
} else if (action.equals("type")) {
type(args, command);
} else if (action.equals("ids")) {
ids(args, command);
} else if (action.equals("notification")) {
notification(args, command);
} else if (action.equals("notifications")) {
notifications(args, command);
} else if (action.equals("hasDoNotDisturbPermissions")) {
hasDoNotDisturbPermissions(command);
} else if (action.equals("requestDoNotDisturbPermissions")) {
requestDoNotDisturbPermissions(command);
} else if (action.equals("isIgnoringBatteryOptimizations")) {
isIgnoringBatteryOptimizations(command);
} else if (action.equals("requestIgnoreBatteryOptimizations")) {
requestIgnoreBatteryOptimizations(command);
}
}
});
return true;
}
/**
* Determine if do not disturb permissions have been granted
*
* @return true if we still need to acquire do not disturb permissions.
*/
private boolean needsDoNotDisturbPermissions() {
Context mContext = this.cordova.getActivity().getApplicationContext();
NotificationManager mNotificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
return SDK_INT >= M && !mNotificationManager.isNotificationPolicyAccessGranted();
}
/**
* Determine if we have do not disturb permissions.
*
* @param command callback context. Returns with true if the we have
* permissions, false if we do not.
*/
private void hasDoNotDisturbPermissions(CallbackContext command) {
success(command, !needsDoNotDisturbPermissions());
}
/**
* Launch an activity to request do not disturb permissions
*
* @param command callback context. Returns with results of
* hasDoNotDisturbPermissions after the activity is closed.
*/
private void requestDoNotDisturbPermissions(CallbackContext command) {
if (needsDoNotDisturbPermissions()) {
this.callbackContext = command;
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginResult.setKeepCallback(true); // Keep callback
command.sendPluginResult(pluginResult);
Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
cordova.startActivityForResult(this, intent, REQUEST_PERMISSIONS_CALL);
return;
}
success(command, true);
}
/**
* Determine if do not battery optimization permissions have been granted
*
* @return true if we are succcessfully ignoring battery permissions.
*/
private boolean ignoresBatteryOptimizations() {
Context mContext = this.cordova.getActivity().getApplicationContext();
PowerManager pm = (PowerManager) mContext.getSystemService(POWER_SERVICE);
return SDK_INT <= M || pm.isIgnoringBatteryOptimizations(mContext.getPackageName());
}
/**
* Determine if we have do not disturb permissions.
*
* @param command callback context. Returns with true if the we have
* permissions, false if we do not.
*/
private void isIgnoringBatteryOptimizations(CallbackContext command) {
success(command, ignoresBatteryOptimizations());
}
/**
* Launch an activity to request do not disturb permissions
*
* @param command callback context. Returns with results of
* hasDoNotDisturbPermissions after the activity is closed.
*/
private void requestIgnoreBatteryOptimizations(CallbackContext command) {
if (!ignoresBatteryOptimizations()) {
this.callbackContext = command;
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginResult.setKeepCallback(true); // Keep callback
command.sendPluginResult(pluginResult);
String packageName = this.cordova.getContext().getPackageName();
String action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS;
// use the generic intent if we don't have access to request ignore permissions
// directly
// User can add "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" to the manifest, but
// risks having the app banned.
try {
PackageManager packageManager = this.cordova.getContext().getPackageManager();
PackageInfo pi = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
for (int i = 0; i < pi.requestedPermissions.length; ++i) {
if (pi.requestedPermissions[i].equals(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)) {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
}
}
} catch (PackageManager.NameNotFoundException e) {
// leave action as default if package not found
}
try {
Intent intent = new Intent(action);
intent.setData(Uri.parse("package:" + packageName));
cordova.startActivityForResult(this, intent, REQUEST_IGNORE_BATTERY_CALL);
} catch (ActivityNotFoundException e) {
// could not find the generic ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
// and did not have access to launch REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
// Fallback to just figuring out if battery optimizations are removed (probably
// not)
// since we can't ask the user to set it, because we can't launch an activity.
isIgnoringBatteryOptimizations(command);
this.callbackContext = null;
}
return;
}
success(command, true);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_PERMISSIONS_CALL && this.callbackContext != null) {
hasDoNotDisturbPermissions(this.callbackContext);
// clean up callback context.
this.callbackContext = null;
} else if (requestCode == REQUEST_IGNORE_BATTERY_CALL && this.callbackContext != null) {
isIgnoringBatteryOptimizations(this.callbackContext);
this.callbackContext = null;
}
super.onActivityResult(requestCode, resultCode, data);
}
/**
* 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.
*/
private void check(CallbackContext command) {
boolean allowed = getNotMgr().hasPermission();
success(command, allowed);
}
/**
* Request permission for local notifications.
*
* @param command The callback context used when calling back into JavaScript.
*/
private void request(CallbackContext command) {
check(command);
}
/**
* Register action group.
*
* @param args The exec() arguments in JSON form.
* @param command The callback context used when calling back into JavaScript.
*/
private void actions(JSONArray args, CallbackContext command) {
int task = args.optInt(0);
String id = args.optString(1);
JSONArray list = args.optJSONArray(2);
Context context = cordova.getActivity();
switch (task) {
case 0:
ActionGroup group = ActionGroup.parse(context, id, list);
ActionGroup.register(group);
command.success();
break;
case 1:
ActionGroup.unregister(id);
command.success();
break;
case 2:
boolean found = ActionGroup.isRegistered(id);
success(command, found);
break;
}
}
/**
* Schedule multiple local notifications.
*
* @param toasts The notifications to schedule.
* @param command The callback context used when calling back into JavaScript.
*/
private void schedule(JSONArray toasts, CallbackContext command) {
Manager mgr = getNotMgr();
for (int i = 0; i < toasts.length(); i++) {
JSONObject dict = toasts.optJSONObject(i);
Options options = new Options(dict);
Request request = new Request(options);
Notification toast = mgr.schedule(request, TriggerReceiver.class);
if (toast != null) {
fireEvent("add", toast);
}
}
check(command);
}
/**
* Update multiple local notifications.
*
* @param updates Notification properties including their IDs.
* @param command The callback context used when calling back into JavaScript.
*/
private void update(JSONArray updates, CallbackContext command) {
Manager mgr = getNotMgr();
for (int i = 0; i < updates.length(); i++) {
JSONObject update = updates.optJSONObject(i);
int id = update.optInt("id", 0);
Notification toast = mgr.update(id, update, TriggerReceiver.class);
if (toast == null)
continue;
fireEvent("update", toast);
}
check(command);
}
/**
* Cancel multiple local notifications.
*
* @param ids Set of local notification IDs.
* @param command The callback context used when calling back into JavaScript.
*/
private void cancel(JSONArray ids, CallbackContext command) {
Manager mgr = getNotMgr();
for (int i = 0; i < ids.length(); i++) {
int id = ids.optInt(i, 0);
Notification toast = mgr.cancel(id);
if (toast == null)
continue;
fireEvent("cancel", toast);
}
command.success();
}
/**
* Cancel all scheduled notifications.
*
* @param command The callback context used when calling back into JavaScript.
*/
private void cancelAll(CallbackContext command) {
getNotMgr().cancelAll();
fireEvent("cancelall");
command.success();
}
/**
* Clear multiple local notifications without canceling them.
*
* @param ids Set of local notification IDs.
* @param command The callback context used when calling back into JavaScript.
*/
private void clear(JSONArray ids, CallbackContext command) {
Manager mgr = getNotMgr();
for (int i = 0; i < ids.length(); i++) {
int id = ids.optInt(i, 0);
Notification toast = mgr.clear(id);
if (toast == null)
continue;
fireEvent("clear", toast);
}
command.success();
}
/**
* Clear all triggered notifications without canceling them.
*
* @param command The callback context used when calling back into JavaScript.
*/
private void clearAll(CallbackContext command) {
getNotMgr().clearAll();
fireEvent("clearall");
command.success();
}
/**
* Get the type of the notification (unknown, scheduled, triggered).
*
* @param args The exec() arguments in JSON form.
* @param command The callback context used when calling back into JavaScript.
*/
private void type(JSONArray args, CallbackContext command) {
int id = args.optInt(0);
Notification toast = getNotMgr().get(id);
if (toast == null) {
command.success("unknown");
return;
}
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 args The exec() arguments in JSON form.
* @param command The callback context used when calling back into JavaScript.
*/
private void ids(JSONArray args, CallbackContext command) {
int type = args.optInt(0);
Manager mgr = getNotMgr();
List<Integer> ids;
switch (type) {
case 0:
ids = mgr.getIds();
break;
case 1:
ids = mgr.getIdsByType(SCHEDULED);
break;
case 2:
ids = mgr.getIdsByType(TRIGGERED);
break;
default:
ids = new ArrayList<Integer>(0);
break;
}
command.success(new JSONArray(ids));
}
/**
* Options from local notification.
*
* @param args The exec() arguments in JSON form.
* @param command The callback context used when calling back into JavaScript.
*/
private void notification(JSONArray args, CallbackContext command) {
int id = args.optInt(0);
Options opts = getNotMgr().getOptions(id);
if (opts != null) {
command.success(opts.getDict());
} else {
command.success();
}
}
/**
* Set of options from local notification.
*
* @param args The exec() arguments in JSON form.
* @param command The callback context used when calling back into JavaScript.
*/
private void notifications(JSONArray args, CallbackContext command) {
int type = args.optInt(0);
JSONArray ids = args.optJSONArray(1);
Manager mgr = getNotMgr();
List<JSONObject> options;
switch (type) {
case 0:
options = mgr.getOptions();
break;
case 1:
options = mgr.getOptionsByType(SCHEDULED);
break;
case 2:
options = mgr.getOptionsByType(TRIGGERED);
break;
case 3:
options = mgr.getOptionsById(toList(ids));
break;
default:
options = new ArrayList<JSONObject>(0);
break;
}
command.success(new JSONArray(options));
}
/**
* Call all pending callbacks after the deviceready event has been fired.
*/
private static synchronized void deviceready() {
deviceready = true;
for (String js : eventQueue) {
sendJavascript(js);
}
eventQueue.clear();
}
/**
* Invoke success callback with a single boolean argument.
*
* @param command The callback context used when calling back into JavaScript.
* @param arg The single argument to pass through.
*/
private void success(CallbackContext command, boolean arg) {
PluginResult result = new PluginResult(PluginResult.Status.OK, arg);
command.sendPluginResult(result);
}
/**
* Fire given event on JS side. Does inform all event listeners.
*
* @param event The event name.
*/
private void fireEvent(String event) {
fireEvent(event, null, new JSONObject());
}
/**
* Fire given event on JS side. Does inform all event listeners.
*
* @param event The event name.
* @param notification Optional notification to pass with.
*/
static void fireEvent(String event, Notification notification) {
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", isInForeground());
data.put("queued", !deviceready);
if (toast != null) {
data.put("notification", toast.getId());
}
} catch (JSONException e) {
e.printStackTrace();
}
if (toast != null) {
params = toast.toString() + "," + data.toString();
} else {
params = data.toString();
}
js = "cordova.plugins.notification.local.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.
*/
private static synchronized void sendJavascript(final String js) {
if (!deviceready || webView == null) {
eventQueue.add(js);
return;
}
final CordovaWebView view = webView.get();
((Activity) (view.getContext())).runOnUiThread(new Runnable() {
public void run() {
view.loadUrl("javascript:" + js);
View engineView = view.getEngine().getView();
if (!isInForeground()) {
engineView.dispatchWindowVisibilityChanged(View.VISIBLE);
}
}
});
}
/**
* If the app is running in foreground.
*/
public static boolean isInForeground() {
if (!deviceready || webView == null)
return false;
CordovaWebView view = webView.get();
KeyguardManager km = (KeyguardManager) view.getContext().getSystemService(Context.KEYGUARD_SERVICE);
// noinspection SimplifiableIfStatement
if (km != null && km.isKeyguardLocked())
return false;
return view.getView().getWindowVisibility() == View.VISIBLE;
}
/**
* If the app is running.
*/
static boolean isAppRunning() {
return webView != null;
}
/**
* Convert JSON array of integers to List.
*
* @param ary Array of integers.
*/
private List<Integer> toList(JSONArray ary) {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < ary.length(); i++) {
list.add(ary.optInt(i));
}
return list;
}
/**
* Notification manager instance.
*/
private Manager getNotMgr() {
return Manager.getInstance(cordova.getActivity());
}
}
// codebeat:enable[TOO_MANY_FUNCTIONS]

View File

@ -0,0 +1,97 @@
/*
* 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.localnotification;
import android.content.Context;
import android.util.Log;
import java.util.Date;
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.Request;
import de.appplant.cordova.plugin.notification.receiver.AbstractRestoreReceiver;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.isAppRunning;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.isInForeground;
/**
* This class is triggered upon reboot of the device. It needs to re-register
* the alarms with the AlarmManager since these alarms are lost in case of
* reboot.
*/
public class RestoreReceiver extends AbstractRestoreReceiver {
/**
* Called when a local notification need to be restored.
*
* @param request Set of notification options.
* @param toast Wrapper around the local notification.
*/
@Override
public void onRestore (Request request, Notification toast) {
Date date = request.getTriggerDate();
boolean after = date != null && date.after(new Date());
if (!after && toast.isHighPrio()) {
performNotification(toast);
} else {
// reschedule if we aren't firing here.
// If we do fire, performNotification takes care of
// next schedule.
Context ctx = toast.getContext();
Manager mgr = Manager.getInstance(ctx);
if (after || toast.isRepeating()) {
mgr.schedule(request, TriggerReceiver.class);
}
}
}
@Override
public void dispatchAppEvent(String key, Notification notification) {
fireEvent(key, notification);
}
@Override
public boolean checkAppRunning() {
return isAppRunning();
}
/**
* Build notification specified by options.
*
* @param builder Notification builder.
*/
@Override
public Notification buildNotification (Builder builder) {
return builder
.setClickActivity(ClickReceiver.class)
.setClearReceiver(ClearReceiver.class)
.build();
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.content.Context;
import android.os.Bundle;
import android.os.PowerManager;
import java.util.Calendar;
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;
import de.appplant.cordova.plugin.notification.Request;
import de.appplant.cordova.plugin.notification.receiver.AbstractTriggerReceiver;
import de.appplant.cordova.plugin.notification.util.LaunchUtils;
import static android.content.Context.POWER_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.isAppRunning;
import static de.appplant.cordova.plugin.localnotification.LocalNotification.isInForeground;
import static java.util.Calendar.MINUTE;
import static android.os.Build.VERSION_CODES.P;
/**
* 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, re-schedule the alarm if necessary and fire trigger event.
*
* @param notification Wrapper around the local notification.
* @param bundle The bundled extras.
*/
@Override
public void onTrigger(Notification notification, Bundle bundle) {
performNotification(notification);
}
@Override
public void dispatchAppEvent(String key, Notification notification) {
fireEvent(key, notification);
}
@Override
public boolean checkAppRunning() {
return isAppRunning();
}
/**
* Build notification specified by options.
*
* @param builder Notification builder.
* @param bundle The bundled extras.
*/
@Override
public Notification buildNotification(Builder builder, Bundle bundle) {
return builder.setClickActivity(ClickReceiver.class).setClearReceiver(ClearReceiver.class).setExtras(bundle)
.build();
}
}

View File

@ -0,0 +1,32 @@
/*
* 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()
jcenter()
maven {
url "https://maven.google.com"
}
}
if (!project.ext.has('appShortcutBadgerVersion')) {
ext.appShortcutBadgerVersion = '1.1.22'
}
dependencies {
implementation "me.leolin:ShortcutBadger:${appShortcutBadgerVersion}@aar"
}

View File

@ -0,0 +1,507 @@
/*
* 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;
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 androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.MessagingStyle.Message;
import androidx.media.app.NotificationCompat.MediaStyle;
import android.support.v4.media.session.MediaSessionCompat;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Paint;
import android.graphics.Canvas;
import java.util.List;
import java.util.Random;
import de.appplant.cordova.plugin.notification.action.Action;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static de.appplant.cordova.plugin.notification.Notification.EXTRA_UPDATE;
/**
* Builder class for local notifications. Build fully configured local
* notification specified by JSON object passed from JS side.
*/
public final class Builder {
// Application context passed by constructor
private final Context context;
// Notification options passed by JS
private final Options options;
// To generate unique request codes
private final Random random = new Random();
// Receiver to handle the clear event
private Class<?> clearReceiver;
// Activity to handle the click event
private Class<?> clickActivity;
// Additional extras to merge into each intent
private Bundle extras;
/**
* Constructor
*
* @param options Notification options
*/
public Builder(Options options) {
this.context = options.getContext();
this.options = options;
}
/**
* Set clear receiver.
*
* @param receiver Broadcast receiver for the clear event.
*/
public Builder setClearReceiver(Class<?> receiver) {
this.clearReceiver = receiver;
return this;
}
/**
* Set click 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() {
NotificationCompat.Builder builder;
if (options.isSilent()) {
return new Notification(context, options);
}
Uri sound = options.getSound();
Bundle extras = new Bundle();
extras.putInt(Notification.EXTRA_ID, options.getId());
extras.putString(Options.EXTRA_SOUND, sound.toString());
builder = findOrCreateBuilder()
.setDefaults(options.getDefaults())
.setExtras(extras)
.setOnlyAlertOnce(false)
.setChannelId(options.getChannel())
.setContentTitle(options.getTitle())
.setContentText(options.getText())
.setTicker(options.getText())
.setNumber(options.getNumber())
.setAutoCancel(options.isAutoClear())
.setOngoing(options.isSticky())
.setColor(options.getColor())
.setVisibility(options.getVisibility())
.setPriority(options.getPrio())
.setShowWhen(options.showClock())
.setUsesChronometer(options.showChronometer())
.setGroup(options.getGroup())
.setGroupSummary(options.getGroupSummary())
.setTimeoutAfter(options.getTimeout())
.setLights(options.getLedColor(), options.getLedOn(), options.getLedOff());
if (!sound.equals(Uri.EMPTY) && !isUpdate()) {
builder.setSound(sound);
}
// API < 26. Setting sound to null will prevent playing if we have no sound for any reason,
// including a 0 volume.
if (options.isWithoutSound()) {
builder.setSound(null);
}
if (options.isWithProgressBar()) {
builder.setProgress(
options.getProgressMaxValue(),
options.getProgressValue(),
options.isIndeterminateProgress());
}
if (options.hasLargeIcon()) {
builder.setSmallIcon(options.getSmallIcon());
Bitmap largeIcon = options.getLargeIcon();
if (options.getLargeIconType().equals("circle")) {
largeIcon = getCircleBitmap(largeIcon);
}
builder.setLargeIcon(largeIcon);
} else {
builder.setSmallIcon(options.getSmallIcon());
}
if (options.useFullScreenIntent()) {
applyFullScreenIntent(builder);
}
applyStyle(builder);
applyActions(builder);
applyDeleteReceiver(builder);
applyContentReceiver(builder);
return new Notification(context, options, builder);
}
void applyFullScreenIntent(NotificationCompat.Builder builder) {
String pkgName = context.getPackageName();
Intent intent = context
.getPackageManager()
.getLaunchIntentForPackage(pkgName)
.putExtra("launchNotificationId", options.getId());
int reqCode = random.nextInt();
// request code and flags not added for demo purposes
PendingIntent pendingIntent = PendingIntent.getActivity(context, reqCode, intent, FLAG_UPDATE_CURRENT);
builder.setFullScreenIntent(pendingIntent, true);
}
/**
* Convert a bitmap to a circular bitmap.
* This code has been extracted from the Phonegap Plugin Push plugin:
* https://github.com/phonegap/phonegap-plugin-push
*
* @param bitmap Bitmap to convert.
* @return Circular bitmap.
*/
private Bitmap getCircleBitmap(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final int color = Color.RED;
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
float cx = bitmap.getWidth() / 2;
float cy = bitmap.getHeight() / 2;
float radius = cx < cy ? cx : cy;
canvas.drawCircle(cx, cy, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
bitmap.recycle();
return output;
}
/**
* 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;
}
MediaSessionCompat.Token token = options.getMediaSessionToken();
if (token != null) {
applyMediaStyle(builder, token);
return;
}
List<Bitmap> pics = options.getAttachments();
if (pics.size() > 0) {
applyBigPictureStyle(builder, pics);
return;
}
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;
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.
*/
private void applyDeleteReceiver(NotificationCompat.Builder builder) {
if (clearReceiver == null)
return;
Intent intent = new Intent(context, clearReceiver)
.setAction(options.getIdentifier())
.putExtra(Notification.EXTRA_ID, options.getId());
if (extras != null) {
intent.putExtras(extras);
}
int reqCode = random.nextInt();
PendingIntent deleteIntent = PendingIntent.getBroadcast(
context, reqCode, intent, FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(deleteIntent);
}
/**
* Set intent to handle the click event. Will bring the app to
* foreground.
*
* @param builder Local notification builder instance.
*/
private void applyContentReceiver(NotificationCompat.Builder builder) {
if (clickActivity == null)
return;
Intent intent = new Intent(context, clickActivity)
.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);
if (extras != null) {
intent.putExtras(extras);
}
int reqCode = random.nextInt();
PendingIntent contentIntent = PendingIntent.getService(
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)
.putExtra(Notification.EXTRA_ID, options.getId())
.putExtra(Action.EXTRA_ID, action.getId())
.putExtra(Options.EXTRA_LAUNCH, action.isLaunchingApp())
.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
if (extras != null) {
intent.putExtras(extras);
}
int reqCode = random.nextInt();
return PendingIntent.getService(
context, reqCode, intent, FLAG_UPDATE_CURRENT);
}
/**
* If the builder shall build an notification or an updated version.
*
* @return true in case of an updated version.
*/
private boolean isUpdate() {
return extras != null && extras.getBoolean(EXTRA_UPDATE, false);
}
/**
* Returns a cached builder instance or creates a new one.
*/
private NotificationCompat.Builder findOrCreateBuilder() {
int key = options.getId();
NotificationCompat.Builder builder = Notification.getCachedBuilder(key);
if (builder == null) {
builder = new NotificationCompat.Builder(context, options.getChannel());
}
return builder;
}
}

View File

@ -0,0 +1,468 @@
/*
* 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.
*/
// codebeat:disable[TOO_MANY_FUNCTIONS]
package de.appplant.cordova.plugin.notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioAttributes;
import android.net.Uri;
import android.service.notification.StatusBarNotification;
import androidx.core.app.NotificationManagerCompat;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import de.appplant.cordova.plugin.badge.BadgeImpl;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW;
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 final class Manager {
// The application context
private Context context;
/**
* Constructor
*
* @param context Application context
*/
private Manager(Context context) {
this.context = context;
}
/**
* Static method to retrieve class instance.
*
* @param context Application context
*/
public static Manager getInstance(Context context) {
return new Manager(context);
}
/**
* Check if app has local notification permission.
*/
public boolean hasPermission() {
return getNotCompMgr().areNotificationsEnabled();
}
/**
* Schedule local notification specified by request.
*
* @param request Set of notification options.
* @param receiver Receiver to handle the trigger event.
*/
public Notification schedule(Request request, Class<?> receiver) {
Options options = request.getOptions();
Notification toast = new Notification(context, options);
toast.schedule(request, receiver);
return toast;
}
/**
* Build channel with options
*
* @param soundUri Uri for custom sound (empty to use default)
* @param shouldVibrate whether not vibration should occur during the
* notification
* @param hasSound whether or not sound should play during the notification
* @param channelName the name of the channel (null will pick an appropriate
* default name for the options provided).
* @return channel ID of newly created (or reused) channel
*/
public String buildChannelWithOptions(Uri soundUri, boolean shouldVibrate, boolean hasSound,
CharSequence channelName, String channelId) {
String defaultChannelId, newChannelId;
CharSequence defaultChannelName;
int importance;
if (hasSound && shouldVibrate) {
defaultChannelId = Options.SOUND_VIBRATE_CHANNEL_ID;
defaultChannelName = Options.SOUND_VIBRATE_CHANNEL_NAME;
importance = IMPORTANCE_HIGH;
shouldVibrate = true;
} else if (hasSound) {
defaultChannelId = Options.SOUND_CHANNEL_ID;
defaultChannelName = Options.SOUND_CHANNEL_NAME;
importance = IMPORTANCE_DEFAULT;
shouldVibrate = false;
} else if (shouldVibrate) {
defaultChannelId = Options.VIBRATE_CHANNEL_ID;
defaultChannelName = Options.VIBRATE_CHANNEL_NAME;
importance = IMPORTANCE_LOW;
shouldVibrate = true;
} else {
defaultChannelId = Options.SILENT_CHANNEL_ID;
defaultChannelName = Options.SILENT_CHANNEL_NAME;
importance = IMPORTANCE_LOW;
shouldVibrate = false;
}
newChannelId = channelId != null ? channelId : defaultChannelId;
createChannel(newChannelId, channelName != null ? channelName : defaultChannelName, importance, shouldVibrate,
soundUri);
return newChannelId;
}
/**
* Create a channel
*/
public void createChannel(String channelId, CharSequence channelName, int importance, Boolean shouldVibrate,
Uri soundUri) {
NotificationManager mgr = getNotMgr();
if (SDK_INT < O)
return;
NotificationChannel channel = mgr.getNotificationChannel(channelId);
if (channel != null)
return;
channel = new NotificationChannel(channelId, channelName, importance);
channel.enableVibration(shouldVibrate);
if (!soundUri.equals(Uri.EMPTY)) {
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build();
channel.setSound(soundUri, attributes);
}
mgr.createNotificationChannel(channel);
}
/**
* Update 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;
notification.update(updates, receiver);
return notification;
}
/**
* Clear local notification specified by ID.
*
* @param id The notification ID.
*/
public Notification clear(int id) {
Notification toast = get(id);
if (toast != null) {
toast.clear();
}
return toast;
}
/**
* Clear all local notifications.
*/
public void clearAll() {
List<Notification> toasts = getByType(TRIGGERED);
for (Notification toast : toasts) {
toast.clear();
}
getNotCompMgr().cancelAll();
setBadge(0);
}
/**
* Clear local notification specified by ID.
*
* @param id The notification ID
*/
public Notification cancel(int id) {
Notification toast = get(id);
if (toast != null) {
toast.cancel();
}
return toast;
}
/**
* Cancel all local notifications.
*/
public void cancelAll() {
List<Notification> notifications = getAll();
for (Notification notification : notifications) {
notification.cancel();
}
getNotCompMgr().cancelAll();
setBadge(0);
}
/**
* All local notifications IDs.
*/
public List<Integer> getIds() {
Set<String> keys = getPrefs().getAll().keySet();
List<Integer> ids = new ArrayList<Integer>();
for (String key : keys) {
try {
ids.add(Integer.parseInt(key));
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
return ids;
}
/**
* All local notification IDs for given type.
*
* @param type The notification life cycle type
*/
public List<Integer> getIdsByType(Notification.Type type) {
if (type == Notification.Type.ALL)
return getIds();
StatusBarNotification[] activeToasts = 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.
*/
private List<Notification> getByIds(List<Integer> ids) {
List<Notification> toasts = new ArrayList<Notification>();
for (int id : ids) {
Notification toast = get(id);
if (toast != null) {
toasts.add(toast);
}
}
return toasts;
}
/**
* List of all local notification.
*/
public List<Notification> getAll() {
return getByIds(getIds());
}
/**
* List of local notifications from given type.
*
* @param type The notification life cycle type
*/
private List<Notification> getByType(Notification.Type type) {
if (type == Notification.Type.ALL)
return getAll();
List<Integer> ids = getIdsByType(type);
return getByIds(ids);
}
/**
* List of properties from all local notifications.
*/
public List<JSONObject> getOptions() {
return getOptionsById(getIds());
}
/**
* List of properties from local notifications with matching ID.
*
* @param ids Set of notification IDs
*/
public List<JSONObject> getOptionsById(List<Integer> ids) {
List<JSONObject> toasts = new ArrayList<JSONObject>();
for (int id : ids) {
Options options = getOptions(id);
if (options != null) {
toasts.add(options.getDict());
}
}
return toasts;
}
/**
* List of properties from all local notifications from given type.
*
* @param type The notification life cycle type
*/
public List<JSONObject> getOptionsByType(Notification.Type type) {
ArrayList<JSONObject> options = new ArrayList<JSONObject>();
List<Notification> notifications = getByType(type);
for (Notification notification : notifications) {
options.add(notification.getOptions().getDict());
}
return options;
}
/**
* Get local notification options.
*
* @param id Notification ID.
*
* @return null if could not found.
*/
public Options getOptions(int id) {
SharedPreferences prefs = getPrefs();
String toastId = Integer.toString(id);
if (!prefs.contains(toastId))
return null;
try {
String json = prefs.getString(toastId, null);
JSONObject dict = new JSONObject(json);
return new Options(context, dict);
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
/**
* Get existent local notification.
*
* @param id Notification ID.
*
* @return null if could not found.
*/
public Notification get(int id) {
Options options = getOptions(id);
if (options == null)
return null;
return new Notification(context, options);
}
/**
* Set the badge number of the app icon.
*
* @param badge The badge number.
*/
public void setBadge(int badge) {
if (badge == 0) {
new BadgeImpl(context).clearBadge();
} else {
new BadgeImpl(context).setBadge(badge);
}
}
/**
* Get all active status bar notifications.
*/
StatusBarNotification[] getActiveNotifications() {
if (SDK_INT >= M) {
return getNotMgr().getActiveNotifications();
} else {
return new StatusBarNotification[0];
}
}
/**
* Shared private preferences for the application.
*/
private SharedPreferences getPrefs() {
return context.getSharedPreferences(PREF_KEY_ID, Context.MODE_PRIVATE);
}
/**
* Notification manager for the application.
*/
private NotificationManager getNotMgr() {
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Notification compat manager for the application.
*/
private NotificationManagerCompat getNotCompMgr() {
return NotificationManagerCompat.from(context);
}
}
// codebeat:enable[TOO_MANY_FUNCTIONS]

View File

@ -0,0 +1,503 @@
/*
* 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;
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.media.AudioManager;
import android.net.Uri;
import android.service.notification.StatusBarNotification;
import android.util.Pair;
import android.util.Log;
import android.util.SparseArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import androidx.collection.ArraySet;
import androidx.core.app.NotificationCompat;
import static android.app.AlarmManager.RTC;
import static android.app.AlarmManager.RTC_WAKEUP;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
import static androidx.core.app.NotificationCompat.PRIORITY_MAX;
import static androidx.core.app.NotificationCompat.PRIORITY_MIN;
import static java.lang.Thread.sleep;
/**
* Wrapper class around OS notification class. Handles basic operations
* like show, delete, cancel for a single local notification instance.
*/
public final class Notification {
// Used to differ notifications by their life cycle state
public enum Type {
ALL, SCHEDULED, TRIGGERED
}
// Extra key for the id
public static final String EXTRA_ID = "NOTIFICATION_ID";
// Extra key for the update flag
public static final String EXTRA_UPDATE = "NOTIFICATION_UPDATE";
// Key for private preferences
static final String PREF_KEY_ID = "NOTIFICATION_ID";
// Key for private preferences
private static final String PREF_KEY_PID = "NOTIFICATION_PID";
// Cache for the builder instances
private static SparseArray<NotificationCompat.Builder> cache = null;
// Application context passed by constructor
private final Context context;
// Notification options passed by JS
private final Options options;
// Builder with full configuration
private final NotificationCompat.Builder builder;
/**
* Constructor
*
* @param context Application context.
* @param options Parsed notification options.
* @param builder Pre-configured notification builder.
*/
Notification (Context context, Options options, NotificationCompat.Builder builder) {
this.context = context;
this.options = options;
this.builder = builder;
}
/**
* 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;
}
/**
* Get application context.
*/
public Context getContext() {
return context;
}
/**
* Get notification options.
*/
public Options getOptions() {
return options;
}
/**
* Get notification ID.
*/
public int getId() {
return options.getId();
}
/**
* If it's a repeating notification.
*/
public boolean isRepeating() {
return getOptions().getTrigger().has("every");
}
/**
* If the notifications priority is high or above.
*/
public boolean isHighPrio() {
return getOptions().getPrio() >= PRIORITY_HIGH;
}
/**
* Notification type can be one of triggered or scheduled.
*/
public Type getType() {
Manager mgr = Manager.getInstance(context);
StatusBarNotification[] toasts = mgr.getActiveNotifications();
int id = getId();
for (StatusBarNotification toast : toasts) {
if (toast.getId() == id) {
return Type.TRIGGERED;
}
}
return Type.SCHEDULED;
}
/**
* Schedule the local notification.
*
* @param request Set of notification options.
* @param receiver Receiver to handle the trigger event.
*/
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();
cancelScheduledAlarms();
do {
Date date = request.getTriggerDate();
Log.d("local-notification", "Next trigger at: " + date);
if (date == null)
continue;
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()) {
unpersist();
return;
}
persist(ids);
if (!options.isInfiniteTrigger()) {
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_UPDATE_CURRENT);
try {
switch (options.getPrio()) {
case PRIORITY_MIN:
mgr.setExact(RTC, time, pi);
break;
case PRIORITY_MAX:
if (SDK_INT >= M) {
AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(time, pi);
mgr.setAlarmClock(info, pi);
} else {
mgr.setExact(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
}
}
}
/**
* Trigger local notification specified by options.
*
* @param intent The intent to broadcast.
* @param cls The broadcast class.
*
* @return false if the receiver could not be invoked.
*/
private boolean trigger (Intent intent, Class<?> cls) {
BroadcastReceiver receiver;
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() {
getNotMgr().cancel(getId());
if (isRepeating()) return;
unpersist();
}
/**
* Cancel the local notification.
*/
public void cancel() {
cancelScheduledAlarms();
unpersist();
getNotMgr().cancel(getId());
clearCache();
}
/**
* Cancel the scheduled future local notification.
*
* Create an intent that looks similar, to the one that was registered
* using schedule. Making sure the notification id in the action is the
* same. Now we can search for such an intent using the 'getService'
* method and cancel it.
*/
private void cancelScheduledAlarms() {
SharedPreferences prefs = getPrefs(PREF_KEY_PID);
String id = options.getIdentifier();
Set<String> actions = prefs.getStringSet(id, null);
if (actions == null)
return;
for (String action : actions) {
Intent intent = new Intent(action);
PendingIntent pi = PendingIntent.getBroadcast(
context, 0, intent, 0);
if (pi != null) {
getAlarmMgr().cancel(pi);
}
}
}
/**
* Present the local notification to user.
*/
public void show() {
if (builder == null) return;
if (options.showChronometer()) {
cacheBuilder();
}
grantPermissionToPlaySoundFromExternal();
new NotificationVolumeManager(context, options)
.adjustAlarmVolume();
getNotMgr().notify(getId(), builder.build());
}
/**
* Update the notification properties.
*
* @param updates The properties to update.
* @param receiver Receiver to handle the trigger event.
*/
void update (JSONObject updates, Class<?> receiver) {
mergeJSONObjects(updates);
persist(null);
if (getType() != Type.TRIGGERED)
return;
Intent intent = new Intent(context, receiver)
.setAction(PREF_KEY_ID + options.getId())
.putExtra(Notification.EXTRA_ID, options.getId())
.putExtra(Notification.EXTRA_UPDATE, true);
trigger(intent, receiver);
}
/**
* Encode options to JSON.
*/
public String toString() {
JSONObject dict = options.getDict();
JSONObject json = new JSONObject();
try {
json = new JSONObject(dict.toString());
} catch (JSONException e) {
e.printStackTrace();
}
return json.toString();
}
/**
* 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 (Set<String> ids) {
String id = options.getIdentifier();
SharedPreferences.Editor editor;
editor = getPrefs(PREF_KEY_ID).edit();
editor.putString(id, options.toString());
editor.apply();
if (ids == null)
return;
editor = getPrefs(PREF_KEY_PID).edit();
editor.putStringSet(id, ids);
editor.apply();
}
/**
* Remove the notification from the Android shared Preferences.
*/
private void unpersist () {
String[] keys = { PREF_KEY_ID, PREF_KEY_PID };
String id = options.getIdentifier();
SharedPreferences.Editor editor;
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);
}
/**
* Merge two JSON objects.
*/
private void mergeJSONObjects (JSONObject updates) {
JSONObject dict = options.getDict();
Iterator it = updates.keys();
while (it.hasNext()) {
try {
String key = (String)it.next();
dict.put(key, updates.opt(key));
} catch (JSONException e) {
e.printStackTrace();
}
}
}
/**
* Caches the builder instance so it can be used later.
*/
private void cacheBuilder() {
if (cache == null) {
cache = new SparseArray<NotificationCompat.Builder>();
}
cache.put(getId(), builder);
}
/**
* Find the cached builder instance.
*
* @param key The key under where to look for the builder.
*
* @return null if no builder instance could be found.
*/
static NotificationCompat.Builder getCachedBuilder (int key) {
return (cache != null) ? cache.get(key) : null;
}
/**
* Caches the builder instance so it can be used later.
*/
private void clearCache () {
if (cache != null) {
cache.delete(getId());
}
}
/**
* Shared private preferences for the application.
*/
private SharedPreferences getPrefs (String key) {
return context.getSharedPreferences(key, Context.MODE_PRIVATE);
}
/**
* Notification manager for the application.
*/
private NotificationManager getNotMgr () {
return (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Alarm manager for the application.
*/
private AlarmManager getAlarmMgr () {
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
}

View File

@ -0,0 +1,233 @@
package de.appplant.cordova.plugin.notification;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.util.Log;
import java.util.Timer;
import java.util.TimerTask;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
import static java.lang.Thread.sleep;
/**
* Class to handle all notification volume changes
*/
public class NotificationVolumeManager {
/**
* Amount of time to sleep while polling to see if all volume writers are closed.
*/
final private int VOLUME_WRITER_POLLING_DURATION = 200;
/**
* Key for volume writer counter in shared preferences
*/
final private String VOLUME_CONFIG_WRITER_COUNT_KEY = "volumeConfigWriterCount";
/**
* Tag for logs
*/
final String TAG = "NotificationVolumeMgr";
/**
* Notification manager
*/
private NotificationManager notificationManager;
/**
* Audio Manager
*/
private AudioManager audioManager;
/**
* Shared preferences, used to store settings across processes
*/
private SharedPreferences settings;
/**
* Options for the notification
*/
private Options options;
/**
* Initialize the NotificationVolumeManager
* @param context Application context
*/
public NotificationVolumeManager (Context context, Options options) {
this.settings = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
this.notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
this.options = options;
}
/**
* Ensure that this is the only volume writer.
* Wait until others have closed.
* TODO: Better locking mechanism to ensure concurrency (file lock?)
* @throws InterruptedException Throws an interrupted exception, required by sleep call.
*/
@SuppressLint("ApplySharedPref")
private void ensureOnlyVolumeWriter () throws InterruptedException {
int writerCount = settings.getInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0) + 1;
settings.edit().putInt(VOLUME_CONFIG_WRITER_COUNT_KEY, writerCount).commit();
int resetDelay = options.getResetDelay();
if (resetDelay == 0) {
resetDelay = Options.DEFAULT_RESET_DELAY;
}
int resetDelayMs = resetDelay * 1000;
int sleepTotal = 0;
// Wait until we are the only writer left.
while(writerCount > 1) {
if (sleepTotal > resetDelayMs) {
throw new InterruptedException("Volume writer timeout exceeded reset delay." +
"Something must have gone wrong. Reset volume writer counts to 0 " +
"and reset volume settings to user settings.");
}
sleep(VOLUME_WRITER_POLLING_DURATION);
sleepTotal += VOLUME_WRITER_POLLING_DURATION;
writerCount = settings.getInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0);
}
}
/**
* Remove one count from active volume writers. Used when writer is finished.
*/
@SuppressLint("ApplySharedPref")
private void decrementVolumeWriter () {
int writerCount = settings.getInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0) - 1;
settings.edit().putInt(VOLUME_CONFIG_WRITER_COUNT_KEY, Math.max(writerCount, 0)).commit();
}
/**
* Reset volume writer counts to 0. To be used in error conditions.
*/
@SuppressLint("ApplySharedPref")
private void resetVolumeWriter () {
settings.edit().putInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0).commit();
}
/**
* Set the volume for our ringer
* @param ringerMode ringer mode enum. Normal ringer or vibration.
* @param volume volume.
*/
private void setVolume (int ringerMode, int volume) {
// After delay, user could have set phone to do not disturb.
// If so and we can't change the ringer, quit so we don't create an error condition
if (canChangeRinger()) {
// Change ringer mode
audioManager.setRingerMode(ringerMode);
// Change to new Volume
audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, volume, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
}
/**
* Set the volume to the last user settings from shared preferences.
*/
private void setVolumeToUserSettings () {
int ringMode = settings.getInt("userRingerMode", -1);
int volume = settings.getInt("userVolume", -1);
setVolume(ringMode, volume);
}
/**
* Figure out if we can change the ringer.
* In Android M+, we can't change out of do not disturb if we don't have explicit permission.
* @return whether or not we can change the ringer.
*/
private boolean canChangeRinger() {
return SDK_INT < M || notificationManager.isNotificationPolicyAccessGranted()
|| audioManager.getRingerMode() != AudioManager.RINGER_MODE_SILENT;
}
/**
* Adjusts alarm Volume
* Options object. Contains our volume, reset and vibration settings.
*/
@SuppressLint("ApplySharedPref")
public void adjustAlarmVolume () {
Integer volume = options.getVolume();
if (volume.equals(Options.VOLUME_NOT_SET) || !canChangeRinger()) {
return;
}
try {
ensureOnlyVolumeWriter();
boolean vibrate = options.isWithVibration();
int delay = options.getResetDelay();
if (delay <= 0) {
delay = Options.DEFAULT_RESET_DELAY;
}
// Count of all alarms currently sounding
Integer count = settings.getInt("alarmCount", 0);
settings.edit().putInt("alarmCount", count + 1).commit();
// Get current phone volume
int userVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION);
// Get Ringer mode
int userRingerMode = audioManager.getRingerMode();
// If this is the first alarm store the users ringer and volume settings
if (count.equals(0)) {
settings.edit().putInt("userVolume", userVolume).apply();
settings.edit().putInt("userRingerMode", userRingerMode).apply();
}
// Calculates a new volume based on the study configure volume percentage and the devices max volume integer
if (volume > 0) {
// Gets devices max volume integer
int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
// Calculates new volume based on devices max volume
double newVolume = Math.ceil(maxVolume * (volume / 100.00));
setVolume(AudioManager.RINGER_MODE_NORMAL, (int) newVolume);
} else {
// Volume of 0
if (vibrate) {
// Change mode to vibrate
setVolume(AudioManager.RINGER_MODE_VIBRATE, 0);
}
}
// Timer to change users sound back
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
int currentCount = settings.getInt("alarmCount", 0);
currentCount = Math.max(currentCount - 1, 0);
settings.edit().putInt("alarmCount", currentCount).apply();
if (currentCount == 0) {
setVolumeToUserSettings();
}
}
}, delay * 1000);
} catch (InterruptedException e) {
Log.e(TAG, "interrupted waiting for volume set. "
+ "Reset to user setting, and set counts to 0: " + e.toString());
resetVolumeWriter();
setVolumeToUserSettings();
} finally {
decrementVolumeWriter();
}
}
}

View File

@ -0,0 +1,755 @@
/*
* 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.
*/
// codebeat:disable[TOO_MANY_FUNCTIONS]
package de.appplant.cordova.plugin.notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.MessagingStyle.Message;
import android.support.v4.media.session.MediaSessionCompat;
import org.json.JSONArray;
import org.json.JSONObject;
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.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
import static androidx.core.app.NotificationCompat.DEFAULT_LIGHTS;
import static androidx.core.app.NotificationCompat.DEFAULT_SOUND;
import static androidx.core.app.NotificationCompat.DEFAULT_VIBRATE;
import static androidx.core.app.NotificationCompat.PRIORITY_MAX;
import static androidx.core.app.NotificationCompat.PRIORITY_MIN;
import static androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC;
import static androidx.core.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 final class Options {
// Default Channel ID for SDK < 26
static final String DEFAULT_CHANNEL_ID = "default-channel-id";
// Silent channel
static final String SILENT_CHANNEL_ID = "silent-channel-id";
static final CharSequence SILENT_CHANNEL_NAME = "Silent Notifications";
// Vibrate only channel
static final String VIBRATE_CHANNEL_ID = "vibrate-channel-id";
static final CharSequence VIBRATE_CHANNEL_NAME = "Low Priority Notifications";
// Sound only channel
static final String SOUND_CHANNEL_ID = "sound-channel-id";
static final CharSequence SOUND_CHANNEL_NAME = "Medium Priority Notifications";
// Sound and vibrate channel
static final String SOUND_VIBRATE_CHANNEL_ID = "sound-vibrate-channel-id";
static final CharSequence SOUND_VIBRATE_CHANNEL_NAME = "High Priority Notifications";
// Key name for bundled sound extra
static final String EXTRA_SOUND = "NOTIFICATION_SOUND";
// 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";
public final static Integer DEFAULT_RESET_DELAY = 5;
public final static Integer VOLUME_NOT_SET = -1;
// Default wakelock timeout
public final static Integer DEFAULT_WAKE_LOCK_TIMEOUT = 15000;
// Default icon type
private static final String DEFAULT_ICON_TYPE = "square";
// The original JSON object
private final JSONObject options;
// The application context
private final Context context;
// Asset util instance
private final AssetUtil assets;
/**
* When creating without a context, various methods might not work well.
*
* @param options The options dict map.
*/
public Options(JSONObject options) {
this.options = options;
this.context = null;
this.assets = null;
}
/**
* Constructor
*
* @param context The application context.
* @param options The options dict map.
*/
public Options(Context context, JSONObject options) {
this.context = context;
this.options = options;
this.assets = AssetUtil.getInstance(context);
}
/**
* Application context.
*/
public Context getContext() {
return context;
}
/**
* Wrapped JSON object.
*/
public JSONObject getDict() {
return options;
}
/**
* JSON object as string.
*/
public String toString() {
return options.toString();
}
/**
* Gets the ID for the local notification.
*
* @return 0 if the user did not specify.
*/
public Integer getId() {
return options.optInt("id", 0);
}
/**
* The identifier for the local notification.
*
* @return The notification ID as the string
*/
String getIdentifier() {
return getId().toString();
}
/**
* Badge number for the local notification.
*/
public int getBadgeNumber() {
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 isSticky() {
return options.optBoolean("sticky", false);
}
/**
* autoClear flag for local notifications.
*/
Boolean isAutoClear() {
return options.optBoolean("autoClear", false);
}
/**
* Gets the raw trigger spec as provided by the user.
*/
public JSONObject getTrigger() {
return options.optJSONObject("trigger");
}
/**
* Gets the value of the silent flag.
*/
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);
}
/**
* flag to auto-launch the application as the notification fires
*/
public boolean isAutoLaunchingApp() {
return options.optBoolean("autoLaunch", true);
}
/**
* wakeup flag for the notification.
*/
public boolean shallWakeUp() {
return options.optBoolean("wakeup", true);
}
/**
* Use a fullScreenIntent
*/
public boolean useFullScreenIntent() { return options.optBoolean("fullScreenIntent", true); }
/**
* Whether or not to trigger a notification in the app.
*/
public boolean triggerInApp() { return options.optBoolean("triggerInApp", false); }
/**
* Timeout for wakeup (only used if shallWakeUp() is true)
*/
public int getWakeLockTimeout() {
return options.optInt("wakeLockTimeout", DEFAULT_WAKE_LOCK_TIMEOUT);
}
/**
* Gets the value for the timeout flag.
*/
long getTimeout() {
return options.optLong("timeoutAfter");
}
/**
* The channel id of that notification.
*/
String getChannel() {
// If we have a low enough SDK for it not to matter,
// short-circuit.
if (SDK_INT < O) {
return DEFAULT_CHANNEL_ID;
}
Uri soundUri = getSound();
boolean hasSound = !isWithoutSound();
boolean shouldVibrate = isWithVibration();
CharSequence channelName = options.optString("channelName", null);
String channelId = options.optString("channelId", null);
channelId = Manager.getInstance(context).buildChannelWithOptions(soundUri, shouldVibrate, hasSound, channelName,
channelId);
return channelId;
}
/**
* If the group shall show a summary.
*/
boolean getGroupSummary() {
return options.optBoolean("groupSummary", false);
}
/**
* Text for the local notification.
*/
public String getText() {
Object text = options.opt("text");
return text instanceof String ? (String) text : "";
}
/**
* Title for the local notification.
*/
public String getTitle() {
String title = options.optString("title", "");
if (title.isEmpty()) {
title = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
}
return title;
}
/**
* The notification color for LED.
*/
int getLedColor() {
Object cfg = options.opt("led");
String hex = null;
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");
}
if (hex == null)
return 0;
try {
hex = stripHex(hex);
int aRGB = Integer.parseInt(hex, 16);
return aRGB + 0xFF000000;
} catch (NumberFormatException e) {
e.printStackTrace();
}
return 0;
}
/**
* The notification color for LED.
*/
int getLedOn() {
Object cfg = options.opt("led");
int defVal = 1000;
if (cfg instanceof JSONArray)
return options.optJSONArray("led").optInt(1, defVal);
if (cfg instanceof JSONObject)
return options.optJSONObject("led").optInt("on", defVal);
return defVal;
}
/**
* The notification color for LED.
*/
int getLedOff() {
Object cfg = options.opt("led");
int defVal = 1000;
if (cfg instanceof JSONArray)
return options.optJSONArray("led").optInt(2, defVal);
if (cfg instanceof JSONObject)
return options.optJSONObject("led").optInt("off", defVal);
return defVal;
}
/**
* 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)
return NotificationCompat.COLOR_DEFAULT;
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 NotificationCompat.COLOR_DEFAULT;
}
/**
* Sound file path for the local notification.
*/
Uri getSound() {
return assets.parse(options.optString("sound", null));
}
/**
* Icon resource ID for the local notification.
*/
boolean hasLargeIcon() {
String icon = options.optString("icon", null);
return icon != null;
}
/**
* Icon bitmap for the local notification.
*/
Bitmap getLargeIcon() {
String icon = options.optString("icon", null);
Uri uri = assets.parse(icon);
Bitmap bmp = null;
try {
bmp = assets.getIconFromUri(uri);
} catch (Exception e) {
e.printStackTrace();
}
return bmp;
}
/**
* Type of the large icon.
*/
String getLargeIconType() {
return options.optString("iconType", DEFAULT_ICON_TYPE);
}
/**
* Small icon resource ID for the local notification.
*/
int getSmallIcon() {
String icon = options.optString("smallIcon", DEFAULT_ICON);
int resId = assets.getResId(icon);
if (resId == 0) {
resId = assets.getResId(DEFAULT_ICON);
}
if (resId == 0) {
resId = android.R.drawable.ic_popup_reminder;
}
return resId;
}
/**
* Get the volume
*/
public Integer getVolume() {
return options.optInt("alarmVolume", VOLUME_NOT_SET);
}
/**
* Returns the resetDelay until the sound changes revert back to the users
* settings.
*
* @return resetDelay
*/
public Integer getResetDelay() {
return options.optInt("resetDelay", DEFAULT_RESET_DELAY);
}
/**
* If the phone should vibrate.
*/
public boolean isWithVibration() {
return options.optBoolean("vibrate", true);
}
/**
* If the phone should play no sound.
*/
public boolean isWithoutSound() {
Object value = options.opt("sound");
return value == null || value.equals(false) || options.optInt("alarmVolume") == 0;
}
/**
* If the phone should play the default sound.
*/
public 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 getPrio() {
int prio = options.optInt("priority");
return Math.min(Math.max(prio, PRIORITY_MIN), PRIORITY_MAX);
}
/**
* If the notification shall show the when date.
*/
boolean showClock() {
Object clock = options.opt("clock");
return (clock instanceof Boolean) ? (Boolean) clock : true;
}
/**
* If the notification shall show the when date.
*/
boolean showChronometer() {
Object clock = options.opt("clock");
return (clock instanceof String) && clock.equals("chronometer");
}
/**
* 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);
}
/**
* If the trigger shall be infinite.
*/
public boolean isInfiniteTrigger() {
JSONObject trigger = options.optJSONObject("trigger");
return trigger.has("every") && trigger.optInt("count", -1) < 0;
}
/**
* 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() {
Object value = options.opt("actions");
String groupId = null;
JSONArray actions = null;
ActionGroup group = null;
if (value instanceof String) {
groupId = (String) value;
} else if (value instanceof JSONArray) {
actions = (JSONArray) value;
}
if (groupId != null) {
group = ActionGroup.lookup(groupId);
} else if (actions != null && actions.length() > 0) {
group = ActionGroup.parse(context, actions);
}
return (group != null) ? group.getActions() : 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;
}
}
// codebeat:enable[TOO_MANY_FUNCTIONS]

View File

@ -0,0 +1,277 @@
/*
* 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 notifications 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;
/**
* Create a request with a base date specified through the passed options.
*
* @param options The options spec.
*/
public Request(Options options) {
this.options = options;
this.spec = options.getTrigger();
this.count = Math.max(spec.optInt("count"), 1);
this.trigger = buildTrigger();
this.triggerDate = trigger.getNextTriggerDate(getBaseDate());
}
/**
* Create a request with a base date specified via base argument.
*
* @param options The options spec.
* @param base The base date from where to calculate the next trigger.
*/
public Request(Options options, Date base) {
this.options = options;
this.spec = options.getTrigger();
this.count = Math.max(spec.optInt("count"), 1);
this.trigger = buildTrigger();
this.triggerDate = trigger.getNextTriggerDate(base);
}
/**
* Gets the options spec.
*/
public Options getOptions() {
return options;
}
/**
* The identifier for the request.
*
* @return The notification ID as the string
*/
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.
*/
public Date getTriggerDate() {
Calendar now = Calendar.getInstance();
if (triggerDate == null)
return null;
long time = triggerDate.getTime();
if ((now.getTimeInMillis() - time) > 60000)
return null;
if (time >= spec.optLong("before", time + 1))
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) {
List<Integer> cmp1 = getMatchingComponents();
List<Integer> cmp2 = getSpecialMatchingComponents();
return new MatchTrigger(cmp1, cmp2);
}
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> getMatchingComponents() {
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 an array of all date parts to construct a datetime instance.
*
* @return [min, hour, day, month, year]
*/
private List<Integer> getSpecialMatchingComponents() {
JSONObject every = spec.optJSONObject("every");
return Arrays.asList(
(Integer) every.opt("weekday"),
(Integer) every.opt("weekdayOrdinal"),
(Integer) every.opt("weekOfMonth"),
(Integer) every.opt("quarter")
);
}
/**
* Gets the base date from where to calculate the next trigger date.
*/
private Date getBaseDate() {
if (spec.has("requestBaseDate")) {
return new Date(spec.optLong("requestBaseDate"));
} else
if (spec.has("at")) {
return new Date(spec.optLong("at", 0));
} else
if (spec.has("firstAt")) {
return new Date(spec.optLong("firstAt", 0));
} else
if (spec.has("after")) {
return new Date(spec.optLong("after", 0));
} else {
return new Date();
}
}
}

View File

@ -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 androidx.core.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;
}
}

View File

@ -0,0 +1,154 @@
/*
* 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 {
// 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) {
groups.put(group.getId(), group);
}
/**
* Unregister the action group.
*
* @param id The id of the action group to remove.
*/
public static void unregister (String id) {
groups.remove(id);
}
/**
* Check if a action group with that id is registered.
*
* @param id The id of the action group to check for.
*/
public static boolean isRegistered (String id) {
return groups.containsKey(id);
}
/**
* Creates an action group by parsing the specified action specs.
*
* @param list The list of actions.
*
* @return A new action group.
*/
public static ActionGroup parse (Context context, JSONArray list) {
return parse(context, null, list);
}
/**
* Creates an action group by parsing the specified action specs.
*
* @param id The id for the action group.
* @param list The list of actions.
*
* @return A new action group.
*/
public static ActionGroup parse (Context context, String id, JSONArray list) {
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));
}
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;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.Manager;
import de.appplant.cordova.plugin.notification.Notification;
/**
* Abstract delete receiver for local notifications. Creates the local
* notification and calls the event functions for further proceeding.
*/
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
*/
@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
if (bundle == null)
return;
int toastId = bundle.getInt(Notification.EXTRA_ID);
Notification toast = Manager.getInstance(context).get(toastId);
if (toast == null)
return;
onClear(toast, bundle);
}
/**
* Called when a local notification was cleared from outside of the app.
*
* @param notification Wrapper around the local notification.
* @param bundle The bundled extras.
*/
abstract public void onClear (Notification notification, Bundle bundle);
}

View File

@ -0,0 +1,97 @@
/*
* 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.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
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 AbstractClickReceiver extends IntentService {
// Holds a reference to the intent to handle.
private Intent intent;
public AbstractClickReceiver() {
super("LocalNotificationClickReceiver");
}
/**
* Called when local notification was clicked to launch the main intent.
*/
@Override
protected void onHandleIntent(Intent intent) {
this.intent = intent;
if (intent == null)
return;
Bundle bundle = intent.getExtras();
Context context = getApplicationContext();
if (bundle == null)
return;
int toastId = bundle.getInt(Notification.EXTRA_ID);
Notification toast = Manager.getInstance(context).get(toastId);
if (toast == null)
return;
onClick(toast, bundle);
this.intent = null;
}
/**
* Called when local notification was clicked by the user.
*
* @param notification Wrapper around the local notification.
* @param bundle The bundled extras.
*/
abstract public void onClick (Notification notification, Bundle bundle);
/**
* The invoked action.
*/
protected String getAction() {
return getIntent().getExtras().getString(EXTRA_ID, CLICK_ACTION_ID);
}
/**
* Getter for the received intent.
*/
protected Intent getIntent() {
return intent;
}
}

View File

@ -0,0 +1,120 @@
package de.appplant.cordova.plugin.notification.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.os.PowerManager;
import java.util.Calendar;
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.util.LaunchUtils;
import static android.content.Context.POWER_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static java.util.Calendar.MINUTE;
/**
* The base class for any receiver that is trying to display a notification.
*/
abstract public class AbstractNotificationReceiver extends BroadcastReceiver {
/**
* Perform a notification. All notification logic is here.
* Determines whether to dispatch events, autoLaunch the app, use fullScreenIntents, etc.
* @param notification reference to the notification to be fired
*/
public void performNotification(Notification notification) {
Context context = notification.getContext();
Options options = notification.getOptions();
Manager manager = Manager.getInstance(context);
PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE);
boolean autoLaunch = options.isAutoLaunchingApp() && SDK_INT <= P && !options.useFullScreenIntent();
int badge = options.getBadgeNumber();
if (badge > 0) {
manager.setBadge(badge);
}
if (options.shallWakeUp()) {
wakeUp(notification);
}
if (autoLaunch) {
LaunchUtils.launchApp(context);
}
// Show notification if we should (triggerInApp is false)
// or if we can't trigger in the app due to:
// 1. No autoLaunch configured/supported and app is not running.
// 2. Any SDK >= Oreo is asleep (must be triggered here)
boolean didShowNotification = false;
if (!options.triggerInApp() ||
(!autoLaunch && !checkAppRunning())
) {
didShowNotification = true;
notification.show();
}
// run trigger function if triggerInApp() is true
// and we did not send a notification.
if (options.triggerInApp() && !didShowNotification) {
// wake up even if we didn't set it to
if (!options.shallWakeUp()) {
wakeUp(notification);
}
dispatchAppEvent("trigger", notification);
}
if (!options.isInfiniteTrigger())
return;
Calendar cal = Calendar.getInstance();
cal.add(MINUTE, 1);
Request req = new Request(options, cal.getTime());
manager.schedule(req, this.getClass());
}
/**
* Send the application an event using our notification
* @param key key for our event in the app
* @param notification reference to the notification
*/
abstract public void dispatchAppEvent(String key, Notification notification);
/**
* Check if the application is running.
* Should be developed in local class, which has access to things needed for this.
* @return whether or not app is running
*/
abstract public boolean checkAppRunning();
/**
* Wakeup the device.
*
* @param notification The notification used to wakeup the device.
* contains context and timeout.
*/
private void wakeUp(Notification notification) {
Context context = notification.getContext();
Options options = notification.getOptions();
String wakeLockTag = context.getApplicationInfo().name + ":LocalNotification";
PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE);
if (pm == null)
return;
int level = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE;
PowerManager.WakeLock wakeLock = pm.newWakeLock(level, wakeLockTag);
wakeLock.setReferenceCounted(false);
wakeLock.acquire(options.getWakeLockTimeout());
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.UserManager;
import org.json.JSONObject;
import java.util.List;
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;
import de.appplant.cordova.plugin.notification.Request;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static android.os.Build.VERSION.SDK_INT;
/**
* This class is triggered upon reboot of the device. It needs to re-register
* the alarms with the AlarmManager since these alarms are lost in case of
* reboot.
*/
abstract public class AbstractRestoreReceiver extends AbstractNotificationReceiver {
/**
* Called on device reboot.
*
* @param context Application context
* @param intent Received intent with content data
*/
@Override
public void onReceive (Context context, Intent intent) {
String action = intent.getAction();
if (SDK_INT >= 24) {
UserManager um = (UserManager) context.getSystemService(UserManager.class);
if (um == null || um.isUserUnlocked() == false) return;
}
Manager mgr = Manager.getInstance(context);
List<JSONObject> toasts = mgr.getOptions();
for (JSONObject data : toasts) {
Options options = new Options(context, data);
Request request = new Request(options);
Builder builder = new Builder(options);
Notification toast = buildNotification(builder);
onRestore(request, toast);
}
}
/**
* Called when a local notification need to be restored.
*
* @param request Set of notification options.
* @param toast Wrapper around the local notification.
*/
abstract public void onRestore (Request request, Notification toast);
/**
* Build notification specified by options.
*
* @param builder Notification builder.
*/
abstract public Notification buildNotification (Builder builder);
}

View File

@ -0,0 +1,85 @@
/*
* 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 AbstractNotificationReceiver {
/**
* 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();
if (bundle == null)
return;
int toastId = bundle.getInt(Notification.EXTRA_ID, 0);
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);
}

View File

@ -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;
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.
*/
void 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;
}
}
}

View File

@ -0,0 +1,360 @@
/*
* 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.WEEK;
import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.YEAR;
import static java.util.Calendar.DAY_OF_WEEK;
import static java.util.Calendar.WEEK_OF_MONTH;
import static java.util.Calendar.WEEK_OF_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 };
// Maps these crap where Sunday is the 1st day of the week
private static int[] WEEKDAYS = { 0, 2, 3, 4, 5, 6, 7, 1 };
// Maps these crap where Sunday is the 1st day of the week
private static int[] WEEKDAYS_REV = { 0, 7, 1, 2, 3, 4, 5, 6 };
// The date matching components
private final List<Integer> matchers;
// The special matching components
private final List<Integer> specials;
private static Unit getUnit(List<Integer> matchers, List<Integer> specials) {
Unit unit1 = INTERVALS[1 + matchers.indexOf(null)], unit2 = null;
if (specials.get(0) != null) {
unit2 = WEEK;
}
if (unit2 == null)
return unit1;
return (unit1.compareTo(unit2) < 0) ? unit2 : unit1;
}
/**
* Date matching trigger from now.
*
* @param matchers Describes the date matching parts.
* { day: 15, month: ... }
* @param specials Describes the date matching parts.
* { weekday: 1, weekOfMonth: ... }
*/
public MatchTrigger(List<Integer> matchers, List<Integer> specials) {
super(1, getUnit(matchers, specials));
if (specials.get(0) != null) {
specials.set(0, WEEKDAYS[specials.get(0)]);
}
this.matchers = matchers;
this.specials = specials;
}
/**
* 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) - 1);
}
if (matchers.get(4) != null) {
cal.set(Calendar.YEAR, matchers.get(4));
}
return cal;
}
/**
* Gets the date when to trigger the notification.
*
* @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 cal = getBaseTriggerDate(base);
Calendar now = getCal(base);
if (cal.compareTo(now) >= 0)
return applySpecials(cal);
if (unit == null || cal.get(Calendar.YEAR) < now.get(Calendar.YEAR))
return null;
if (cal.get(Calendar.MONTH) < now.get(Calendar.MONTH)) {
switch (unit) {
case MINUTE:
case HOUR:
case DAY:
case WEEK:
if (matchers.get(4) == null) {
addToDate(cal, now, Calendar.YEAR, 1);
break;
} else
return null;
case YEAR:
addToDate(cal, now, Calendar.YEAR, 1);
break;
}
} else
if (cal.get(Calendar.DAY_OF_YEAR) < now.get(Calendar.DAY_OF_YEAR)) {
switch (unit) {
case MINUTE:
case HOUR:
if (matchers.get(3) == null) {
addToDate(cal, now, Calendar.MONTH, 1);
break;
} else
if (matchers.get(4) == null) {
addToDate(cal, now, Calendar.YEAR, 1);
break;
}
else
return null;
case MONTH:
addToDate(cal, now, Calendar.MONTH, 1);
break;
case YEAR:
addToDate(cal, now, Calendar.YEAR, 1);
break;
}
} else
if (cal.get(Calendar.HOUR_OF_DAY) < now.get(Calendar.HOUR_OF_DAY)) {
switch (unit) {
case MINUTE:
if (matchers.get(2) == null) {
addToDate(cal, now, Calendar.DAY_OF_YEAR, 1);
break;
} else
if (matchers.get(3) == null) {
addToDate(cal, now, Calendar.MONTH, 1);
break;
}
else
return null;
case HOUR:
if (cal.get(Calendar.MINUTE) < now.get(Calendar.MINUTE)) {
addToDate(cal, now, Calendar.HOUR_OF_DAY, 1);
} else {
addToDate(cal, now, Calendar.HOUR_OF_DAY, 0);
}
break;
case DAY:
case WEEK:
addToDate(cal, now, Calendar.DAY_OF_YEAR, 1);
break;
case MONTH:
addToDate(cal, now, Calendar.MONTH, 1);
break;
case YEAR:
addToDate(cal, now, Calendar.YEAR, 1);
break;
}
} else
if (cal.get(Calendar.MINUTE) < now.get(Calendar.MINUTE)) {
switch (unit) {
case MINUTE:
addToDate(cal, now, Calendar.MINUTE, 1);
break;
case HOUR:
addToDate(cal, now, Calendar.HOUR_OF_DAY, 1);
break;
case DAY:
case WEEK:
addToDate(cal, now, Calendar.DAY_OF_YEAR, 1);
break;
case MONTH:
addToDate(cal, now, Calendar.MONTH, 1);
break;
case YEAR:
addToDate(cal, now, Calendar.YEAR, 1);
break;
}
}
return applySpecials(cal);
}
private Date applySpecials (Calendar cal) {
if (specials.get(2) != null && !setWeekOfMonth(cal))
return null;
if (specials.get(0) != null && !setDayOfWeek(cal))
return null;
return cal.getTime();
}
/**
* 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.
*/
private void addToDate (Calendar cal, Calendar now, int field, int count) {
cal.set(field, now.get(field));
cal.add(field, count);
}
/**
* Set the day of the year but ensure that the calendar does point to a
* date in future.
*
* @param cal The calendar to manipulate.
*
* @return true if the operation could be made.
*/
private boolean setDayOfWeek (Calendar cal) {
cal.setFirstDayOfWeek(Calendar.MONDAY);
int day = WEEKDAYS_REV[cal.get(DAY_OF_WEEK)];
int month = cal.get(Calendar.MONTH);
int year = cal.get(Calendar.YEAR);
int dayToSet = WEEKDAYS_REV[specials.get(0)];
if (matchers.get(2) != null)
return false;
if (day > dayToSet) {
if (specials.get(2) == null) {
cal.add(WEEK_OF_YEAR, 1);
} else
if (matchers.get(3) == null) {
cal.add(Calendar.MONTH, 1);
} else
if (matchers.get(4) == null) {
cal.add(Calendar.YEAR, 1);
} else
return false;
}
cal.set(Calendar.SECOND, 0);
cal.set(DAY_OF_WEEK, specials.get(0));
if (matchers.get(3) != null && cal.get(Calendar.MONTH) != month)
return false;
//noinspection RedundantIfStatement
if (matchers.get(4) != null && cal.get(Calendar.YEAR) != year)
return false;
return true;
}
/**
* Set the week of the month but ensure that the calendar does point to a
* date in future.
*
* @param cal The calendar to manipulate.
*
* @return true if the operation could be made.
*/
private boolean setWeekOfMonth (Calendar cal) {
int week = cal.get(WEEK_OF_MONTH);
int year = cal.get(Calendar.YEAR);
int weekToSet = specials.get(2);
if (week > weekToSet) {
if (matchers.get(3) == null) {
cal.add(Calendar.MONTH, 1);
} else
if (matchers.get(4) == null) {
cal.add(Calendar.YEAR, 1);
} else
return false;
if (matchers.get(4) != null && cal.get(Calendar.YEAR) != year)
return false;
}
int month = cal.get(Calendar.MONTH);
cal.set(WEEK_OF_MONTH, weekToSet);
if (cal.get(Calendar.MONTH) != month) {
cal.set(Calendar.DAY_OF_MONTH, 1);
cal.set(Calendar.MONTH, month);
} else
if (matchers.get(2) == null && week != weekToSet) {
cal.set(DAY_OF_WEEK, 2);
}
return true;
}
}

View File

@ -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 androidx.core.content.FileProvider;
public class AssetProvider extends FileProvider {
// Nothing to do here
}

View File

@ -0,0 +1,371 @@
/*
* 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.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.net.Uri;
import android.os.StrictMode;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;
/**
* Util class to map unified asset URIs to native URIs. URIs like file:///
* map to absolute paths while file:// point relatively to the www folder
* within the asset resources. And res:// means a resource from the native
* res folder. Remote assets are accessible via http:// for example.
*/
public final class AssetUtil {
// Name of the storage folder
private static final String STORAGE_FOLDER = "/localnotification";
// Ref to the context passed through the constructor to access the
// resources and app directory.
private final Context context;
/**
* Constructor
*
* @param context Application context.
*/
private AssetUtil(Context context) {
this.context = context;
}
/**
* Static method to retrieve class instance.
*
* @param context Application context.
*/
public static AssetUtil getInstance(Context context) {
return new AssetUtil(context);
}
/**
* The URI for a path.
*
* @param path The given path.
*/
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);
} else if (path.startsWith("file://")) {
return getUriFromAsset(path);
} else if (path.startsWith("http")){
return getUriFromRemote(path);
} else if (path.startsWith("content://")){
return Uri.parse(path);
}
return Uri.EMPTY;
}
/**
* URI for a file.
*
* @param path Absolute path like file:///...
*
* @return URI pointing to the given path.
*/
private Uri getUriFromPath(String path) {
String absPath = path.replaceFirst("file://", "")
.replaceFirst("\\?.*$", "");
File file = new File(absPath);
if (!file.exists()) {
Log.e("Asset", "File not found: " + file.getAbsolutePath());
return Uri.EMPTY;
}
return getUriFromFile(file);
}
/**
* URI for an asset.
*
* @param path Asset path like file://...
*
* @return URI pointing to the given path.
*/
private Uri getUriFromAsset(String path) {
String resPath = path.replaceFirst("file:/", "www")
.replaceFirst("\\?.*$", "");
String fileName = resPath.substring(resPath.lastIndexOf('/') + 1);
File file = getTmpFile(fileName);
if (file == null)
return Uri.EMPTY;
try {
AssetManager assets = context.getAssets();
InputStream in = assets.open(resPath);
FileOutputStream out = new FileOutputStream(file);
copyFile(in, out);
} catch (Exception e) {
Log.e("Asset", "File not found: assets/" + resPath);
e.printStackTrace();
return Uri.EMPTY;
}
return getUriFromFile(file);
}
/**
* The URI for a resource.
*
* @param path The given relative path.
*
* @return URI pointing to the given path.
*/
private Uri getUriForResourcePath(String path) {
Resources res = context.getResources();
String resPath = path.replaceFirst("res://", "");
int resId = getResId(resPath);
if (resId == 0) {
Log.e("Asset", "File not found: " + resPath);
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.
*
* @return Uri of the downloaded file.
*/
private Uri getUriFromRemote(String path) {
File file = getTmpFile();
if (file == null)
return Uri.EMPTY;
try {
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
StrictMode.ThreadPolicy policy =
new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
connection.setRequestProperty("Connection", "close");
connection.setConnectTimeout(5000);
connection.connect();
InputStream in = connection.getInputStream();
FileOutputStream out = new FileOutputStream(file);
copyFile(in, out);
return getUriFromFile(file);
} catch (MalformedURLException e) {
Log.e("Asset", "Incorrect URL");
e.printStackTrace();
} catch (FileNotFoundException e) {
Log.e("Asset", "Failed to create new File from HTTP Content");
e.printStackTrace();
} catch (IOException e) {
Log.e("Asset", "No Input can be created from http Stream");
e.printStackTrace();
}
return Uri.EMPTY;
}
/**
* Copy content from input stream into output stream.
*
* @param in The input stream.
* @param out The output stream.
*/
private void copyFile(InputStream in, FileOutputStream out) {
byte[] buffer = new byte[1024];
int read;
try {
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Resource ID for drawable.
*
* @param resPath Resource path as string.
*
* @return The resource ID or 0 if not found.
*/
public int getResId(String resPath) {
int resId = getResId(context.getResources(), resPath);
return resId;
}
/**
* Get resource ID.
*
* @param res The resources where to look for.
* @param resPath The name of the resource.
*
* @return The resource ID or 0 if not found.
*/
private int getResId(Resources res, String resPath) {
String pkgName = getPkgName(res);
String resName = getBaseName(resPath);
int resId;
resId = res.getIdentifier(resName, "mipmap", pkgName);
if (resId == 0) {
resId = res.getIdentifier(resName, "drawable", pkgName);
}
if (resId == 0) {
resId = res.getIdentifier(resName, "raw", pkgName);
}
return resId;
}
/**
* Convert URI to Bitmap.
*
* @param uri Internal image URI
*/
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.
*/
private String getBaseName (String resPath) {
String drawable = resPath;
if (drawable.contains("/")) {
drawable = drawable.substring(drawable.lastIndexOf('/') + 1);
}
if (resPath.contains(".")) {
drawable = drawable.substring(0, drawable.lastIndexOf('.'));
}
return drawable;
}
/**
* Returns a file located under the external cache dir of that app.
*
* @return File with a random UUID name.
*/
private File getTmpFile () {
// If random UUID is not be enough see
// https://github.com/LukePulverenti/cordova-plugin-local-notifications/blob/267170db14044cbeff6f4c3c62d9b766b7a1dd62/src/android/notification/AssetUtil.java#L255
return getTmpFile(UUID.randomUUID().toString());
}
/**
* 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.
*/
private File getTmpFile (String name) {
File dir = context.getExternalCacheDir();
if (dir == null) {
dir = context.getCacheDir();
}
if (dir == null) {
Log.e("Asset", "Missing cache dir");
return null;
}
String storage = dir.toString() + STORAGE_FOLDER;
//noinspection ResultOfMethodCallIgnored
new File(storage).mkdir();
return new File(storage, name);
}
/**
* Get content URI for the specified file.
*
* @param file The file to get the URI.
*
* @return content://...
*/
private Uri getUriFromFile(File file) {
try {
String authority = context.getPackageName() + ".localnotifications.provider";
return AssetProvider.getUriForFile(context, authority, file);
} catch (IllegalArgumentException e) {
e.printStackTrace();
return Uri.EMPTY;
}
}
/**
* Package name specified by the resource bundle.
*/
private String getPkgName (Resources res) {
return res == Resources.getSystem() ? "android" : context.getPackageName();
}
}

View File

@ -0,0 +1,33 @@
package de.appplant.cordova.plugin.notification.util;
import android.content.Context;
import android.content.Intent;
import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
public final class LaunchUtils {
/***
* Launch main intent from package.
*/
public static void launchApp(Context context) {
String pkgName = context.getPackageName();
Intent intent = context
.getPackageManager()
.getLaunchIntentForPackage(pkgName);
if (intent == null)
return;
intent.addFlags(
FLAG_ACTIVITY_REORDER_TO_FRONT
| FLAG_ACTIVITY_SINGLE_TOP
| FLAG_ACTIVITY_NEW_TASK
);
context.startActivity(intent);
}
}

View File

@ -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>

View File

@ -0,0 +1,52 @@
/*
* 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 <Cordova/CDVPlugin.h>
@import UserNotifications;
@interface APPLocalNotification : CDVPlugin <UNUserNotificationCenterDelegate>
- (void) launch:(CDVInvokedUrlCommand*)command;
- (void) ready:(CDVInvokedUrlCommand*)command;
- (void) actions:(CDVInvokedUrlCommand*)command;
- (void) check:(CDVInvokedUrlCommand*)command;
- (void) request:(CDVInvokedUrlCommand*)command;
- (void) schedule:(CDVInvokedUrlCommand*)command;
- (void) update:(CDVInvokedUrlCommand*)command;
- (void) clear:(CDVInvokedUrlCommand*)command;
- (void) clearAll:(CDVInvokedUrlCommand*)command;
- (void) cancel:(CDVInvokedUrlCommand*)command;
- (void) cancelAll:(CDVInvokedUrlCommand*)command;
- (void) type:(CDVInvokedUrlCommand*)command;
- (void) ids:(CDVInvokedUrlCommand*)command;
- (void) notification:(CDVInvokedUrlCommand*)command;
- (void) notifications:(CDVInvokedUrlCommand*)command;
@end

View File

@ -0,0 +1,719 @@
/*
* 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.
*/
// codebeat:disable[TOO_MANY_FUNCTIONS]
#import "APPLocalNotification.h"
#import "APPNotificationContent.h"
#import "APPNotificationOptions.h"
#import "APPNotificationCategory.h"
#import "UNUserNotificationCenter+APPLocalNotification.h"
#import "UNNotificationRequest+APPLocalNotification.h"
@interface APPLocalNotification ()
@property (strong, nonatomic) UNUserNotificationCenter* center;
@property (NS_NONATOMIC_IOSONLY, nullable, weak) id <UNUserNotificationCenterDelegate> delegate;
@property (readwrite, assign) BOOL deviceready;
@property (readwrite, assign) BOOL isActive;
@property (readonly, nonatomic, retain) NSArray* launchDetails;
@property (readonly, nonatomic, retain) NSMutableArray* eventQueue;
@end
@implementation APPLocalNotification
UNNotificationPresentationOptions const OptionNone = UNNotificationPresentationOptionNone;
UNNotificationPresentationOptions const OptionBadge = UNNotificationPresentationOptionBadge;
UNNotificationPresentationOptions const OptionSound = UNNotificationPresentationOptionSound;
UNNotificationPresentationOptions const OptionAlert = UNNotificationPresentationOptionAlert;
@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) ready:(CDVInvokedUrlCommand*)command
{
deviceready = YES;
[self.commandDelegate runInBackground:^{
for (NSString* js in eventQueue) {
[self.commandDelegate evalJs:js];
}
[eventQueue removeAllObjects];
}];
}
/**
* Schedule notifications.
*
* @param [Array<Hash>] properties A list of key-value properties.
*
* @return [ Void ]
*/
- (void) schedule:(CDVInvokedUrlCommand*)command
{
NSArray* notifications = command.arguments;
[self.commandDelegate runInBackground:^{
for (NSDictionary* options in notifications) {
APPNotificationContent* notification;
// Delete an existing alarm with this ID first
NSNumber* id = [options objectForKey:@"id"];
UNNotificationRequest* oldNotification;
oldNotification = [_center getNotificationWithId:id];
if (oldNotification) {
[_center cancelNotification:oldNotification];
}
// Schedule the new notification
notification = [[APPNotificationContent alloc]
initWithOptions:options];
[self scheduleNotification:notification];
}
[self check:command];
}];
}
/**
* Update notifications.
*
* @param [Array<Hash>] properties A list of key-value properties.
*
* @return [ Void ]
*/
- (void) update:(CDVInvokedUrlCommand*)command
{
NSArray* notifications = command.arguments;
[self.commandDelegate runInBackground:^{
for (NSDictionary* options in notifications) {
NSNumber* id = [options objectForKey:@"id"];
UNNotificationRequest* notification;
notification = [_center getNotificationWithId:id];
if (!notification)
continue;
[self updateNotification:[notification copy]
withOptions:options];
[self fireEvent:@"update" notification:notification];
}
[self check:command];
}];
}
/**
* Clear notifications by id.
*
* @param [ Array<Int> ] The IDs of the notifications to clear.
*
* @return [ Void ]
*/
- (void) clear:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
for (NSNumber* id in command.arguments) {
UNNotificationRequest* notification;
notification = [_center getNotificationWithId:id];
if (!notification)
continue;
[_center clearNotification:notification];
[self fireEvent:@"clear" notification:notification];
}
[self execCallback:command];
}];
}
/**
* Clear all local notifications.
*
* @return [ Void ]
*/
- (void) clearAll:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
[_center clearNotifications];
[self clearApplicationIconBadgeNumber];
[self fireEvent:@"clearall"];
[self execCallback:command];
}];
}
/**
* Cancel notifications by id.
*
* @param [ Array<Int> ] The IDs of the notifications to clear.
*
* @return [ Void ]
*/
- (void) cancel:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
for (NSNumber* id in command.arguments) {
UNNotificationRequest* notification;
notification = [_center getNotificationWithId:id];
if (!notification)
continue;
[_center cancelNotification:notification];
[self fireEvent:@"cancel" notification:notification];
}
[self execCallback:command];
}];
}
/**
* Cancel all local notifications.
*
* @return [ Void ]
*/
- (void) cancelAll:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
[_center cancelNotifications];
[self clearApplicationIconBadgeNumber];
[self fireEvent:@"cancelall"];
[self execCallback:command];
}];
}
/**
* Get type of notification.
*
* @param [ Int ] id The ID of the notification.
*
* @return [ Void ]
*/
- (void) type:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
NSNumber* id = [command argumentAtIndex:0];
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
messageAsString:type];
[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}
/**
* List of notification IDs by type.
*
* @return [ Void ]
*/
- (void) ids:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
int code = [command.arguments[0] intValue];
APPNotificationType type = NotifcationTypeUnknown;
switch (code) {
case 0:
type = NotifcationTypeAll;
break;
case 1:
type = NotifcationTypeScheduled;
break;
case 2:
type = NotifcationTypeTriggered;
break;
}
NSArray* ids = [_center getNotificationIdsByType:type];
CDVPluginResult* result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsArray:ids];
[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}
/**
* Notification by id.
*
* @param [ Number ] id The id of the notification to return.
*
* @return [ Void ]
*/
- (void) notification:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
NSArray* ids = command.arguments;
NSArray* notifications;
notifications = [_center getNotificationOptionsById:ids];
CDVPluginResult* result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsDictionary:[notifications firstObject]];
[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}
/**
* List of notifications by id.
*
* @param [ Array<Number> ] ids The ids of the notifications to return.
*
* @return [ Void ]
*/
- (void) notifications:(CDVInvokedUrlCommand*)command
{
[self.commandDelegate runInBackground:^{
int code = [command.arguments[0] intValue];
APPNotificationType type = NotifcationTypeUnknown;
NSArray* toasts;
NSArray* ids;
switch (code) {
case 0:
type = NotifcationTypeAll;
break;
case 1:
type = NotifcationTypeScheduled;
break;
case 2:
type = NotifcationTypeTriggered;
break;
case 3:
ids = command.arguments[1];
toasts = [_center getNotificationOptionsById:ids];
break;
}
if (toasts == nil) {
toasts = [_center getNotificationOptionsByType:type];
}
CDVPluginResult* result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsArray:toasts];
[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}
/**
* Check for permission to show notifications.
*
* @return [ Void ]
*/
- (void) check:(CDVInvokedUrlCommand*)command
{
[_center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) {
BOOL authorized = settings.authorizationStatus == UNAuthorizationStatusAuthorized;
BOOL enabled = settings.notificationCenterSetting == UNNotificationSettingEnabled;
BOOL permitted = authorized && enabled;
[self execCallback:command arg:permitted];
}];
}
/**
* Request for permission to show notifcations.
*
* @return [ Void ]
*/
- (void) request:(CDVInvokedUrlCommand*)command
{
UNAuthorizationOptions options =
(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert);
[_center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError* e) {
[self check:command];
}];
}
/**
* Register/update an action group.
*
* @return [ Void ]
*/
- (void) actions:(CDVInvokedUrlCommand *)command
{
[self.commandDelegate runInBackground:^{
int code = [command.arguments[0] intValue];
NSString* identifier = [command argumentAtIndex:1];
NSArray* actions = [command argumentAtIndex:2];
UNNotificationCategory* group;
BOOL found;
switch (code) {
case 0:
group = [APPNotificationCategory parse:actions withId:identifier];
[_center addActionGroup:group];
[self execCallback:command];
break;
case 1:
[_center removeActionGroup:identifier];
[self execCallback:command];
break;
case 2:
found = [_center hasActionGroup:identifier];
[self execCallback:command arg:found];
break;
}
}];
}
#pragma mark -
#pragma mark Private
/**
* Schedule the local notification.
*
* @param [ APPNotificationContent* ] notification The notification to schedule.
*
* @return [ Void ]
*/
- (void) scheduleNotification:(APPNotificationContent*)notification
{
__weak APPLocalNotification* weakSelf = self;
UNNotificationRequest* request = notification.request;
NSString* event = [request wasUpdated] ? @"update" : @"add";
[_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) updateNotification:(UNNotificationRequest*)notification
withOptions:(NSDictionary*)newOptions
{
NSMutableDictionary* options = [notification.content.userInfo mutableCopy];
[options addEntriesFromDictionary:newOptions];
[options setObject:[NSDate date] forKey:@"updatedAt"];
APPNotificationContent*
newNotification = [[APPNotificationContent alloc] initWithOptions:options];
[self scheduleNotification:newNotification];
}
#pragma mark -
#pragma mark UNUserNotificationCenterDelegate
/**
* Called when a notification is delivered to the app while being in foreground.
*/
- (void) userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))handler
{
UNNotificationRequest* toast = notification.request;
[_delegate userNotificationCenter:center
willPresentNotification:notification
withCompletionHandler:handler];
if ([toast.trigger isKindOfClass:UNPushNotificationTrigger.class])
return;
APPNotificationOptions* options = toast.options;
if (![notification.request wasUpdated]) {
[self fireEvent:@"trigger" notification:toast];
}
if (options.silent) {
handler(OptionNone);
} else if (!isActive || options.priority > 0) {
handler(OptionBadge|OptionSound|OptionAlert);
} else {
handler(OptionBadge|OptionSound);
}
}
/**
* Called to let your app know which action was selected by the user for a given
* notification.
*/
- (void) userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))handler
{
UNNotificationRequest* toast = response.notification.request;
[_delegate userNotificationCenter:center
didReceiveNotificationResponse:response
withCompletionHandler:handler];
handler();
if ([toast.trigger isKindOfClass:UNPushNotificationTrigger.class])
return;
NSMutableDictionary* data = [[NSMutableDictionary alloc] init];
NSString* action = response.actionIdentifier;
NSString* event = action;
if ([action isEqualToString:UNNotificationDefaultActionIdentifier]) {
event = @"click";
} else
if ([action isEqualToString:UNNotificationDismissActionIdentifier]) {
event = @"clear";
}
if (!deviceready && [event isEqualToString:@"click"]) {
_launchDetails = @[toast.options.id, event];
}
if (![event isEqualToString:@"clear"]) {
[self fireEvent:@"clear" notification:toast];
}
if ([response isKindOfClass:UNTextInputNotificationResponse.class]) {
[data setObject:((UNTextInputNotificationResponse*) response).userText
forKey:@"text"];
}
[self fireEvent:event notification:toast data:data];
}
#pragma mark -
#pragma mark Life Cycle
/**
* Registers obervers after plugin was initialized.
*/
- (void) pluginInitialize
{
eventQueue = [[NSMutableArray alloc] init];
_center = [UNUserNotificationCenter currentNotificationCenter];
_delegate = _center.delegate;
_center.delegate = self;
[_center registerGeneralNotificationCategory];
[self monitorAppStateChanges];
}
/**
* Monitor changes of the app state and update the _isActive flag.
*/
- (void) monitorAppStateChanges
{
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
/**
* Removes the badge number from the app icon.
*/
- (void) clearApplicationIconBadgeNumber
{
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
});
}
/**
* Invokes the callback without any parameter.
*
* @return [ Void ]
*/
- (void) execCallback:(CDVInvokedUrlCommand*)command
{
CDVPluginResult *result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}
/**
* Invokes the callback with a single boolean parameter.
*
* @return [ Void ]
*/
- (void) execCallback:(CDVInvokedUrlCommand*)command arg:(BOOL)arg
{
CDVPluginResult *result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK
messageAsBool:arg];
[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}
/**
* Fire general event.
*
* @param [ NSString* ] event The name of the event to fire.
*
* @return [ Void ]
*/
- (void) fireEvent:(NSString*)event
{
NSMutableDictionary* data = [[NSMutableDictionary alloc] init];
[self fireEvent:event notification:NULL data:data];
}
/**
* 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
{
NSMutableDictionary* data = [[NSMutableDictionary alloc] init];
[self fireEvent:event notification:notitification data:data];
}
/**
* 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:(UNNotificationRequest*)request
data:(NSMutableDictionary*)data
{
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];
dataAsJSON =
[[NSString alloc] initWithData:dataAsData encoding:NSUTF8StringEncoding];
if (request) {
params = [NSString stringWithFormat:@"%@,%@", notiAsJSON, dataAsJSON];
} else {
params = [NSString stringWithFormat:@"%@", dataAsJSON];
}
js = [NSString stringWithFormat:
@"cordova.plugins.notification.local.fireEvent('%@', %@)",
event, params];
if (deviceready) {
[self.commandDelegate evalJs:js];
} else {
[self.eventQueue addObject:js];
}
}
@end
// codebeat:enable[TOO_MANY_FUNCTIONS]

View File

@ -0,0 +1,28 @@
/*
* 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 APPNotificationCategory : NSObject
+ (UNNotificationCategory*) parse:(NSArray*)list withId:(NSString*)groupId;
@end

View File

@ -0,0 +1,111 @@
/*
* 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 "APPNotificationCategory.h"
@import UserNotifications;
@implementation APPNotificationCategory : NSObject
#pragma mark -
#pragma mark Public
/**
* Parse the provided spec map into an action group.
*
* @param [ NSDictionary* ] spec A key-value property map.
* Must contain an id and a list of actions.
*
* @return [ UNNotificationCategory* ]
*/
+ (UNNotificationCategory*) parse:(NSArray*)list withId:(NSString*)groupId
{
NSArray* actions = [self parseActions:list];
return [UNNotificationCategory categoryWithIdentifier:groupId
actions:actions
intentIdentifiers:@[]
options:UNNotificationCategoryOptionCustomDismissAction];
}
#pragma mark -
#pragma mark Private
/**
* The actions of the action group.
*
* @return [ NSArray* ]
*/
+ (NSArray<UNNotificationAction *> *) parseActions:(NSArray*)items
{
NSMutableArray* actions = [[NSMutableArray alloc] init];
for (NSDictionary* item in items) {
NSString* id = item[@"id"];
NSString* title = item[@"title"];
NSString* type = item[@"type"];
UNNotificationActionOptions options = UNNotificationActionOptionNone;
UNNotificationAction* action;
if ([item[@"launch"] boolValue]) {
options = UNNotificationActionOptionForeground;
}
if ([item[@"ui"] isEqualToString:@"decline"]) {
options = options | UNNotificationActionOptionDestructive;
}
if ([item[@"needsAuth"] boolValue]) {
options = options | UNNotificationActionOptionAuthenticationRequired;
}
if ([type isEqualToString:@"input"]) {
NSString* submitTitle = item[@"submitTitle"];
NSString* placeholder = item[@"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;
}
@end

View File

@ -0,0 +1,32 @@
/*
* 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 UserNotifications;
@interface APPNotificationContent : UNMutableNotificationContent
- (id) initWithOptions:(NSDictionary*)dict;
- (APPNotificationOptions*) options;
- (UNNotificationRequest*) request;
@end

View File

@ -0,0 +1,133 @@
/*
* 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.actionGroupId;
}
#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];
}
#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

View File

@ -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=actionGroupId) NSString* actionGroupId;
@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=priority) int priority;
@property (readonly, getter=sound) UNNotificationSound* sound;
@property (readonly, getter=userInfo) NSDictionary* userInfo;
@property (readonly, getter=attachments) NSArray<UNNotificationAttachment*>*attachments;
- (id) initWithDict:(NSDictionary*) dict;
- (UNNotificationTrigger*) trigger;
@end

View File

@ -0,0 +1,796 @@
/*
* 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;
// Maps these crap where Sunday is the 1st day of the week
static NSInteger WEEKDAYS[8] = { 0, 2, 3, 4, 5, 6, 7, 1 };
@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;
return self;
}
#pragma mark -
#pragma mark Properties
/**
* The ID for the notification.
*
* @return [ NSNumber* ]
*/
- (NSNumber*) id
{
NSInteger id = [dict[@"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[@"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[@"text"];
}
/**
* Show notification.
*
* @return [ BOOL ]
*/
- (BOOL) silent
{
return [dict[@"silent"] boolValue];
}
/**
* Show notification in foreground.
*
* @return [ BOOL ]
*/
- (int) priority
{
return [dict[@"priority"] intValue];
}
/**
* The badge number for the notification.
*
* @return [ NSNumber* ]
*/
- (NSNumber*) badge
{
id value = dict[@"badge"];
return (value == NULL) ? NULL : [NSNumber numberWithInt:[value intValue]];
}
/**
* The category of the notification.
*
* @return [ NSString* ]
*/
- (NSString*) actionGroupId
{
id actions = dict[@"actions"];
return ([actions isKindOfClass:NSString.class]) ? actions : kAPPGeneralCategory;
}
/**
* The sound file for the notification.
*
* @return [ UNNotificationSound* ]
*/
- (UNNotificationSound*) sound
{
NSString* path = dict[@"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[@"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;
}
#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[@"updatedAt"]) {
NSMutableDictionary* data = [dict mutableCopy];
[data removeObjectForKey:@"updatedAt"];
return data;
}
return dict;
}
#pragma mark -
#pragma mark Private
- (id) valueForTriggerOption:(NSString*)key
{
return dict[@"trigger"][key];
}
/**
* The date when to fire the notification.
*
* @return [ NSDate* ]
*/
- (NSDate*) triggerDate
{
double timestamp = [[self valueForTriggerOption:@"at"] doubleValue];
return [NSDate dateWithTimeIntervalSince1970:(timestamp / 1000)];
}
/**
* 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;
}
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:seconds
repeats:YES];
NSLog(@"[local-notification] Next trigger at: %@", trigger.nextTriggerDate);
return trigger;
}
/**
* A repeating trigger based on a calendar time intervals defined by the plugin.
*
* @return [ UNCalendarNotificationTrigger* ]
*/
- (UNCalendarNotificationTrigger*) triggerWithDateMatchingComponents:(BOOL)repeats
{
NSCalendar* cal = [self calendarWithMondayAsFirstDay];
NSDateComponents *date = [cal components:[self repeatInterval]
fromDate:[self triggerDate]];
date.timeZone = [NSTimeZone defaultTimeZone];
UNCalendarNotificationTrigger* trigger =
[UNCalendarNotificationTrigger triggerWithDateMatchingComponents:date
repeats:repeats];
NSLog(@"[local-notification] Next trigger at: %@", trigger.nextTriggerDate);
return trigger;
}
/**
* A repeating trigger based on a calendar time intervals defined by the user.
*
* @return [ UNCalendarNotificationTrigger* ]
*/
- (UNCalendarNotificationTrigger*) triggerWithCustomDateMatchingComponents
{
NSCalendar* cal = [self calendarWithMondayAsFirstDay];
NSDateComponents *date = [self customDateComponents];
date.calendar = cal;
date.timeZone = [NSTimeZone defaultTimeZone];
UNCalendarNotificationTrigger* trigger =
[UNCalendarNotificationTrigger triggerWithDateMatchingComponents:date
repeats:YES];
NSLog(@"[local-notification] Next trigger at: %@", trigger.nextTriggerDate);
return trigger;
}
/**
* A repeating trigger based on a location region.
*
* @return [ UNLocationNotificationTrigger* ]
*/
- (UNLocationNotificationTrigger*) triggerWithRegion
{
NSArray* center = [self valueForTriggerOption:@"center"];
double radius = [[self valueForTriggerOption:@"radius"] doubleValue];
BOOL single = [[self valueForTriggerOption:@"single"] boolValue];
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:!single];
}
/**
* 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 = WEEKDAYS[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;
}
/**
* Instance if a calendar where the monday is the first day of the week.
*
* @return [ NSCalendar* ]
*/
- (NSCalendar*) calendarWithMondayAsFirstDay
{
NSCalendar* cal = [[NSCalendar alloc]
initWithCalendarIdentifier:NSCalendarIdentifierISO8601];
cal.firstWeekday = 2;
cal.minimumDaysInFirstWeek = 1;
return cal;
}
@end

View File

@ -0,0 +1,32 @@
/*
* 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 UserNotifications;
@interface UNNotificationRequest (APPLocalNotification)
- (APPNotificationOptions*) options;
- (BOOL) wasUpdated;
- (NSString*) encodeToJSON;
@end

View File

@ -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

View File

@ -0,0 +1,60 @@
/*
* 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;
- (void) registerGeneralNotificationCategory;
- (void) addActionGroup:(UNNotificationCategory*)category;
- (void) removeActionGroup:(NSString*)identifier;
- (BOOL) hasActionGroup:(NSString*)identifier;
- (NSArray*) getNotificationIdsByType:(APPNotificationType)type;
- (UNNotificationRequest*) getNotificationWithId:(NSNumber*)id;
- (APPNotificationType) getTypeOfNotificationWithId:(NSNumber*)id;
- (NSArray*) getNotificationOptions;
- (NSArray*) getNotificationOptionsById:(NSArray*)ids;
- (NSArray*) getNotificationOptionsByType:(APPNotificationType)type;
- (void) clearNotification:(UNNotificationRequest*)notification;
- (void) clearNotifications;
- (void) cancelNotification:(UNNotificationRequest*)notification;
- (void) cancelNotifications;
@end

View File

@ -0,0 +1,397 @@
/*
* 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) addActionGroup:(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];
}];
}
/**
* Remove if the specified category does exist.
*
* @param [ NSString* ] identifier The category id to remove.
*
* @return [ Void ]
*/
- (void) removeActionGroup:(NSString*)identifier
{
[self getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *set) {
NSMutableSet* categories = [NSMutableSet setWithSet:set];
for (UNNotificationCategory* item in categories)
{
if ([item.identifier isEqualToString:identifier]) {
[categories removeObject:item];
break;
}
}
[self setNotificationCategories:categories];
}];
}
/**
* Check if the specified category does exist.
*
* @param [ NSString* ] identifier The category id to check for.
*
* @return [ BOOL ]
*/
- (BOOL) hasActionGroup:(NSString*)identifier
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block BOOL found = NO;
[self getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *items) {
for (UNNotificationCategory* item in items)
{
if ([item.identifier isEqualToString:identifier]) {
found = YES;
dispatch_semaphore_signal(sema);
break;
}
}
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return found;
}
#pragma mark -
#pragma mark LocalNotifications
/**
* List of all delivered or still pending notifications.
*
* @return [ NSArray<UNNotificationRequest*>* ]
*/
- (NSArray*) getNotifications
{
NSMutableArray* notifications = [[NSMutableArray alloc] init];
[notifications addObjectsFromArray:[self getPendingNotifications]];
[notifications addObjectsFromArray:[self getDeliveredNotifications]];
return notifications;
}
/**
* List of all triggered notifications.
*
* @return [ NSArray<UNNotificationRequest*>* ]
*/
- (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.
*
* @return [ NSArray<UNNotificationRequest*>* ]
*/
- (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 [ APPNotificationType ] type Notification life cycle type.
*
* @return [ NSArray<UNNotificationRequest>* ]
*/
- (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.
*
* @return [ NSArray<int>* ]
*/
- (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 [ APPNotificationType ] type Notification life cycle type.
*
* @return [ NSArray<int>* ]
*/
- (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
*
* @return [ UNNotificationRequest* ]
*/
- (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.
*
* @param [ NSNumber* ] id The ID of the notification.
*
* @return [ APPNotificationType ]
*/
- (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.
*
* @return [ NSArray<APPNotificationOptions*>* ]
*/
- (NSArray*) getNotificationOptions
{
return [self getNotificationOptionsByType:NotifcationTypeAll];
}
/**
* List of properties from all notifications of given type.
*
* @param [ APPNotificationType ] type Notification life cycle type.
*
* @return [ NSArray<APPNotificationOptions*>* ]
*/
- (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 [ NSArray<int> ] ids The ids of the notifications to find.
*
* @return [ NSArray<APPNotificationOptions*>* ]
*/
- (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.
*
* @return [ Void ]
*/
- (void) clearNotifications
{
[self removeAllDeliveredNotifications];
}
/*
* Clear Specified notfication.
*
* @param [ UNNotificationRequest* ] notification The notification object.
*
* @return [ Void ]
*/
- (void) clearNotification:(UNNotificationRequest*)toast
{
[self removeDeliveredNotificationsWithIdentifiers:@[toast.identifier]];
}
/*
* Cancel all notfications.
*
* @return [ Void ]
*/
- (void) cancelNotifications
{
[self removeAllPendingNotificationRequests];
[self removeAllDeliveredNotifications];
}
/*
* Cancel specified notfication.
*
* @param [ UNNotificationRequest* ] notification The notification object.
*
* @return [ Void ]
*/
- (void) cancelNotification:(UNNotificationRequest*)toast
{
NSArray* ids = @[toast.identifier];
[self removeDeliveredNotificationsWithIdentifiers:ids];
[self removePendingNotificationRequestsWithIdentifiers:ids];
}
@end

View File

@ -0,0 +1,578 @@
/*
* 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);
};
/**
* Request permission to show notifications.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
*
* @return [ Void ]
*/
exports.request = function (success, error) {
exports.check(success, error);
};
/**
* Check to see if the user has allowed "Do Not Disturb" permissions for this app.
* This is required to use alarmVolume to take a user out of silent mode.
*
* Callback contains true or false for whether or not we have this permission.
*
* @param {Function} callback The function to be exec as the callback.
* @param {Object} scope callback function's scope
*/
exports.hasDoNotDisturbPermission = function (success, error) {
impl.hasDoNotDisturbPermission(success, error);
}
/**
* Request "Do Not Disturb" permissions for this app.
* The only way to do this is to launch the global do not distrub settings for all apps.
* This permission is required to use alarmVolume to take a user out of silent mode.
*
* Callback is deferred until
*
* @param {Function} callback The function to be exec as the callback.
* @param {Object} scope callback function's scope
*/
exports.requestDoNotDisturbPermissions = function (success, error) {
impl.requestDoNotDisturbPermissions(success, error);
}
/**
* Check to see if the app is ignoring battery optimizations. This needs
* to be whitelisted by the user.
*
* Callback contains true or false for whether or not we have this permission.
*
* @param {Function} callback The function to be exec as the callback.
* @param {Object} scope callback function's scope
*/
exports.isIgnoringBatteryOptimizations = function (success, error) {
impl.isIgnoringBatteryOptimizations(success, error);
}
/**
* Request permission to ignore battery optimizations.
* The only way to do this is to launch the global battery optimization settings for all apps.
* This permission is required to allow alarm to trigger logic within the app while the app is dead.
*
* Callback is deferred until user returns.
*
* @param {Function} callback The function to be exec as the callback.
* @param {Object} scope callback function's scope
*/
exports.requestIgnoreBatteryOptimizations = function (success, error) {
impl.requestIgnoreBatteryOptimizations(success, error);
}
/**
* Schedule notifications.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
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);
}
exports.check(success, error);
};
/**
* Update notifications.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
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);
}
exports.check(success, error);
};
/**
* 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, args) {
var toasts = impl.clear(args) || [];
for (var toast of toasts) {
exports.fireEvent('clear', toast);
}
success();
};
/**
* Clear all notifications.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
*
* @return [ Void ]
*/
exports.clearAll = function (success, error) {
impl.clearAll();
exports.fireEvent('clearall');
success();
};
/**
* 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, args) {
var toasts = impl.cancel(args) || [];
for (var toast of toasts) {
exports.fireEvent('cancel', toast);
}
success();
};
/**
* Cancel all notifications.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
*
* @return [ Void ]
*/
exports.cancelAll = function (success, error) {
impl.cancelAll();
exports.fireEvent('cancelall');
success();
};
/**
* Get the type of notification.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
exports.type = function (success, error, args) {
var type = impl.type(args[0]);
success(type);
};
/**
* List of all notification ids.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
exports.ids = function (success, error, args) {
var ids = impl.ids(args[0]) || [];
success(Array.from(ids));
};
/**
* Get a single notification by id.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
exports.notification = function (success, error, args) {
var obj = impl.notification(args[0]);
success(exports.clone(obj));
};
/**
* List of (all) notifications.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
exports.notifications = function (success, error, args) {
var objs = impl.notifications(args[0], args[1]) || [];
success(exports.cloneAll(objs));
};
/**
* Manage action groups.
*
* @param [ Function ] success Success callback
* @param [ Function ] error Error callback
* @param [ Array ] args Interface arguments
*
* @return [ Void ]
*/
exports.actions = function (success, error, args) {
var ActionGroup = LocalNotification.ActionGroup,
code = args[0],
id = args[1],
res = [],
list, group;
switch (code) {
case 0:
list = exports.parseActions({ actions:args[2] });
group = new ActionGroup(id, list);
ActionGroup.register(group);
break;
case 1:
ActionGroup.unregister(id);
break;
case 2:
res.push(ActionGroup.isRegistered(id));
break;
}
success.apply(this, res);
};
/**
* 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;
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 = [];
if (!Array.isArray(objs)) {
objs = Array.from(objs);
}
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.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;
return opts;
};
/**
* Parse trigger spec into instance of prefered type.
*
* @param [ Object ] obj The notification options map.
*
* @return [ LocalNotification.Trigger ]
*/
exports.parseTrigger = function (obj) {
var trigger = new LocalNotification.Toast.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;
}
return trigger;
};
/**
* Parse trigger.every spec into instance of prefered type.
*
* @param [ Object ] spec The trigger.every object.
*
* @return [ LocalNotification.Every|String ]
*/
exports.parseEvery = function (spec) {
var every = new LocalNotification.Toast.Every();
if (typeof spec !== 'object') return spec;
for (var prop in every) {
if (spec.hasOwnProperty(prop)) every[prop] = parseInt(spec[prop]);
}
return every;
};
/**
* Parse action specs into instances of prefered types.
*
* @param [ Object ] obj The notification options map.
*
* @return [ Array<LocalNotification.Action> ]
*/
exports.parseActions = function (obj) {
var spec = obj.actions,
actions = [], btn;
if (!spec) return actions;
if (typeof spec === 'string') {
var group = LocalNotification.ActionGroup.lookup(spec);
return group ? group.actions : actions;
}
for (var action of spec) {
if (!action.type || action.type == 'button') {
btn = new LocalNotification.Toast.Button();
} else
if (action.type == 'input') {
btn = new LocalNotification.Toast.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.Toast.ProgressBar(),
spec = obj.progressBar;
if (!spec) return bar;
for (var prop in bar) {
if (spec[prop]) bar[prop] = spec[prop];
}
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);

View File

@ -0,0 +1,94 @@
/*
* 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 global::LocalNotificationProxy.LocalNotification.Toast;
public sealed class ActionGroup
{
/// <summary>
/// Saves all groups for later lookup.
/// </summary>
private static Dictionary<string, ActionGroup> groups = new Dictionary<string, ActionGroup>();
/// <summary>
/// Initializes a new instance of the <see cref="ActionGroup"/> class.
/// </summary>
/// <param name="id">The ID of the action group.</param>
/// <param name="actions">The list of actions to group for.</param>
public ActionGroup(string id, [ReadOnlyArray] IAction[] actions)
{
this.Id = id;
this.Actions = actions;
}
/// <summary>
/// Gets or sets the action group ID.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the notification actions.
/// </summary>
public IAction[] Actions { get; set; }
/// <summary>
/// Lookup the action groups with the specified group id.
/// </summary>
/// <param name="id">The ID of the action group to find.</param>
/// <returns>Null if no group was found.</returns>
public static ActionGroup Lookup(string id)
{
return groups[id];
}
/// <summary>
/// Register the provided set of actions under the specified group id.
/// </summary>
/// <param name="group">The action group to register.</param>
public static void Register(ActionGroup group)
{
groups.Add(group.Id, group);
}
/// <summary>
/// Unregister the action group.
/// </summary>
/// <param name="id">The id of the action group to remove.</param>
public static void Unregister(string id)
{
groups.Remove(id);
}
/// <summary>
/// Check if a action group with that id is registered.
/// </summary>
/// <param name="id">The id of the action group to check for.</param>
/// <returns>True if a group with the ID could be found.</returns>
public static bool IsRegistered(string id)
{
return groups.ContainsKey(id);
}
}
}

View File

@ -0,0 +1,192 @@
/*
* 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.Diagnostics;
using Microsoft.Toolkit.Uwp.Notifications;
using Windows.UI.Notifications;
internal class Builder
{
/// <summary>
/// The provided trigger request.
/// </summary>
private readonly Request request;
/// <summary>
/// Initializes a new instance of the <see cref="Builder"/> class.
/// </summary>
/// <param name="request">Trigger request.</param>
public Builder(Request request)
{
this.request = request;
this.Content = new Notification(request.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>
/// 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>
/// 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.request.TriggerDate;
ScheduledToastNotification notification;
Debug.WriteLine("[local-notification] Next trigger at: " + at);
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.request.Occurrence > 2)
{
notification.Group = notification.Tag;
}
return notification;
}
}
}

View File

@ -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 request = new Request(options);
var builder = new Builder(request);
do
{
var toast = builder.Build();
if (toast == null)
{
break;
}
ToastNotifier.AddToSchedule(toast);
}
while (request.MoveNext());
}
}
/// <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;
}
}
}

View File

@ -0,0 +1,319 @@
/*
* 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 System.Collections.Generic;
using global::LocalNotificationProxy.LocalNotification.Toast;
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 instance as an serialized xml element.
/// </summary>
/// <returns>Element with all property values set as attributes.</returns>
public string GetXml()
{
return this.Options.GetXml();
}
}
}

View File

@ -0,0 +1,219 @@
/*
* 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 global::LocalNotificationProxy.LocalNotification.Toast;
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 Toast.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 = Toast.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();
}
}
}

View File

@ -0,0 +1,275 @@
/*
* 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;
internal class Request
{
/// <summary>
/// The right trigger for the options
/// </summary>
private readonly Trigger.DateTrigger trigger;
/// <summary>
/// How often the trigger shall occur.
/// </summary>
private readonly int count;
/// <summary>
/// The trigger spec.
/// </summary>
private readonly Toast.Trigger spec;
/// <summary>
/// The current trigger date.
/// </summary>
private DateTime? triggerDate;
/// <summary>
/// Initializes a new instance of the <see cref="Request"/> class.
/// </summary>
/// <param name="options">The options spec.</param>
public Request(Options options)
{
this.Options = options;
this.spec = options.Trigger;
this.count = Math.Max(this.spec.Count, 1);
this.trigger = this.BuildTrigger();
this.triggerDate = this.trigger.GetNextTriggerDate(this.GetBaseDate());
}
/// <summary>
/// Gets the options spec.
/// </summary>
public Options Options { get; private set; }
/// <summary>
/// Gets the value of the internal occurrence counter.
/// </summary>
public int Occurrence { get => this.trigger.Occurrence; }
/// <summary>
/// Gets a value indicating whether there's one more trigger date to calculate.
/// </summary>
public bool HasNext { get => this.triggerDate.HasValue && this.Occurrence <= this.count; }
/// <summary>
/// Gets the current trigger date.
/// </summary>
public DateTime? TriggerDate
{
get
{
if (!this.triggerDate.HasValue)
{
return null;
}
if (this.spec.Before != 0)
{
var before = this.ToDateTime(this.spec.Before);
if (this.triggerDate.Value >= before)
{
return null;
}
}
var minDate = DateTime.Now.AddSeconds(0.2);
if (this.triggerDate >= minDate)
{
return this.triggerDate;
}
if ((minDate - this.triggerDate).Value.TotalMinutes <= 1)
{
return minDate;
}
return null;
}
}
/// <summary>
/// Moves the internal occurrence counter by one.
/// </summary>
/// <returns>false if it wasnt possible anymore to move ahead.</returns>
public bool MoveNext()
{
if (this.HasNext)
{
this.triggerDate = this.GetNextTriggerDate();
}
else
{
this.triggerDate = null;
}
return this.triggerDate.HasValue;
}
/// <summary>
/// Gets the next trigger date based on the current trigger date.
/// </summary>
/// <returns>null if there's no next one.</returns>
private DateTime? GetNextTriggerDate()
{
if (this.triggerDate.HasValue)
{
return this.trigger.GetNextTriggerDate(this.triggerDate.Value);
}
else
{
return null;
}
}
/// <summary>
/// Build the trigger specified in options.
/// </summary>
/// <returns>Configured trigger instance</returns>
private Trigger.DateTrigger BuildTrigger()
{
if (this.spec.Every is Toast.Every)
{
var matchers = this.GetMatchers();
var specials = this.GetSpecials();
return new Trigger.MatchTrigger(matchers, specials);
}
var unit = this.GetUnit();
var ticks = this.GetTicks();
return new Trigger.IntervalTrigger(ticks, unit);
}
/// <summary>
/// Gets the unit value.
/// </summary>
/// <returns>SECOND by default.</returns>
private Trigger.DateTrigger.Unit GetUnit()
{
var unit = "SECOND";
if (!string.IsNullOrEmpty(this.spec.Unit))
{
unit = this.spec.Unit;
}
else if (this.spec.Every is string)
{
unit = this.spec.Every as string;
}
return (Trigger.DateTrigger.Unit)Enum.Parse(
typeof(Trigger.DateTrigger.Unit), unit.ToUpper());
}
/// <summary>
/// Gets the tick value.
/// </summary>
/// <returns>Defaults to 0</returns>
private int GetTicks()
{
if (this.spec.At != 0)
{
return 0;
}
else if (this.spec.In != 0)
{
return this.spec.In;
}
else if (this.spec.Every is string)
{
return 1;
}
else if (this.spec.Every is double)
{
return Convert.ToInt32(this.spec.Every);
}
return 0;
}
/// <summary>
/// Gets an array of all date parts to construct a datetime instance.
/// </summary>
/// <returns>[min, hour, day, month, year]</returns>
private int?[] GetMatchers()
{
var every = this.spec.Every as Toast.Every;
return new int?[]
{
every.Minute, every.Hour, every.Day, every.Month, every.Year
};
}
/// <summary>
/// Gets an array of all date parts to construct a datetime instance.
/// </summary>
/// <returns>[weekday, weekdayOrdinal, weekOfMonth, quarter]</returns>
private int?[] GetSpecials()
{
var every = this.spec.Every as Toast.Every;
return new int?[]
{
every.Weekday, every.WeekdayOrdinal, every.WeekOfMonth, every.Quarter
};
}
/// <summary>
/// Gets the base date from where to calculate the next trigger date.
/// </summary>
/// <returns>Usually points to now</returns>
private DateTime GetBaseDate()
{
if (this.spec.At != 0)
{
return this.ToDateTime(this.spec.At);
}
else if (this.spec.FirstAt != 0)
{
return this.ToDateTime(this.spec.FirstAt);
}
else if (this.spec.After != 0)
{
return this.ToDateTime(this.spec.After);
}
return DateTime.Now;
}
/// <summary>
/// Unix time from milliseconds.
/// </summary>
/// <param name="ms">Milliseconds </param>
/// <returns>DateTime object.</returns>
private DateTime ToDateTime(long ms)
{
return DateTimeOffset.FromUnixTimeMilliseconds(ms).LocalDateTime;
}
}
}

View File

@ -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.Toast
{
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;
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.Toast
{
using System;
using Windows.Data.Xml.Dom;
public sealed class Every
{
/// <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 day of week.
/// </summary>
public int? Weekday { get; set; }
/// <summary>
/// Gets or sets the week of year.
/// </summary>
public int? Week { get; set; }
/// <summary>
/// Gets or sets the day of ordinal week.
/// </summary>
public int? WeekdayOrdinal { get; set; }
/// <summary>
/// Gets or sets the week of month.
/// </summary>
public int? WeekOfMonth { get; set; }
/// <summary>
/// Gets or sets the month.
/// </summary>
public int? Month { get; set; }
/// <summary>
/// Gets or sets the quarter.
/// </summary>
public int? Quarter { get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Deserializes the XML string into an instance of Every.
/// </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 Every Parse(string xml)
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var every = new Every();
var node = doc.DocumentElement;
if (node.GetAttributeNode("minute") != null)
{
every.Minute = Convert.ToInt32(node.GetAttribute("minute"));
}
if (node.GetAttributeNode("hour") != null)
{
every.Hour = Convert.ToInt32(node.GetAttribute("hour"));
}
if (node.GetAttributeNode("day") != null)
{
every.Day = Convert.ToInt32(node.GetAttribute("day"));
}
if (node.GetAttributeNode("weekday") != null)
{
every.Weekday = Convert.ToInt32(node.GetAttribute("weekday"));
}
if (node.GetAttributeNode("week") != null)
{
every.Week = Convert.ToInt32(node.GetAttribute("week"));
}
if (node.GetAttributeNode("weekdayordinal") != null)
{
every.WeekdayOrdinal = Convert.ToInt32(node.GetAttribute("weekdayOrdinal"));
}
if (node.GetAttributeNode("weekOfMonth") != null)
{
every.WeekOfMonth = Convert.ToInt32(node.GetAttribute("weekOfMonth"));
}
if (node.GetAttributeNode("month") != null)
{
every.Month = Convert.ToInt32(node.GetAttribute("month"));
}
if (node.GetAttributeNode("year") != null)
{
every.Year = Convert.ToInt32(node.GetAttribute("year"));
}
return every;
}
/// <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("every");
if (this.Minute.HasValue)
{
node.SetAttribute("minute", this.Minute.ToString());
}
if (this.Hour.HasValue)
{
node.SetAttribute("hour", this.Hour.ToString());
}
if (this.Day.HasValue)
{
node.SetAttribute("day", this.Day.ToString());
}
if (this.Weekday.HasValue)
{
node.SetAttribute("weekday", this.Weekday.ToString());
}
if (this.Week.HasValue)
{
node.SetAttribute("week", this.Week.ToString());
}
if (this.WeekdayOrdinal.HasValue)
{
node.SetAttribute("weekdayOrdinal", this.WeekdayOrdinal.ToString());
}
if (this.WeekOfMonth.HasValue)
{
node.SetAttribute("weekOfMonth", this.WeekOfMonth.ToString());
}
if (this.Month.HasValue)
{
node.SetAttribute("month", this.Month.ToString());
}
if (this.Year.HasValue)
{
node.SetAttribute("year", this.Year.ToString());
}
return node.GetXml();
}
}
}

View File

@ -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.Toast
{
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; }
}
}

View File

@ -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.Toast
{
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;
}
}

View File

@ -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.Toast
{
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;
}
}

View File

@ -0,0 +1,155 @@
/*
* 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.Toast
{
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 fix trigger date.
/// </summary>
public long At { get; set; } = 0;
/// <summary>
/// Gets or sets the first trigger date.
/// </summary>
public long FirstAt { get; set; } = 0;
/// <summary>
/// Gets or sets the before trigger date.
/// </summary>
public long Before { get; set; } = 0;
/// <summary>
/// Gets or sets the after trigger date.
/// </summary>
public long After { 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; }
/// <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 = long.Parse(node.GetAttribute("at"));
trigger.FirstAt = long.Parse(node.GetAttribute("firstAt"));
trigger.Before = long.Parse(node.GetAttribute("before"));
trigger.After = long.Parse(node.GetAttribute("after"));
trigger.In = int.Parse(node.GetAttribute("in"));
trigger.Count = int.Parse(node.GetAttribute("count"));
trigger.Occurrence = int.Parse(node.GetAttribute("occurrence"));
if (node.GetAttributeNode("unit") != null)
{
trigger.Unit = node.GetAttribute("unit");
}
if (node.GetAttributeNode("strEvery") != null)
{
trigger.Every = node.GetAttribute("strEvery");
}
else if (node.GetAttributeNode("hshEvery") != null)
{
trigger.Every = Toast.Every.Parse(node.GetAttribute("hshEvery"));
}
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("firstAt", this.FirstAt.ToString());
node.SetAttribute("before", this.Before.ToString());
node.SetAttribute("after", this.After.ToString());
node.SetAttribute("in", this.In.ToString());
node.SetAttribute("count", this.Count.ToString());
node.SetAttribute("occurrence", this.Occurrence.ToString());
if (!string.IsNullOrEmpty(this.Unit))
{
node.SetAttribute("unit", this.Unit);
}
if (this.Every != null)
{
if (this.Every is Every)
{
node.SetAttribute("hshEvery", (this.Every as Every).GetXml());
}
else
{
node.SetAttribute("strEvery", this.Every.ToString());
}
}
return node.GetXml();
}
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.Trigger
{
using System;
internal abstract class DateTrigger
{
/// <summary>
/// Default unit is SECOND
/// </summary>
public enum Unit : byte
{
NULL, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR
}
/// <summary>
/// Gets or sets the occurrence counter.
/// </summary>
public int Occurrence { get; protected set; } = 1;
/// <summary>
/// Gets the next trigger date.
/// </summary>
/// <param name="date">The date from where to calculate the next one.</param>
/// <returns>Null if there is no next trigger date.</returns>
public abstract DateTime? GetNextTriggerDate(DateTime date);
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.Trigger
{
using System;
internal class IntervalTrigger : DateTrigger
{
/// <summary>
/// The number of ticks per interval.
/// </summary>
private readonly int ticks;
/// <summary>
/// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
/// </summary>
/// <param name="ticks">The number of ticks per interval.</param>
/// <param name="unit">The unit of the ticks.</param>
public IntervalTrigger(int ticks, Unit unit)
{
this.ticks = ticks;
this.Unit = unit;
}
/// <summary>
/// Gets the unit of the ticks
/// </summary>
internal new Unit Unit { get; private set; }
/// <summary>
/// Gets the next trigger date.
/// </summary>
/// <param name="date">The date from where to calculate the next one.</param>
/// <returns>Null if there is no next trigger date.</returns>
public override DateTime? GetNextTriggerDate(DateTime date)
{
this.Occurrence += 1;
return this.AddInterval(date);
}
/// <summary>
/// Adds the interval to the specified date.
/// </summary>
/// <param name="date">The date where to add the interval of ticks</param>
/// <returns>A new datetime instance</returns>
protected DateTime AddInterval(DateTime date)
{
switch (this.Unit)
{
case Unit.SECOND:
return date.AddSeconds(this.ticks);
case Unit.MINUTE:
return date.AddMinutes(this.ticks);
case Unit.HOUR:
return date.AddHours(this.ticks);
case Unit.DAY:
return date.AddDays(this.ticks);
case Unit.WEEK:
return date.AddDays(this.ticks * 7);
case Unit.MONTH:
return date.AddMonths(this.ticks);
case Unit.QUARTER:
return date.AddMonths(this.ticks * 3);
case Unit.YEAR:
return date.AddYears(this.ticks);
default:
return date;
}
}
}
}

View File

@ -0,0 +1,397 @@
/*
* 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.Trigger
{
using System;
using System.Globalization;
internal class MatchTrigger : IntervalTrigger
{
/// <summary>
/// Used to determine the interval
/// </summary>
private static readonly Unit?[] INTERVALS = { null, Unit.MINUTE, Unit.HOUR, Unit.DAY, Unit.MONTH, Unit.YEAR };
/// <summary>
/// Maps these crap where Sunday is the 1st day of the week.
/// </summary>
private static readonly int[] WEEKDAYS = { 7, 1, 2, 3, 4, 5, 6 };
/// <summary>
/// The matching components.
/// </summary>
private readonly int?[] matchers;
/// <summary>
/// The special matching components.
/// </summary>
private readonly int?[] specials;
/// <summary>
/// Initializes a new instance of the <see cref="MatchTrigger"/> class.
/// </summary>
/// <param name="matchers">The matching components.</param>
/// <param name="specials">The special matching components.</param>
public MatchTrigger(int?[] matchers, int?[] specials)
: base(1, GetUnit(matchers, specials))
{
this.matchers = matchers;
this.specials = specials;
}
/// <summary>
/// Gets the next trigger date.
/// </summary>
/// <param name="baseDate">The date from where to calculate the next one.</param>
/// <returns>Null if there is no next trigger date.</returns>
public override DateTime? GetNextTriggerDate(DateTime baseDate)
{
DateTime date = baseDate;
if (this.Occurrence > 1)
{
date = this.AddInterval(baseDate);
}
this.Occurrence += 1;
return this.GetTriggerDate(date);
}
/// <summary>
/// Gets the unit interval specified by the matching components.
/// </summary>
/// <param name="matchers">The matching components.</param>
/// <param name="specials">The special matching components.</param>
/// <returns>The interval unit.</returns>
private static Unit GetUnit(int?[] matchers, int?[] specials)
{
Unit unit1 = INTERVALS[1 + Array.IndexOf(matchers, null)].GetValueOrDefault(Unit.NULL),
unit2 = Unit.NULL;
if (specials[0].HasValue)
{
unit2 = Unit.WEEK;
}
if (unit2 == Unit.NULL)
{
return unit1;
}
return (unit1.CompareTo(unit2) < 0) ? unit2 : unit1;
}
/// <summary>
/// Gets the date from where to start calculating the initial trigger date.
/// </summary>
/// <param name="date">Usually now.</param>
/// <returns>The initial trigger date.</returns>
private DateTime GetBaseTriggerDate(DateTime date)
{
return new DateTime(
this.matchers[4].GetValueOrDefault(date.Year),
this.matchers[3].GetValueOrDefault(date.Month),
this.matchers[2].GetValueOrDefault(date.Day),
this.matchers[1].GetValueOrDefault(0),
this.matchers[0].GetValueOrDefault(0),
0);
}
/// <summary>
/// Gets the date when to trigger the notification.
/// </summary>
/// <param name="now">The date from where to calculate the trigger date.</param>
/// <returns>null if there's none trigger date.</returns>
private DateTime? GetTriggerDate(DateTime now)
{
var date = this.GetBaseTriggerDate(now);
if (date >= now)
{
return this.ApplySpecials(date);
}
if (this.Unit == Unit.NULL || date.Year < now.Year)
{
return null;
}
if (date.Month < now.Month)
{
switch (this.Unit)
{
case Unit.MINUTE:
case Unit.HOUR:
case Unit.DAY:
case Unit.WEEK:
if (!this.matchers[4].HasValue)
{
date = date.AddYears(now.Year - date.Year + 1);
break;
}
else
{
return null;
}
case Unit.YEAR:
date = date.AddYears(now.Year - date.Year + 1);
break;
}
}
else if (date.DayOfYear < now.DayOfYear)
{
switch (this.Unit)
{
case Unit.MINUTE:
case Unit.HOUR:
if (!this.matchers[3].HasValue)
{
date = date.AddMonths(now.Month - date.Month + 1);
}
else if (!this.matchers[4].HasValue)
{
date = date.AddYears(now.Year - date.Year + 1);
}
else
{
return null;
}
break;
case Unit.MONTH:
date = date.AddMonths(now.Month - date.Month + 1);
break;
case Unit.YEAR:
date = date.AddYears(now.Year - date.Year + 1);
break;
}
}
else if (date.Hour < now.Hour)
{
switch (this.Unit)
{
case Unit.MINUTE:
if (!this.matchers[2].HasValue)
{
date = date.AddDays(now.DayOfYear - date.DayOfYear + 1);
}
else if (!this.matchers[3].HasValue)
{
date = date.AddMonths(now.Month - date.Month + 1);
}
else
{
return null;
}
break;
case Unit.HOUR:
if (date.Minute < now.Minute)
{
date = date.AddHours(now.Hour - date.Hour + 1);
}
else
{
date = date.AddHours(now.Hour - date.Hour);
}
break;
case Unit.DAY:
case Unit.WEEK:
date = date.AddDays(now.Day - date.Day + 1);
break;
case Unit.MONTH:
date = date.AddMonths(now.Month - date.Month + 1);
break;
case Unit.YEAR:
date.AddYears(now.Year - date.Year + 1);
break;
}
}
else if (date.Minute < now.Minute)
{
switch (this.Unit)
{
case Unit.MINUTE:
date = date.AddMinutes(now.Minute - date.Minute + 1);
break;
case Unit.HOUR:
date = date.AddHours(now.Hour - date.Hour + 1);
break;
case Unit.DAY:
case Unit.WEEK:
date = date.AddDays(now.DayOfYear - date.DayOfYear + 1);
break;
case Unit.MONTH:
date = date.AddMonths(now.Month - date.Month + 1);
break;
case Unit.YEAR:
date = date.AddYears(now.Year - date.Year + 1);
break;
}
}
return this.ApplySpecials(date);
}
/// <summary>
/// Applies the special matching components.
/// </summary>
/// <param name="date">The date to manipulate</param>
/// <returns>The manipulated date.</returns>
private DateTime? ApplySpecials(DateTime date)
{
DateTime? cal = date;
if (this.specials[2].HasValue)
{
cal = this.SetWeekOfMonth(date);
}
if (this.specials[0].HasValue && cal.HasValue)
{
cal = this.SetDayOfWeek(cal.Value);
}
return cal;
}
/// <summary>
/// Set the day of the year but ensure that the calendar does point to a
/// date in future.
/// </summary>
/// <param name="date">The date to manipulate</param>
/// <returns>The calculatred date or null if not possible</returns>
private DateTime? SetDayOfWeek(DateTime date)
{
var day = WEEKDAYS[(int)date.DayOfWeek];
var month = date.Month;
var year = date.Year;
var dayToSet = this.specials[0].Value;
if (this.matchers[2].HasValue)
{
return null;
}
if (day > dayToSet)
{
if (!this.specials[2].HasValue)
{
date = date.AddDays(7);
}
else if (!this.matchers[3].HasValue)
{
date = date.AddMonths(1);
}
else if (!this.matchers[4].HasValue)
{
date = date.AddYears(1);
}
else
{
return null;
}
}
date = date.AddDays(dayToSet - day);
if (this.matchers[3].HasValue && date.Month != month)
{
return null;
}
if (this.matchers[4].HasValue && date.Year != year)
{
return null;
}
return date;
}
/// <summary>
/// Set the day of the year but ensure that the calendar does point to a
/// date in future.
/// </summary>
/// <param name="date">The date to manipulate</param>
/// <returns>The calculatred date or null if not possible</returns>
private DateTime? SetWeekOfMonth(DateTime date)
{
var week = this.GetWeekOFMonth(date);
var year = date.Year;
var weekToSet = this.specials[2].Value;
if (week > weekToSet)
{
if (!this.matchers[3].HasValue)
{
date = date.AddMonths(1);
}
else if (!this.matchers[4].HasValue)
{
date = date.AddYears(1);
}
else
{
return null;
}
if (this.matchers[4].HasValue && date.Year != year)
{
return null;
}
}
int month = date.Month;
date = date.AddDays((weekToSet - week) * 7);
if (date.Month != month)
{
date = new DateTime(date.Year, month, 1, date.Hour, date.Minute, 0);
}
else if (!this.matchers[2].HasValue && week != weekToSet)
{
date = date.AddDays(1 - WEEKDAYS[(int)date.DayOfWeek]);
}
return date;
}
/// <summary>
/// Calculates the week of month for the given date.
/// </summary>
/// <param name="date">The date to calculate the week number</param>
/// <returns>1..5</returns>
private int GetWeekOFMonth(DateTime date)
{
var cal = CultureInfo.CurrentCulture.Calendar;
var firstDay = new DateTime(date.Year, date.Month, 1);
var weekOfDate = cal.GetWeekOfYear(date, CalendarWeekRule.FirstDay, DayOfWeek.Monday);
var weekOfFirstDay = cal.GetWeekOfYear(firstDay, CalendarWeekRule.FirstDay, DayOfWeek.Monday);
return weekOfDate - weekOfFirstDay + 1;
}
}
}

View File

@ -0,0 +1,174 @@
/*
* 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>
/// <param name="code">The type of the notifications to return for.</param>
/// <returns>List of numbers</returns>
public int[] Ids(int code)
{
var type = Notification.Type.Unknown;
switch (code)
{
case 0:
type = Notification.Type.All;
break;
case 1:
type = Notification.Type.Scheduled;
break;
case 2:
type = Notification.Type.Triggered;
break;
}
return this.manager.GetIdsByType(type).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="code">The type of the notifications to return for.</param>
/// <param name="ids">Optional list of IDs to find.</param>
/// <returns>List of options instances</returns>
public Options[] Notifications(int code, [ReadOnlyArray] int[] ids)
{
var type = Notification.Type.Unknown;
switch (code)
{
case 0:
type = Notification.Type.All;
break;
case 1:
type = Notification.Type.Scheduled;
break;
case 2:
type = Notification.Type.Triggered;
break;
case 3:
return this.manager.GetOptions(ids).ToArray();
}
return this.manager.GetOptionsByType(type).ToArray();
}
}
}

View File

@ -0,0 +1,157 @@
<?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.16299.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\ActionGroup.cs" />
<Compile Include="LocalNotification\Builder.cs" />
<Compile Include="LocalNotification\Request.cs" />
<Compile Include="LocalNotification\Toast\Button.cs" />
<Compile Include="LocalNotification\Toast\Every.cs" />
<Compile Include="LocalNotification\Notification.cs" />
<Compile Include="LocalNotification\Toast\IAction.cs" />
<Compile Include="LocalNotification\Toast\Input.cs" />
<Compile Include="LocalNotification\Manager.cs" />
<Compile Include="LocalNotification\Options.cs" />
<Compile Include="LocalNotification\Toast\ProgressBar.cs" />
<Compile Include="LocalNotification\Toast\Trigger.cs" />
<Compile Include="LocalNotification\Trigger\DateTrigger.cs" />
<Compile Include="LocalNotification\Trigger\IntervalTrigger.cs" />
<Compile Include="LocalNotification\Trigger\MatchTrigger.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore">
<Version>5.0.2</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.Portable.Compatibility">
<Version>1.0.2</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>
<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>

View File

@ -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)]

File diff suppressed because it is too large Load Diff

21
index.html 100644
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>

39
jsconfig.json 100644
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": [
"src/*"
],
"app/*": [
"*"
],
"components/*": [
"src/components/*"
],
"layouts/*": [
"src/layouts/*"
],
"pages/*": [
"src/pages/*"
],
"assets/*": [
"src/assets/*"
],
"boot/*": [
"src/boot/*"
],
"stores/*": [
"src/stores/*"
],
"vue$": [
"node_modules/vue/dist/vue.runtime.esm-bundler.js"
]
}
},
"exclude": [
"dist",
".quasar",
"node_modules"
]
}

6522
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More