diff --git a/CHANGELOG.md b/CHANGELOG.md index 611e02f8b..87716553a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,45 @@ ChangeLog Please also read the [Upgrade Guide](https://github.com/katzer/cordova-plugin-local-notifications/wiki/Upgrade-Guide) for more information. +#### 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 diff --git a/README.md b/README.md index 878632ba0..20f853df4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,14 @@ +[![npm version](https://badge.fury.io/js/de.appplant.cordova.plugin.local-notification.svg)](http://badge.fury.io/js/de.appplant.cordova.plugin.local-notification) [![PayPayl donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L3HKQCD9UA35A "Donate once-off to this project using Paypal") -#### :bangbang: Please vote for these cordova-windows issues :bangbang: -1. https://issues.apache.org/jira/browse/CB-8674 _(Missing launch arguments)_ -2. https://issues.apache.org/jira/browse/CB-8946 _(Missing ToastCapable flag)_ - -Thanks a lot! - Cordova Local-Notification Plugin ================================= The essential purpose of local notifications is to enable an application to inform its users that it has something for them — for example, a message or an upcoming appointment — when the application isn’t running in the foreground.
They are scheduled by an application and delivered on the same device. - + ### How they appear to the user Users see notifications in the following ways: @@ -29,10 +24,11 @@ For example, applications that depend on servers for messages or data can poll t ## Supported Platforms The current 0.8 branch supports the following platforms: -- __iOS__ _(including iOS8)_
+- __iOS__ _(>= 8)_
- __Android__ _(SDK >=7)_ - __Windows 8.1__ _(added with v0.8.2)_ - __Windows Phone 8.1__ _(added with v0.8.2)_ +- __Windows 10__ _(added with v0.8.3)_ Find out more informations [here][wiki_platforms] in our wiki. @@ -119,7 +115,7 @@ Thank you! This software is released under the [Apache 2.0 License][apache2_license]. -© 2013-2015 appPlant UG, Inc. All rights reserved +© 2013-2016 appPlant UG, Inc. All rights reserved [cordova]: https://cordova.apache.org diff --git a/package.json b/package.json new file mode 100644 index 000000000..f4d685d29 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "de.appplant.cordova.plugin.local-notification", + "cordova_name": "Cordova LocalNotification Plugin", + "version": "0.8.4", + "description": "Schedules and queries for local notifications", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/katzer/cordova-plugin-local-notifications.git" + }, + "keywords": [ + "appplant", + "notification", + "local notification", + "cordova", + "ecosystem:cordova", + "cordova-android", + "cordova-ios", + "cordova-windows" + ], + "platforms": [ + "ios", + "android", + "windows" + ], + "engines": [ + { + "name": "cordova", + "version": ">=3.6.0" + }, + { + "name": "cordova-plugman", + "version": ">=4.3.0" + }, + { + "name": "cordova-windows", + "version": ">=4.2.0" + } + ], + "dependencies": { + "cordova-plugin-device": "*", + "cordova-plugin-app-event": ">=1.1.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" +} diff --git a/plugin.xml b/plugin.xml index 169260784..250391e0e 100644 --- a/plugin.xml +++ b/plugin.xml @@ -3,7 +3,7 @@ + version="0.8.4"> LocalNotification @@ -20,11 +20,14 @@ - + + + + @@ -53,8 +56,6 @@ - - @@ -79,7 +80,7 @@ - + @@ -203,6 +204,10 @@ + + + + @@ -215,14 +220,6 @@ - - - - - - - - diff --git a/scripts/windows/broadcastActivateEvent.js b/scripts/windows/broadcastActivateEvent.js deleted file mode 100755 index 40a90ca0a..000000000 --- a/scripts/windows/broadcastActivateEvent.js +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright (c) 2013-2015 by appPlant UG. All rights reserved. - * - * @APPPLANT_LICENSE_HEADER_START@ - * - * This file contains Original Code and/or Modifications of Original Code - * as defined in and that are subject to the Apache License - * Version 2.0 (the 'License'). You may not use this file except in - * compliance with the License. Please obtain a copy of the License at - * http://opensource.org/licenses/Apache-2.0/ and read it before using this - * file. - * - * The Original Code and all software distributed under the License are - * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER - * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, - * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. - * Please see the License for the specific language governing rights and - * limitations under the License. - * - * @APPPLANT_LICENSE_HEADER_END@ - */ - - -// Includes a snippet into the cordova-core js file -// to fire the activated event after device is ready - - -var fs = require('fs'), - rootdir = process.argv[2]; - -if (!rootdir) - return; - -/** - * Replaces a string with another one in a file. - * - * @param {String} path - * Absolute or relative file path from cordova root project. - * @param {String} to_replace - * The string to replace. - * @param {String} - * The string to replace with. - */ -function replace (filename, to_replace, replace_with) { - var data = fs.readFileSync(filename, 'utf8'), - result; - - if (data.indexOf(replace_with) > -1) - return; - - result = data.replace(to_replace, replace_with); - fs.writeFileSync(filename, result, 'utf8'); -} - -// Fires the activated event again after device is ready -var snippet = - "var activatedHandler = function (args) {" + - "channel.deviceready.subscribe(function () {" + - "app.queueEvent(args);" + - "});" + - "};" + - "app.addEventListener('activated', activatedHandler, false);" + - "document.addEventListener('deviceready', function () {" + - "app.removeEventListener('activated', activatedHandler);" + - "}, false);\n" + - " app.start();"; - -// Path to cordova-core js files where the snippet needs to be included -var files = [ - 'platforms/windows/www/cordova.js', - 'platforms/windows/platform_www/cordova.js' -]; - -// Includes the snippet before app.start() is called -for (var i = 0; i < files.length; i++) { - replace(files[i], 'app.start();', snippet); -} diff --git a/scripts/windows/setToastCapable.js b/scripts/windows/setToastCapable.js deleted file mode 100755 index 46b561024..000000000 --- a/scripts/windows/setToastCapable.js +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright (c) 2013-2015 by appPlant UG. All rights reserved. - * - * @APPPLANT_LICENSE_HEADER_START@ - * - * This file contains Original Code and/or Modifications of Original Code - * as defined in and that are subject to the Apache License - * Version 2.0 (the 'License'). You may not use this file except in - * compliance with the License. Please obtain a copy of the License at - * http://opensource.org/licenses/Apache-2.0/ and read it before using this - * file. - * - * The Original Code and all software distributed under the License are - * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER - * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, - * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. - * Please see the License for the specific language governing rights and - * limitations under the License. - * - * @APPPLANT_LICENSE_HEADER_END@ - */ - - -// Hook sets ToastCapable on true to enable local-notifications - - -var fs = require('fs'), - rootdir = process.argv[2]; - -if (!rootdir) - return; - -/** - * Replaces a string with another one in a file. - * - * @param {String} path - * Absolute or relative file path from cordova root project. - * @param {String} to_replace - * The string to replace. - * @param {String} - * The string to replace with. - */ -function replace (filename, to_replace, replace_with) { - var data = fs.readFileSync(filename, 'utf8'), - result; - - if (data.indexOf('ToastCapable') > -1) - return; - - result = data.replace(new RegExp(to_replace, 'g'), replace_with); - - fs.writeFileSync(filename, result, 'utf8'); -} - -// Set ToastCapable for Windows Phone -replace('platforms/windows/package.phone.appxmanifest', ' options = + getNotificationMgr().getOptionsBy(type, toList(ids)); - command.success(options); + if (options.isEmpty()) { + // Status.NO_RESULT led to no callback invocation :( + // Status.OK led to no NPE and crash + result = new PluginResult(PluginResult.Status.NO_RESULT); + } else { + result = new PluginResult(PluginResult.Status.OK, options.get(0)); + } + + command.sendPluginResult(result); } /** diff --git a/src/android/RestoreReceiver.java b/src/android/RestoreReceiver.java index 7de4e3282..675ea7cab 100644 --- a/src/android/RestoreReceiver.java +++ b/src/android/RestoreReceiver.java @@ -44,6 +44,8 @@ public class RestoreReceiver extends AbstractRestoreReceiver { public void onRestore (Notification notification) { if (notification.isScheduled()) { notification.schedule(); + } else { + notification.cancel(); } } diff --git a/src/android/notification/AbstractClickActivity.java b/src/android/notification/AbstractClickActivity.java index a02a9981e..5b70e1480 100644 --- a/src/android/notification/AbstractClickActivity.java +++ b/src/android/notification/AbstractClickActivity.java @@ -67,6 +67,16 @@ public void onCreate (Bundle state) { } } + /** + * Fixes "Unable to resume activity" error. + * Theme_NoDisplay: Activities finish themselves before being resumed. + */ + @Override + protected void onResume() { + super.onResume(); + finish(); + } + /** * Called when local notification was clicked by the user. * diff --git a/src/android/notification/AbstractTriggerReceiver.java b/src/android/notification/AbstractTriggerReceiver.java index fc6759c52..459e6d83a 100644 --- a/src/android/notification/AbstractTriggerReceiver.java +++ b/src/android/notification/AbstractTriggerReceiver.java @@ -70,7 +70,7 @@ public void onReceive(Context context, Intent intent) { Builder builder = new Builder(options); Notification notification = buildNotification(builder); - boolean updated = notification.isUpdate(); + boolean updated = notification.isUpdate(false); onTrigger(notification, updated); } diff --git a/src/android/notification/AssetUtil.java b/src/android/notification/AssetUtil.java index 2da8a2c3b..66662e557 100644 --- a/src/android/notification/AssetUtil.java +++ b/src/android/notification/AssetUtil.java @@ -42,6 +42,7 @@ 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:/// @@ -61,15 +62,15 @@ class AssetUtil { // resources and app directory. private final Context context; - /** - * Constructor - * - * @param context + /** + * Constructor + * + * @param context * Application context - */ - private AssetUtil(Context context) { - this.context = context; - } + */ + private AssetUtil(Context context) { + this.context = context; + } /** * Static method to retrieve class instance. @@ -100,223 +101,199 @@ Uri parseSound (String path) { return parse(path); } - /** - * The URI for a path. - * - * @param path + /** + * The URI for a path. + * + * @param path * The given path - */ + */ Uri parse (String path) { - 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); - } - - 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://", ""); - File file = new File(absPath); - - if (!file.exists()) { - Log.e("Asset", "File not found: " + file.getAbsolutePath()); - return Uri.EMPTY; - } - - return Uri.fromFile(file); - } - - /** - * URI for an asset. - * - * @param path - * Asset path like file://... - * - * @return + 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); + } + + return Uri.EMPTY; + } + + /** + * URI for a file. + * + * @param path + * Absolute path like file:///... + * + * @return * URI pointing to the given path - */ - private Uri getUriFromAsset(String path) { - File dir = context.getExternalCacheDir(); + */ + private Uri getUriFromPath(String path) { + String absPath = path.replaceFirst("file://", ""); + File file = new File(absPath); + + if (!file.exists()) { + Log.e("Asset", "File not found: " + file.getAbsolutePath()); + return Uri.EMPTY; + } - if (dir == null) { - Log.e("Asset", "Missing external cache dir"); - return Uri.EMPTY; - } + return Uri.fromFile(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"); String fileName = resPath.substring(resPath.lastIndexOf('/') + 1); - String storage = dir.toString() + STORAGE_FOLDER; - File file = new File(storage, fileName); + File file = getTmpFile(fileName); - //noinspection ResultOfMethodCallIgnored - new File(storage).mkdir(); + if (file == null) { + Log.e("Asset", "Missing external cache dir"); + return Uri.EMPTY; + } - try { - AssetManager assets = context.getAssets(); - FileOutputStream outStream = new FileOutputStream(file); - InputStream inputStream = assets.open(resPath); + try { + AssetManager assets = context.getAssets(); + FileOutputStream outStream = new FileOutputStream(file); + InputStream inputStream = assets.open(resPath); - copyFile(inputStream, outStream); + copyFile(inputStream, outStream); - outStream.flush(); - outStream.close(); + outStream.flush(); + outStream.close(); - return Uri.fromFile(file); + return Uri.fromFile(file); - } catch (Exception e) { - Log.e("Asset", "File not found: assets/" + resPath); - e.printStackTrace(); - } + } catch (Exception e) { + Log.e("Asset", "File not found: assets/" + resPath); + e.printStackTrace(); + } - return Uri.EMPTY; - } + return Uri.EMPTY; + } - /** - * The URI for a resource. - * - * @param path - * The given relative path - * - * @return + /** + * The URI for a resource. + * + * @param path + * The given relative path + * + * @return * URI pointing to the given path - */ - private Uri getUriForResourcePath(String path) { - File dir = context.getExternalCacheDir(); - - if (dir == null) { - Log.e("Asset", "Missing external cache dir"); - return Uri.EMPTY; - } - + */ + private Uri getUriForResourcePath(String path) { String resPath = path.replaceFirst("res://", ""); + int resId = getResIdForDrawable(resPath); + File file = getTmpFile(); - int resId = getResIdForDrawable(resPath); - - if (resId == 0) { - Log.e("Asset", "File not found: " + resPath); - return Uri.EMPTY; - } - - String resName = extractResourceName(resPath); - String extName = extractResourceExtension(resPath); - String storage = dir.toString() + STORAGE_FOLDER; - File file = new File(storage, resName + extName); + if (resId == 0) { + Log.e("Asset", "File not found: " + resPath); + return Uri.EMPTY; + } - //noinspection ResultOfMethodCallIgnored - new File(storage).mkdir(); + if (file == null) { + Log.e("Asset", "Missing external cache dir"); + return Uri.EMPTY; + } - try { - Resources res = context.getResources(); - FileOutputStream outStream = new FileOutputStream(file); - InputStream inputStream = res.openRawResource(resId); - copyFile(inputStream, outStream); + try { + Resources res = context.getResources(); + FileOutputStream outStream = new FileOutputStream(file); + InputStream inputStream = res.openRawResource(resId); + copyFile(inputStream, outStream); - outStream.flush(); - outStream.close(); + outStream.flush(); + outStream.close(); - return Uri.fromFile(file); + return Uri.fromFile(file); - } catch (Exception e) { - e.printStackTrace(); - } + } catch (Exception e) { + e.printStackTrace(); + } return Uri.EMPTY; - } + } - /** - * Uri from remote located content. + /** + * Uri from remote located content. * - * @param path + * @param path * Remote address * - * @return + * @return * Uri of the downloaded file - */ - private Uri getUriFromRemote(String path) { - File dir = context.getExternalCacheDir(); + */ + private Uri getUriFromRemote(String path) { + File file = getTmpFile(); - if (dir == null) { + if (file == null) { Log.e("Asset", "Missing external cache dir"); return Uri.EMPTY; } - String resName = extractResourceName(path); - String extName = extractResourceExtension(path); - String storage = dir.toString() + STORAGE_FOLDER; - File file = new File(storage, resName + extName); - - //noinspection ResultOfMethodCallIgnored - new File(storage).mkdir(); - try { URL url = new URL(path); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - StrictMode.ThreadPolicy policy = - new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.ThreadPolicy policy = + new StrictMode.ThreadPolicy.Builder().permitAll().build(); - StrictMode.setThreadPolicy(policy); + StrictMode.setThreadPolicy(policy); connection.setRequestProperty("Connection", "close"); connection.setConnectTimeout(5000); - connection.connect(); + connection.connect(); - InputStream input = connection.getInputStream(); - FileOutputStream outStream = new FileOutputStream(file); + InputStream input = connection.getInputStream(); + FileOutputStream outStream = new FileOutputStream(file); - copyFile(input, outStream); + copyFile(input, outStream); - outStream.flush(); - outStream.close(); + outStream.flush(); + outStream.close(); - return Uri.fromFile(file); + return Uri.fromFile(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(); - } + } 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, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - int read; - - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } + } + + /** + * Copy content from input stream into output stream. + * + * @param in + * The input stream + * @param out + * The output stream + */ + private void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } /** * Resource ID for drawable. @@ -343,7 +320,7 @@ int getResIdForDrawable(String resPath) { * Resource path as string */ int getResIdForDrawable(String clsName, String resPath) { - String drawable = extractResourceName(resPath); + String drawable = getBaseName(resPath); int resId = 0; try { @@ -372,7 +349,7 @@ Bitmap getIconFromDrawable (String drawable) { } if (iconId == 0) { - iconId = android.R.drawable.ic_menu_info_details; + iconId = android.R.drawable.screen_background_dark_transparent; } return BitmapFactory.decodeResource(res, iconId); @@ -396,7 +373,7 @@ Bitmap getIconFromUri (Uri uri) throws IOException { * @param resPath * Resource path as string */ - private String extractResourceName (String resPath) { + private String getBaseName (String resPath) { String drawable = resPath; if (drawable.contains("/")) { @@ -411,19 +388,39 @@ private String extractResourceName (String resPath) { } /** - * Extract extension of drawable resource from path. + * Returns a file located under the external cache dir of that app. * - * @param resPath - * Resource path as string + * @return + * File with a random UUID name */ - private String extractResourceExtension (String resPath) { - String extName = "png"; + 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()); + } - if (resPath.contains(".")) { - extName = resPath.substring(resPath.lastIndexOf('.')); + /** + * 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) { + Log.e("Asset", "Missing external cache dir"); + return null; } - return extName; + String storage = dir.toString() + STORAGE_FOLDER; + + //noinspection ResultOfMethodCallIgnored + new File(storage).mkdir(); + + return new File(storage, name); } /** diff --git a/src/android/notification/Builder.java b/src/android/notification/Builder.java index a0be8b939..ab8db3dce 100644 --- a/src/android/notification/Builder.java +++ b/src/android/notification/Builder.java @@ -115,30 +115,36 @@ public Builder setClickActivity(Class activity) { * Creates the notification with all its options passed through JS. */ public Notification build() { - Uri sound = options.getSoundUri(); - NotificationCompat.BigTextStyle style; + Uri sound = options.getSoundUri(); + int smallIcon = options.getSmallIcon(); + int ledColor = options.getLedColor(); NotificationCompat.Builder builder; - style = new NotificationCompat.BigTextStyle() - .bigText(options.getText()); - builder = new NotificationCompat.Builder(context) .setDefaults(0) .setContentTitle(options.getTitle()) .setContentText(options.getText()) .setNumber(options.getBadgeNumber()) .setTicker(options.getText()) - .setSmallIcon(options.getSmallIcon()) - .setLargeIcon(options.getIconBitmap()) .setAutoCancel(options.isAutoClear()) .setOngoing(options.isOngoing()) - .setStyle(style) - .setLights(options.getLedColor(), 500, 500); + .setColor(options.getColor()); + + if (ledColor != 0) { + builder.setLights(ledColor, options.getLedOnTime(), options.getLedOffTime()); + } if (sound != null) { builder.setSound(sound); } + if (smallIcon == 0) { + builder.setSmallIcon(options.getIcon()); + } else { + builder.setSmallIcon(options.getSmallIcon()); + builder.setLargeIcon(options.getIconBitmap()); + } + applyDeleteReceiver(builder); applyContentReceiver(builder); @@ -157,14 +163,14 @@ private void applyDeleteReceiver(NotificationCompat.Builder builder) { if (clearReceiver == null) return; - Intent deleteIntent = new Intent(context, clearReceiver) + Intent intent = new Intent(context, clearReceiver) .setAction(options.getIdStr()) .putExtra(Options.EXTRA, options.toString()); - PendingIntent dpi = PendingIntent.getBroadcast( - context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent deleteIntent = PendingIntent.getBroadcast( + context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - builder.setDeleteIntent(dpi); + builder.setDeleteIntent(deleteIntent); } /** @@ -183,10 +189,10 @@ private void applyContentReceiver(NotificationCompat.Builder builder) { .putExtra(Options.EXTRA, options.toString()) .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - int requestCode = new Random().nextInt(); + int reqCode = new Random().nextInt(); PendingIntent contentIntent = PendingIntent.getActivity( - context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT); + context, reqCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(contentIntent); } diff --git a/src/android/notification/ClickActivity.java b/src/android/notification/ClickActivity.java index 01af5c457..dcc0390dc 100644 --- a/src/android/notification/ClickActivity.java +++ b/src/android/notification/ClickActivity.java @@ -40,6 +40,12 @@ public class ClickActivity extends AbstractClickActivity { @Override public void onClick(Notification notification) { launchApp(); + + if (notification.isRepeating()) { + notification.clear(); + } else { + notification.cancel(); + } } /** diff --git a/src/android/notification/Manager.java b/src/android/notification/Manager.java index 03ea384f9..9af3c94f2 100644 --- a/src/android/notification/Manager.java +++ b/src/android/notification/Manager.java @@ -31,7 +31,6 @@ import org.json.JSONObject; import java.util.ArrayList; -import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -121,7 +120,7 @@ public Notification update (int id, JSONObject updates, Class receiver) { notification.getOptions().getDict(), updates); try { - options.putOpt("updatedAt", new Date().getTime()); + options.put("updated", true); } catch (JSONException ignore) {} return schedule(options, receiver); @@ -193,7 +192,11 @@ public List getIds() { ArrayList ids = new ArrayList(); for (String key : keys) { - ids.add(Integer.parseInt(key)); + try { + ids.add(Integer.parseInt(key)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } } return ids; diff --git a/src/android/notification/Notification.java b/src/android/notification/Notification.java index 5dba9d54f..ad363788d 100644 --- a/src/android/notification/Notification.java +++ b/src/android/notification/Notification.java @@ -138,24 +138,25 @@ public boolean isTriggered () { /** * If the notification is an update. + * + * @param keepFlag + * Set to false to remove the flag from the option map */ - protected boolean isUpdate () { - - if (!options.getDict().has("updatedAt")) - return false; + protected boolean isUpdate (boolean keepFlag) { + boolean updated = options.getDict().optBoolean("updated", false); - long now = new Date().getTime(); - - long updatedAt = options.getDict().optLong("updatedAt", now); + if (!keepFlag) { + options.getDict().remove("updated"); + } - return (now - updatedAt) < 1000; + return updated; } /** * Notification type can be one of pending or scheduled. */ public Type getType () { - return isTriggered() ? Type.TRIGGERED : Type.SCHEDULED; + return isScheduled() ? Type.SCHEDULED : Type.TRIGGERED; } /** @@ -184,14 +185,14 @@ public void schedule() { /** * Clear the local notification without canceling repeating alarms. - * */ public void clear () { - if (!isRepeating() && wasInThePast()) { + + if (!isRepeating() && wasInThePast()) unpersist(); - } else { + + if (!isRepeating()) getNotMgr().cancel(getId()); - } } /** @@ -239,13 +240,6 @@ private void showNotification () { } } - /** - * Show as modal dialog when in foreground. - */ - private void showDialog () { - // TODO - } - /** * Count of triggers since schedule. */ @@ -276,7 +270,7 @@ public String toString() { } json.remove("firstAt"); - json.remove("updatedAt"); + json.remove("updated"); json.remove("soundUri"); json.remove("iconUri"); diff --git a/src/android/notification/Options.java b/src/android/notification/Options.java index 198a52f41..e78035958 100644 --- a/src/android/notification/Options.java +++ b/src/android/notification/Options.java @@ -27,6 +27,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; +import android.support.v4.app.NotificationCompat; import org.json.JSONException; import org.json.JSONObject; @@ -109,6 +110,9 @@ private void parseInterval() { if (every.equals("month")) { interval = AlarmManager.INTERVAL_DAY * 31; } else + if (every.equals("quarter")) { + interval = AlarmManager.INTERVAL_HOUR * 2190; + } else if (every.equals("year")) { interval = AlarmManager.INTERVAL_DAY * 365; } else { @@ -125,10 +129,10 @@ private void parseInterval() { */ private void parseAssets() { - if (options.has("iconUri")) + if (options.has("iconUri") && !options.optBoolean("updated")) return; - Uri iconUri = assets.parse(options.optString("icon", "icon")); + Uri iconUri = assets.parse(options.optString("icon", "res://icon")); Uri soundUri = assets.parseSound(options.optString("sound", null)); try { @@ -213,10 +217,7 @@ public Date getTriggerDate() { * Trigger date in milliseconds. */ public long getTriggerTime() { - return Math.max( - System.currentTimeMillis(), - options.optLong("at", 0) * 1000 - ); + return options.optLong("at", 0) * 1000; } /** @@ -238,12 +239,68 @@ public String getTitle() { * The notification color for LED */ public int getLedColor() { - String hex = options.optString("led", "000000"); - int aRGB = Integer.parseInt(hex,16); + String hex = options.optString("led", null); + + if (hex == null) { + return 0; + } - aRGB += 0xFF000000; + int aRGB = Integer.parseInt(hex, 16); - return aRGB; + return aRGB + 0xFF000000; + } + + /** + * @return + * The time that the LED should be on (in milliseconds). + */ + public int getLedOnTime() { + String timeOn = options.optString("ledOnTime", null); + + if (timeOn == null) { + return 1000; + } + + try { + return Integer.parseInt(timeOn); + } catch (NumberFormatException e) { + return 1000; + } + } + + /** + * @return + * The time that the LED should be off (in milliseconds). + */ + public int getLedOffTime() { + String timeOff = options.optString("ledOffTime", null); + + if (timeOff == null) { + return 1000; + } + + try { + return Integer.parseInt(timeOff); + } catch (NumberFormatException e) { + return 1000; + } + } + + /** + * @return + * The notification background color for the small icon + * Returns null, if no color is given. + */ + public int getColor() { + String hex = options.optString("color", null); + + if (hex == null) { + return NotificationCompat.COLOR_DEFAULT; + } + + int aRGB = Integer.parseInt(hex, 16); + + return aRGB + 0xFF000000; } /** @@ -265,34 +322,47 @@ public Uri getSoundUri() { * Icon bitmap for the local notification. */ public Bitmap getIconBitmap() { - String icon = options.optString("icon", "icon"); Bitmap bmp; - try{ + try { Uri uri = Uri.parse(options.optString("iconUri")); bmp = assets.getIconFromUri(uri); } catch (Exception e){ - bmp = assets.getIconFromDrawable(icon); + e.printStackTrace(); + bmp = assets.getIconFromDrawable("icon"); } return bmp; } /** - * Small icon resource ID for the local notification. + * Icon resource ID for the local notification. */ - public int getSmallIcon () { - String icon = options.optString("smallIcon", ""); + public int getIcon () { + String icon = options.optString("icon", ""); int resId = assets.getResIdForDrawable(icon); if (resId == 0) { - resId = android.R.drawable.screen_background_dark; + resId = getSmallIcon(); + } + + if (resId == 0) { + resId = android.R.drawable.ic_popup_reminder; } return resId; } + /** + * Small icon resource ID for the local notification. + */ + public int getSmallIcon () { + String icon = options.optString("smallIcon", ""); + + return assets.getResIdForDrawable(icon); + } + /** * JSON object as string. */ diff --git a/src/ios/APPLocalNotification.m b/src/ios/APPLocalNotification.m index 43cf9f8cb..03a2d23e5 100644 --- a/src/ios/APPLocalNotification.m +++ b/src/ios/APPLocalNotification.m @@ -25,7 +25,6 @@ #import "APPLocalNotificationOptions.h" #import "UIApplication+APPLocalNotification.h" #import "UILocalNotification+APPLocalNotification.h" -#import "AppDelegate+APPRegisterUserNotificationSettings.h" @interface APPLocalNotification () @@ -393,7 +392,7 @@ - (void) getOption:(CDVInvokedUrlCommand*)command } result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK - messageAsDictionary:notifications[0]]; + messageAsDictionary:[notifications firstObject]]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; @@ -571,12 +570,11 @@ - (void) didReceiveLocalNotification:(NSNotification*)localNotification { UILocalNotification* notification = [localNotification object]; - if ([notification wasUpdated]) + if ([notification userInfo] == NULL || [notification wasUpdated]) return; NSTimeInterval timeInterval = [notification timeIntervalSinceLastTrigger]; - - NSString* event = (timeInterval <= 1 && deviceready) ? @"trigger" : @"click"; + NSString* event = timeInterval < 0.2 && deviceready ? @"trigger" : @"click"; [self fireEvent:event notification:notification]; @@ -616,8 +614,7 @@ - (void) didFinishLaunchingWithOptions:(NSNotification*)notification */ - (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings*)settings { - if (_command) - { + if (_command) { [self hasPermission:_command]; _command = NULL; } @@ -631,25 +628,7 @@ - (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings*)settin */ - (void) pluginInitialize { - NSNotificationCenter* center = [NSNotificationCenter - defaultCenter]; - eventQueue = [[NSMutableArray alloc] init]; - - [center addObserver:self - selector:@selector(didReceiveLocalNotification:) - name:CDVLocalNotification - object:nil]; - - [center addObserver:self - selector:@selector(didFinishLaunchingWithOptions:) - name:UIApplicationDidFinishLaunchingNotification - object:nil]; - - [center addObserver:self - selector:@selector(didRegisterUserNotificationSettings:) - name:UIApplicationRegisterUserNotificationSettings - object:nil]; } /** diff --git a/src/ios/APPLocalNotificationOptions.h b/src/ios/APPLocalNotificationOptions.h index 73c3ef758..627ba00f7 100644 --- a/src/ios/APPLocalNotificationOptions.h +++ b/src/ios/APPLocalNotificationOptions.h @@ -20,6 +20,9 @@ * * @APPPLANT_LICENSE_HEADER_END@ */ + +#import +#import @interface APPLocalNotificationOptions : NSObject diff --git a/src/ios/APPLocalNotificationOptions.m b/src/ios/APPLocalNotificationOptions.m index ce931ce7e..6918919b4 100644 --- a/src/ios/APPLocalNotificationOptions.m +++ b/src/ios/APPLocalNotificationOptions.m @@ -46,7 +46,7 @@ @implementation APPLocalNotificationOptions */ - (id) initWithDict:(NSDictionary*)dictionary { - self = [super init]; + self = [self init]; self.dict = dictionary; @@ -173,6 +173,9 @@ - (NSCalendarUnit) repeatInterval else if ([interval isEqualToString:@"month"]) { return NSCalendarUnitMonth; } + else if ([interval isEqualToString:@"quarter"]) { + return NSCalendarUnitQuarter; + } else if ([interval isEqualToString:@"year"]) { return NSCalendarUnitYear; } diff --git a/src/ios/UIApplication+APPLocalNotification.m b/src/ios/UIApplication+APPLocalNotification.m index 60b6daac3..21fab3199 100644 --- a/src/ios/UIApplication+APPLocalNotification.m +++ b/src/ios/UIApplication+APPLocalNotification.m @@ -188,7 +188,9 @@ - (UILocalNotification*) localNotificationWithId:(NSNumber*)id for (UILocalNotification* notification in notifications) { - if ([notification.options.id isEqualToNumber:id]) { + NSString* fid = [NSString stringWithFormat:@"%@", notification.options.id]; + + if ([fid isEqualToString:[id stringValue]]) { return notification; } } diff --git a/src/ios/UILocalNotification+APPLocalNotification.m b/src/ios/UILocalNotification+APPLocalNotification.m index 80012f89f..383725389 100644 --- a/src/ios/UILocalNotification+APPLocalNotification.m +++ b/src/ios/UILocalNotification+APPLocalNotification.m @@ -41,7 +41,7 @@ @implementation UILocalNotification (APPLocalNotification) */ - (id) initWithOptions:(NSDictionary*)dict { - self = [super init]; + self = [self init]; [self setUserInfo:dict]; [self __init]; @@ -168,9 +168,12 @@ - (NSString*) encodeToJSON [obj removeObjectForKey:@"updatedAt"]; + if (obj == NULL || obj.count == 0) + return json; + data = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingPrettyPrinted - error:Nil]; + error:NULL]; json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; diff --git a/src/windows/LocalNotificationCore.js b/src/windows/LocalNotificationCore.js index f27193e17..beb18148a 100644 --- a/src/windows/LocalNotificationCore.js +++ b/src/windows/LocalNotificationCore.js @@ -103,9 +103,7 @@ proxy.core = { toast.id = options.id; toast.tag = 'Toast' + toast.id; - Notifications.ToastNotificationManager - .createToastNotifier() - .addToSchedule(toast); + this.getToastNotifier().addToSchedule(toast); }, /** @@ -326,9 +324,12 @@ proxy.core = { ids = []; for (var i = 0; i < toasts.length; i++) { - var toast = toasts[i]; + var toast = toasts[i], + toastId = this.getToastId(toast); - ids.push(this.getToastId(toast)); + if (ids.indexOf(toastId) == -1) { + ids.push(toastId); + } } return ids; diff --git a/src/windows/LocalNotificationUtil.js b/src/windows/LocalNotificationUtil.js index 4081a0b84..5679254f6 100644 --- a/src/windows/LocalNotificationUtil.js +++ b/src/windows/LocalNotificationUtil.js @@ -412,8 +412,8 @@ channel.onCordovaReady.subscribe(function () { }); // Handle onclick event -WinJS.Application.addEventListener('activated', function (args) { - var id = args.detail.arguments, +document.addEventListener('activated', function (e) { + var id = e.args, notification = exports.getAll([id])[0]; if (!notification) diff --git a/www/local-notification-core.js b/www/local-notification-core.js index 03faa834e..afcc24575 100644 --- a/www/local-notification-core.js +++ b/www/local-notification-core.js @@ -55,52 +55,74 @@ exports.setDefaults = function (newDefaults) { /** * Schedule a new local notification. * - * @param {Object} opts + * @param {Object} msgs * The notification properties * @param {Function} callback * A function to be called after the notification has been canceled * @param {Object?} scope * The scope for the callback function + * @param {Object?} args + * skipPermission:true schedules the notifications immediatly without + * registering or checking for permission */ -exports.schedule = function (opts, callback, scope) { - this.registerPermission(function(granted) { +exports.schedule = function (msgs, callback, scope, args) { + var fn = function(granted) { - if (!granted) - return; + if (!granted) return; - var notifications = Array.isArray(opts) ? opts : [opts]; + var notifications = Array.isArray(msgs) ? msgs : [msgs]; for (var i = 0; i < notifications.length; i++) { - var properties = notifications[i]; + var notification = notifications[i]; - this.mergeWithDefaults(properties); - this.convertProperties(properties); + this.mergeWithDefaults(notification); + this.convertProperties(notification); } this.exec('schedule', notifications, callback, scope); - }, this); + }; + + if (args && args.skipPermission) { + fn.call(this, true); + } else { + this.registerPermission(fn, this); + } }; /** * Update existing notifications specified by IDs in options. * - * @param {Object} options + * @param {Object} notifications * The notification properties to update * @param {Function} callback * A function to be called after the notification has been updated * @param {Object?} scope * The scope for the callback function + * @param {Object?} args + * skipPermission:true schedules the notifications immediatly without + * registering or checking for permission */ -exports.update = function (opts, callback, scope) { - var notifications = Array.isArray(opts) ? opts : [opts]; +exports.update = function (msgs, callback, scope, args) { + var fn = function(granted) { - for (var i = 0; i < notifications.length; i++) { - var properties = notifications[i]; + if (!granted) return; - this.convertProperties(properties); - } + var notifications = Array.isArray(msgs) ? msgs : [msgs]; + + for (var i = 0; i < notifications.length; i++) { + var notification = notifications[i]; + + this.convertProperties(notification); + } - this.exec('update', notifications, callback, scope); + this.exec('update', notifications, callback, scope); + }; + + if (args && args.skipPermission) { + fn.call(this, true); + } else { + this.registerPermission(fn, this); + } }; /** @@ -414,6 +436,13 @@ exports.hasPermission = function (callback, scope) { * The callback function's scope */ exports.registerPermission = function (callback, scope) { + + if (this._registered) { + return this.hasPermission(callback, scope); + } else { + this._registered = true; + } + var fn = this.createCallbackFn(callback, scope); if (device.platform != 'iOS') { @@ -441,6 +470,9 @@ exports.registerPermission = function (callback, scope) { */ exports.on = function (event, callback, scope) { + if (typeof callback !== "function") + return; + if (!this._listener[event]) { this._listener[event] = []; } diff --git a/www/local-notification-util.js b/www/local-notification-util.js index 7e96b64c5..e87409a66 100644 --- a/www/local-notification-util.js +++ b/www/local-notification-util.js @@ -44,6 +44,9 @@ exports._defaults = { // listener exports._listener = {}; +// Registered permission flag +exports._registered = false; + /******** * UTIL * @@ -60,11 +63,14 @@ exports.applyPlatformSpecificOptions = function () { switch (device.platform) { case 'Android': - defaults.icon = 'res://icon'; - defaults.smallIcon = 'res://ic_popup_reminder'; + defaults.icon = 'res://ic_popup_reminder'; + defaults.smallIcon = undefined; defaults.ongoing = false; defaults.autoClear = true; - defaults.led = 'FFFFFF'; + defaults.led = undefined; + defaults.ledOnTime = undefined; + defaults.ledOffTime = undefined; + defaults.color = undefined; break; } @@ -168,6 +174,16 @@ exports.convertProperties = function (options) { options.data = JSON.stringify(options.data); } + if (options.every) { + if (device.platform == 'iOS' && typeof options.every != 'string') { + options.every = this.getDefaults().every; + var warning = 'Every option is not a string: ' + options.id; + warning += '. Expects one of: second, minute, hour, day, week, '; + warning += 'month, year on iOS.'; + console.warn(warning); + } + } + return options; }; diff --git a/www/local-notification.js b/www/local-notification.js index 9e9bd2844..fc647a6e3 100644 --- a/www/local-notification.js +++ b/www/local-notification.js @@ -47,29 +47,35 @@ exports.setDefaults = function (defaults) { /** * Schedule a new local notification. * - * @param {Object} opts + * @param {Object} notifications * The notification properties * @param {Function} callback * A function to be called after the notification has been canceled * @param {Object?} scope * The scope for the callback function + * @param {Object?} args + * skipPermission:true schedules the notifications immediatly without + * registering or checking for permission */ -exports.schedule = function (opts, callback, scope) { - this.core.schedule(opts, callback, scope); +exports.schedule = function (notifications, callback, scope, args) { + this.core.schedule(notifications, callback, scope, args); }; /** * Update existing notifications specified by IDs in options. * - * @param {Object} options + * @param {Object} notifications * The notification properties to update * @param {Function} callback * A function to be called after the notification has been updated * @param {Object?} scope * The scope for the callback function + * @param {Object?} args + * skipPermission:true schedules the notifications immediatly without + * registering or checking for permission */ -exports.update = function (opts, callback, scope) { - this.core.update(opts, callback, scope); +exports.update = function (notifications, callback, scope, args) { + this.core.update(notifications, callback, scope, args); }; /** @@ -366,5 +372,5 @@ exports.on = function (event, callback, scope) { * The function to be exec as callback */ exports.un = function (event, callback) { - this.core.un(event, callback, scope); + this.core.un(event, callback); };