diff --git a/src/core/emulator/providers/helper.ts b/src/core/emulator/providers/helper.ts index 53317da62..4c866ebc8 100644 --- a/src/core/emulator/providers/helper.ts +++ b/src/core/emulator/providers/helper.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileProvider } from '@providers/file'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { File } from '@ionic-native/file'; -import { LocalNotifications } from '@ionic-native/local-notifications'; +import { LocalNotifications, ILocalNotification } from '@ionic-native/local-notifications'; import { CoreAppProvider } from '@providers/app'; import { CoreInitDelegate, CoreInitHandler } from '@providers/init'; import { CoreLoggerProvider } from '@providers/logger'; @@ -157,9 +157,11 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler { // There is a new notification, show it. return getDataFn(notification).then((titleAndText) => { - const localNotif = { + const localNotif: ILocalNotification = { id: 1, - at: new Date(), + trigger: { + at: new Date() + }, title: titleAndText.title, text: titleAndText.text, data: { diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index d1685ec80..0936960e7 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { LocalNotifications, ILocalNotification } from '@ionic-native/local-notifications'; +import { LocalNotifications, ILocalNotification, ILocalNotificationAction } from '@ionic-native/local-notifications'; import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -21,6 +21,7 @@ import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; import * as moment from 'moment'; +import { Subject, Observable } from 'rxjs'; /** * Emulates the Cordova Globalization plugin in desktop apps and in browser. @@ -76,16 +77,40 @@ export class LocalNotificationsMock extends LocalNotifications { protected appDB: SQLiteDB; protected scheduled: { [i: number]: any } = {}; protected triggered: { [i: number]: any } = {}; - protected observers; + protected observers: {[event: string]: Subject}; protected defaults = { - text: '', - title: '', - sound: '', - badge: 0, - id: 0, - data: undefined, - every: undefined, - at: undefined + actions : [], + attachments : [], + autoClear : true, + badge : null, + channel : null, + clock : true, + color : null, + data : null, + defaults : 0, + foreground : null, + group : null, + groupSummary : false, + icon : null, + id : 0, + launch : true, + led : true, + lockscreen : true, + mediaSession : null, + number : 0, + priority : 0, + progressBar : false, + silent : false, + smallIcon : 'res://icon', + sound : true, + sticky : false, + summary : null, + text : '', + timeoutAfter : false, + title : '', + trigger : { type : 'calendar' }, + vibrate : false, + wakeup : true }; constructor(private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider) { @@ -96,20 +121,32 @@ export class LocalNotificationsMock extends LocalNotifications { // Initialize observers. this.observers = { - schedule: [], - trigger: [], - click: [], - update: [], - clear: [], - clearall: [], - cancel: [], - cancelall: [] + schedule: new Subject(), + trigger: new Subject(), + click: new Subject(), + update: new Subject(), + clear: new Subject(), + clearall: new Subject(), + cancel: new Subject(), + cancelall: new Subject(), }; } /** - * Cancels single or multiple notifications - * @param notificationId {any} A single notification id, or an array of notification ids. + * Adds a group of actions. + * + * @param {any} groupId The id of the action group + * @param {ILocalNotificationAction[]} actions The actions of this group + * @returns {Promise} + */ + addActions(groupId: any, actions: ILocalNotificationAction[]): Promise { + return Promise.reject('Not supported in desktop apps.'); + } + + /** + * Cancels single or multiple notifications. + * + * @param {any} notificationId A single notification id, or an array of notification ids. * @returns {Promise} Returns a promise when the notification is canceled */ cancel(notificationId: any): Promise { @@ -135,7 +172,11 @@ export class LocalNotificationsMock extends LocalNotifications { */ cancelAll(): Promise { return this.cancel(Object.keys(this.scheduled)).then(() => { - this.triggerEvent('cancelall', 'foreground'); + this.fireEvent('cancelall', { + event: 'cancelall', + foreground: true, + queued: false + }); }); } @@ -162,7 +203,7 @@ export class LocalNotificationsMock extends LocalNotifications { this.removeNotification(id); if (!omitEvent) { - this.triggerEvent(eventName, notification, 'foreground'); + this.fireEvent(eventName, notification); } } @@ -181,7 +222,8 @@ export class LocalNotificationsMock extends LocalNotifications { // Clear the notifications. notificationId.forEach((id) => { // Cancel only the notifications that aren't repeating. - if (this.scheduled[id] && this.scheduled[id].notification && !this.scheduled[id].notification.every) { + if (this.scheduled[id] && this.scheduled[id].notification && + (!this.scheduled[id].notification.trigger || !this.scheduled[id].notification.trigger.every)) { promises.push(this.cancelNotification(id, false, 'clear')); } }); @@ -195,7 +237,11 @@ export class LocalNotificationsMock extends LocalNotifications { */ clearAll(): Promise { return this.clear(Object.keys(this.scheduled)).then(() => { - this.triggerEvent('clearall', 'foreground'); + this.fireEvent('clearall', { + event: 'clearall', + foreground: true, + queued: false + }); }); } @@ -225,44 +271,251 @@ export class LocalNotificationsMock extends LocalNotifications { */ protected convertProperties(notification: ILocalNotification): ILocalNotification { if (notification.id) { - if (isNaN(notification.id)) { - notification.id = this.defaults.id; - } else { - notification.id = Number(notification.id); - } + notification.id = this.parseToInt('id', notification); } if (notification.title) { notification.title = notification.title.toString(); } - if (notification.text) { - notification.text = notification.text.toString(); - } - if (notification.badge) { - if (isNaN(notification.badge)) { - notification.badge = this.defaults.badge; - } else { - notification.badge = Number(notification.badge); - } + notification.badge = this.parseToInt('badge', notification); } - if (notification.at) { - if (typeof notification.at == 'object') { - notification.at = notification.at.getTime(); - } + if (notification.defaults) { + notification.defaults = this.parseToInt('defaults', notification); + } - notification.at = Math.round(notification.at / 1000); + if (typeof notification.timeoutAfter === 'boolean') { + notification.timeoutAfter = notification.timeoutAfter ? 3600000 : null; + } + + if (notification.timeoutAfter) { + notification.timeoutAfter = this.parseToInt('timeoutAfter', notification); } if (typeof notification.data == 'object') { notification.data = JSON.stringify(notification.data); } + this.convertPriority(notification); + this.convertTrigger(notification); + this.convertActions(notification); + this.convertProgressBar(notification); + return notification; } + /** + * Parse a property to number, returning the default value if not valid. + * Code extracted from the Cordova plugin. + * + * @param {string} prop Name of property to convert. + * @param {any} notification Notification where to search the property. + * @return {number} Converted number or default value. + */ + protected parseToInt(prop: string, notification: any): number { + if (isNaN(notification[prop])) { + return this.defaults[prop]; + } else { + return Number(notification[prop]); + } + } + + /** + * Convert the priority of a notification. + * Code extracted from the Cordova plugin. + * + * @param {any} notification Notification. + * @return {any} Notification. + */ + protected convertPriority(notification: any): any { + let prio = notification.priority || notification.prio || 0; + + if (typeof prio === 'string') { + prio = { min: -2, low: -1, high: 1, max: 2 }[prio] || 0; + } + + if (notification.foreground === true) { + prio = Math.max(prio, 1); + } + + if (notification.foreground === false) { + prio = Math.min(prio, 0); + } + + notification.priority = prio; + + return notification; + } + + /** + * Convert the actions of a notification. + * Code extracted from the Cordova plugin. + * + * @param {any} notification Notification. + * @return {any} Notification. + */ + protected convertActions(notification: any): any { + const actions = []; + + if (!notification.actions || typeof notification.actions === 'string') { + return notification; + } + + for (let i = 0, len = notification.actions.length; i < len; i++) { + const action = notification.actions[i]; + + if (!action.id) { + // Ignore action, it has no ID. + continue; + } + + action.id = action.id.toString(); + + actions.push(action); + } + + notification.actions = actions; + + return notification; + } + + /** + * Convert the trigger of a notification. + * Code extracted from the Cordova plugin. + * + * @param {any} notification Notification. + * @return {any} Notification. + */ + protected convertTrigger(notification: any): any { + const trigger = notification.trigger || {}; + let date = this.getValueFor(trigger, 'at', 'firstAt', 'date'); + + const dateToNum = (date: any): number => { + const num = typeof date == 'object' ? date.getTime() : date; + + return Math.round(num); + }; + + if (!notification.trigger) { + return notification; + } + + if (!trigger.type) { + trigger.type = trigger.center ? 'location' : 'calendar'; + } + + const isCal = trigger.type == 'calendar'; + + if (isCal && !date) { + date = this.getValueFor(notification, 'at', 'firstAt', 'date'); + } + + if (isCal && !trigger.every && notification.every) { + trigger.every = notification.every; + } + + if (isCal && (trigger.in || trigger.every)) { + date = null; + } + + if (isCal && date) { + trigger.at = dateToNum(date); + } + + if (isCal && trigger.firstAt) { + trigger.firstAt = dateToNum(trigger.firstAt); + } + + if (isCal && trigger.before) { + trigger.before = dateToNum(trigger.before); + } + + if (isCal && trigger.after) { + trigger.after = dateToNum(trigger.after); + } + + if (!trigger.count) { + trigger.count = trigger.every ? 5 : 1; + } + + if (!isCal) { + trigger.notifyOnEntry = !!trigger.notifyOnEntry; + trigger.notifyOnExit = trigger.notifyOnExit === true; + trigger.radius = trigger.radius || 5; + trigger.single = !!trigger.single; + } + + if (!isCal || trigger.at) { + delete trigger.every; + } + + delete notification.every; + delete notification.at; + delete notification.firstAt; + delete notification.date; + + notification.trigger = trigger; + + return notification; + } + + /** + * Convert the progress bar of a notification. + * Code extracted from the Cordova plugin. + * + * @param {any} notification Notification. + * @return {any} Notification. + */ + protected convertProgressBar(notification: any): any { + let cfg = notification.progressBar; + + if (cfg === undefined) { + return notification; + } + + if (typeof cfg === 'boolean') { + cfg = notification.progressBar = { enabled: cfg }; + } + + if (typeof cfg.enabled !== 'boolean') { + cfg.enabled = !!(cfg.value || cfg.maxValue || cfg.indeterminate !== null); + } + + cfg.value = cfg.value || 0; + + cfg.enabled = !!cfg.enabled; + + if (cfg.enabled && notification.clock === true) { + notification.clock = 'chronometer'; + } + + return notification; + } + + /** + * Not an official interface, however its possible to manually fire events. + * + * @param {string} eventName The name of the event. Available events: schedule, trigger, click, update, clear, clearall, cancel, + * cancelall. Custom event names are possible for actions + * @param {any} args Optional arguments + */ + fireEvent(eventName: string, args: any): void { + if (this.observers[eventName]) { + this.observers[eventName].next(args); + } + } + + /** + * Fire queued events once the device is ready and all listeners are registered. + * + * @returns {Promise} + */ + fireQueuedEvents(): Promise { + return Promise.resolve(); + } + /** * Get a notification object. * @@ -282,12 +535,21 @@ export class LocalNotificationsMock extends LocalNotifications { return Promise.resolve(this.getNotifications(undefined, true, true)); } + /** + * Gets the (platform specific) default settings. + * + * @returns {Promise} An object with all default settings + */ + getDefaults(): Promise { + return Promise.resolve(this.defaults); + } + /** * Get all the notification ids. * * @returns {Promise>} */ - getAllIds(): Promise> { + getIds(): Promise> { let ids = this.utils.mergeArraysWithoutDuplicates(Object.keys(this.scheduled), Object.keys(this.triggered)); ids = ids.map((id) => { return Number(id); @@ -304,9 +566,13 @@ export class LocalNotificationsMock extends LocalNotifications { protected getAllNotifications(): Promise { return this.appDB.getAllRecords(this.DESKTOP_NOTIFS_TABLE).then((notifications) => { notifications.forEach((notification) => { - notification.at = new Date(notification.at); + notification.trigger = { + at: new Date(notification.at) + }; notification.data = this.textUtils.parseJSON(notification.data); notification.triggered = !!notification.triggered; + + this.mergeWithDefaults(notification); }); return notifications; @@ -318,7 +584,7 @@ export class LocalNotificationsMock extends LocalNotifications { * * @returns {Promise>} */ - getAllScheduled(): Promise> { + getScheduled(): Promise> { return Promise.resolve(this.getNotifications(undefined, true, false)); } @@ -327,14 +593,14 @@ export class LocalNotificationsMock extends LocalNotifications { * * @returns {Promise>} */ - getAllTriggered(): Promise> { + getTriggered(): Promise> { return Promise.resolve(this.getNotifications(undefined, false, true)); } /** * Get a set of notifications. If ids isn't specified, return all the notifications. * - * @param {Number[]} [ids] Ids of notifications to get. If not specified, get all notifications. + * @param {number[]} [ids] Ids of notifications to get. If not specified, get all notifications. * @param {boolean} [getScheduled] Get scheduled notifications. * @param {boolean} [getTriggered] Get triggered notifications. * @return {ILocalNotification[]} List of notifications. @@ -362,13 +628,19 @@ export class LocalNotificationsMock extends LocalNotifications { } /** - * Get a scheduled notification object. + * Get the trigger "at" in milliseconds. * - * @param {any} notificationId The id of the notification to ge. - * @returns {Promise} + * @param {ILocalNotification} notification Notification to get the trigger from. + * @return {number} Trigger time. */ - getScheduled(notificationId: any): Promise { - return Promise.resolve(this.getNotifications([Number(notificationId)], true, false)[0]); + protected getNotificationTriggerAt(notification: ILocalNotification): number { + const triggerAt = (notification.trigger && notification.trigger.at) || 0; + + if (typeof triggerAt != 'number') { + return triggerAt.getTime(); + } + + return triggerAt; } /** @@ -384,16 +656,6 @@ export class LocalNotificationsMock extends LocalNotifications { return Promise.resolve(ids); } - /** - * Get a triggered notification object. - * - * @param {any} notificationId The id of the notification to get. - * @returns {Promise} - */ - getTriggered(notificationId: any): Promise { - return Promise.resolve(this.getNotifications([Number(notificationId)], false, true)[0]); - } - /** * Get the ids of triggered notifications. * @@ -407,12 +669,28 @@ export class LocalNotificationsMock extends LocalNotifications { return Promise.resolve(ids); } + /** + * Get the type (triggered, scheduled) for the notification. + * + * @param {number} id The ID of the notification. + * @return {Promise} + */ + getType(id: number): Promise { + if (this.scheduled[id]) { + return Promise.resolve('scheduled'); + } else if (this.triggered[id]) { + return Promise.resolve('triggered'); + } else { + return Promise.resolve('unknown'); + } + } + /** * Given an object of options and a list of properties, return the first property that exists. * Code extracted from the Cordova plugin. * * @param {ILocalNotification} notification Notification. - * @param {any} ...args List of keys to check. + * @param {any[]} ...args List of keys to check. * @return {any} First value found. */ protected getValueFor(notification: ILocalNotification, ...args: any[]): any { @@ -424,6 +702,16 @@ export class LocalNotificationsMock extends LocalNotifications { } } + /** + * Checks if a group of actions is defined. + * + * @param {any} groupId The id of the action group + * @returns {Promise} Whether the group is defined. + */ + hasActions(groupId: any): Promise { + return Promise.resolve(false); + } + /** * Informs if the app has the permission to show notifications. * @@ -433,6 +721,19 @@ export class LocalNotificationsMock extends LocalNotifications { return Promise.resolve(true); } + /** + * Check if a notification has a given type. + * + * @param {number} id The ID of the notification. + * @param {string} type The type of the notification. + * @returns {Promise} Promise resolved with boolean: whether it has the type. + */ + hasType(id: number, type: string): Promise { + return this.getType(id).then((notifType) => { + return type == notifType; + }); + } + /** * Checks presence of a notification. * @@ -512,26 +813,22 @@ export class LocalNotificationsMock extends LocalNotifications { * @return {ILocalNotification} Treated notification. */ protected mergeWithDefaults(notification: ILocalNotification): ILocalNotification { - notification.at = this.getValueFor(notification, 'at', 'firstAt', 'date'); - notification.text = this.getValueFor(notification, 'text', 'message'); - notification.data = this.getValueFor(notification, 'data', 'json'); + const values = this.getDefaults(); - if (notification.at === undefined || notification.at === null) { - notification.at = new Date(); + if (values.hasOwnProperty('sticky')) { + notification.sticky = this.getValueFor(notification, 'sticky', 'ongoing'); } - for (const key in this.defaults) { - if (notification[key] === null || notification[key] === undefined) { - if (notification.hasOwnProperty(key) && ['data', 'sound'].indexOf(key) > -1) { - notification[key] = undefined; - } else { - notification[key] = this.defaults[key]; - } - } + if (notification.sticky && notification.autoClear !== true) { + notification.autoClear = false; } - for (const key in notification) { - if (!this.defaults.hasOwnProperty(key)) { + Object.assign(values, notification); + + for (const key in values) { + if (values[key] !== null) { + notification[key] = values[key]; + } else { delete notification[key]; } } @@ -545,7 +842,7 @@ export class LocalNotificationsMock extends LocalNotifications { * @param {ILocalNotification} notification Clicked notification. */ protected notificationClicked(notification: ILocalNotification): void { - this.triggerEvent('click', notification, 'foreground'); + this.fireEvent('click', notification); // Focus the app. require('electron').ipcRenderer.send('focusApp'); } @@ -553,20 +850,16 @@ export class LocalNotificationsMock extends LocalNotifications { /** * Sets a callback for a specific event. * - * @param {string} eventName Name of the event. Events: schedule, trigger, click, update, clear, clearall, cancel, cancelall - * @param {any} callback Call back function. + * @param {string} eventName The name of the event. Events: schedule, trigger, click, update, clear, clearall, cancel, + * cancelall. Custom event names are possible for actions. + * @return {Observable} Observable */ - on(eventName: string, callback: any): void { - if (!this.observers[eventName] || typeof callback != 'function') { - // Event not supported, stop. - return; - } - this.observers[eventName].push(callback); + on(eventName: string): Observable { + return this.observers[eventName]; } /** * Parse a interval and convert it to a number of milliseconds (0 if not valid). - * Code extracted from the Cordova plugin. * * @param {string} every Interval to convert. * @return {number} Number of milliseconds of the interval- @@ -607,12 +900,13 @@ export class LocalNotificationsMock extends LocalNotifications { } /** - * Register permission to show notifications if not already granted. + * Removes a group of actions. * - * @returns {Promise} + * @param {any} groupId The id of the action group + * @returns {Promise} */ - registerPermission(): Promise { - return Promise.resolve(true); + removeActions(groupId: any): Promise { + return Promise.reject('Not supported in desktop apps.'); } /** @@ -625,6 +919,15 @@ export class LocalNotificationsMock extends LocalNotifications { return this.appDB.deleteRecords(this.DESKTOP_NOTIFS_TABLE, { id: id }); } + /** + * Request permission to show notifications if not already granted. + * + * @returns {Promise} + */ + requestPermission(): Promise { + return Promise.resolve(true); + } + /** * Schedules a single or multiple notifications. * @@ -656,13 +959,15 @@ export class LocalNotificationsMock extends LocalNotifications { }; this.storeNotification(notification, false); - if (Math.abs(moment().diff(notification.at * 1000, 'days')) > 15) { + const triggerAt = this.getNotificationTriggerAt(notification); + + if (Math.abs(moment().diff(triggerAt, 'days')) > 15) { // Notification should trigger more than 15 days from now, don't schedule it. return; } // Schedule the notification. - const toTriggerTime = notification.at * 1000 - Date.now(), + const toTriggerTime = triggerAt - Date.now(), trigger = (): void => { // Trigger the notification. this.triggerNotification(notification); @@ -672,10 +977,12 @@ export class LocalNotificationsMock extends LocalNotifications { this.storeNotification(notification, true); // Launch the trigger event. - this.triggerEvent('trigger', notification, 'foreground'); + this.fireEvent('trigger', notification); - if (notification.every && this.scheduled[notification.id] && !this.scheduled[notification.id].interval) { - const interval = this.parseInterval(notification.every); + if (notification.trigger.every && this.scheduled[notification.id] && + !this.scheduled[notification.id].interval) { + + const interval = this.parseInterval(notification.trigger.every); if (interval > 0) { this.scheduled[notification.id].interval = setInterval(trigger, interval); } @@ -685,10 +992,22 @@ export class LocalNotificationsMock extends LocalNotifications { this.scheduled[notification.id].timeout = setTimeout(trigger, toTriggerTime); // Launch the scheduled/update event. - this.triggerEvent(eventName, notification, 'foreground'); + this.fireEvent(eventName, notification); }); } + /** + * Overwrites the (platform specific) default settings. + * + * @param {any} defaults The defaults to set. + * @returns {Promise} + */ + setDefaults(defaults: any): Promise { + this.defaults = defaults; + + return Promise.resolve(); + } + /** * Store a notification in local DB. * @@ -702,7 +1021,7 @@ export class LocalNotificationsMock extends LocalNotifications { id : notification.id, title: notification.title, text: notification.text, - at: notification.at ? (typeof notification.at == 'object' ? notification.at.getTime() : notification.at) : 0, + at: this.getNotificationTriggerAt(notification), data: notification.data ? JSON.stringify(notification.data) : '{}', triggered: triggered ? 1 : 0 }; @@ -710,20 +1029,6 @@ export class LocalNotificationsMock extends LocalNotifications { return this.appDB.insertRecord(this.DESKTOP_NOTIFS_TABLE, entry); } - /** - * Trigger an event. - * - * @param {string} eventName Event name. - * @param {any[]} ...args List of parameters to pass. - */ - protected triggerEvent(eventName: string, ...args: any[]): void { - if (this.observers[eventName]) { - this.observers[eventName].forEach((callback) => { - callback.apply(null, args); - }); - } - } - /** * Trigger a notification, using the best method depending on the OS. * @@ -763,7 +1068,7 @@ export class LocalNotificationsMock extends LocalNotifications { } else { // Use Electron default notifications. const notifInstance = new Notification(notification.title, { - body: notification.text + body: notification.text }); // Listen for click events. @@ -773,23 +1078,6 @@ export class LocalNotificationsMock extends LocalNotifications { } } - /** - * Removes a callback of a specific event. - * - * @param {string} eventName Name of the event. Events: schedule, trigger, click, update, clear, clearall, cancel, cancelall - * @param {any} callback Call back function. - */ - un(eventName: string, callback: any): void { - if (this.observers[eventName] && this.observers[eventName].length) { - for (let i = 0; i < this.observers[eventName].length; i++) { - if (this.observers[eventName][i] == callback) { - this.observers[eventName].splice(i, 1); - break; - } - } - } - } - /** * Updates a previously scheduled notification. Must include the id in the options parameter. *