Vmeda.Online/src/providers/local-notifications.ts

505 lines
18 KiB
TypeScript
Raw Normal View History

// (C) Copyright 2015 Martin Dougiamas
//
// 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.
import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { LocalNotifications, ILocalNotification } from '@ionic-native/local-notifications';
import { CoreAppProvider } from './app';
import { CoreConfigProvider } from './config';
import { CoreLoggerProvider } from './logger';
import { CoreDomUtilsProvider } from './utils/dom';
import { CoreUtilsProvider } from './utils/utils';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreConstants } from '@core/constants';
import { Subject } from 'rxjs';
/**
* Local notification.
*/
export interface CoreILocalNotification extends ILocalNotification {
/**
* Number of milliseconds to turn the led on (Android only).
* @type {number}
*/
ledOnTime?: number;
/**
* Number of milliseconds to turn the led off (Android only).
* @type {number}
*/
ledOffTime?: number;
2018-01-29 09:05:20 +00:00
}
/*
* Generated class for the LocalNotificationsProvider provider.
*
* See https://angular.io/guide/dependency-injection for more info on providers
* and Angular DI.
*
* @todo We might have to translate the old component name to the new one.
* Otherwise the unique ID of local notifications could change.
*/
@Injectable()
export class CoreLocalNotificationsProvider {
// Variables for the database.
protected SITES_TABLE = 'notification_sites'; // Store to asigne unique codes to each site.
protected COMPONENTS_TABLE = 'notification_components'; // Store to asigne unique codes to each component.
protected TRIGGERED_TABLE = 'notifications_triggered'; // Store to prevent re-triggering notifications.
protected tablesSchema = [
{
name: this.SITES_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
},
{
name: 'code',
type: 'INTEGER',
notNull: true
}
]
},
{
name: this.COMPONENTS_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
},
{
name: 'code',
type: 'INTEGER',
notNull: true
}
]
},
{
name: this.TRIGGERED_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true
},
{
name: 'at',
type: 'INTEGER',
notNull: true
}
]
}
];
protected logger;
protected appDB: SQLiteDB;
2018-01-29 09:05:20 +00:00
protected codes: { [s: string]: number } = {};
protected codeRequestsQueue = {};
protected observables = {};
constructor(logger: CoreLoggerProvider, private localNotifications: LocalNotifications, private platform: Platform,
private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private configProvider: CoreConfigProvider,
private domUtils: CoreDomUtilsProvider) {
this.logger = logger.getInstance('CoreLocalNotificationsProvider');
this.appDB = appProvider.getDB();
this.appDB.createTablesFromSchema(this.tablesSchema);
}
/**
* Cancel a local notification.
*
* @param {number} id Notification id.
* @param {string} component Component of the notification.
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when the notification is cancelled.
*/
2018-01-29 09:05:20 +00:00
cancel(id: number, component: string, siteId: string): Promise<any> {
return this.getUniqueNotificationId(id, component, siteId).then((uniqueId) => {
return this.localNotifications.cancel(uniqueId);
});
}
/**
* Cancel all the scheduled notifications belonging to a certain site.
*
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when the notifications are cancelled.
*/
2018-01-29 09:05:20 +00:00
cancelSiteNotifications(siteId: string): Promise<any> {
if (!this.isAvailable()) {
return Promise.resolve();
} else if (!siteId) {
return Promise.reject(null);
}
return this.localNotifications.getAllScheduled().then((scheduled) => {
2018-01-29 09:05:20 +00:00
const ids = [];
scheduled.forEach((notif) => {
if (typeof notif.data == 'string') {
notif.data = JSON.parse(notif.data);
}
if (typeof notif.data == 'object' && notif.data.siteId === siteId) {
ids.push(notif.id);
}
});
return this.localNotifications.cancel(ids);
});
}
/**
* Get a code to create unique notifications. If there's no code assigned, create a new one.
*
* @param {string} table Table to search in local DB.
* @param {string} id ID of the element to get its code.
* @return {Promise<number>} Promise resolved when the code is retrieved.
*/
2018-01-29 09:05:20 +00:00
protected getCode(table: string, id: string): Promise<number> {
const key = table + '#' + id;
// Check if the code is already in memory.
if (typeof this.codes[key] != 'undefined') {
return Promise.resolve(this.codes[key]);
}
// Check if we already have a code stored for that ID.
2018-01-29 09:05:20 +00:00
return this.appDB.getRecord(table, { id: id }).then((entry) => {
this.codes[key] = entry.code;
2018-01-29 09:05:20 +00:00
return entry.code;
}).catch(() => {
// No code stored for that ID. Create a new code for it.
return this.appDB.getRecords(table, undefined, 'code DESC').then((entries) => {
let newCode = 0;
if (entries.length > 0) {
newCode = entries[0].code + 1;
}
2018-01-29 09:05:20 +00:00
return this.appDB.insertRecord(table, { id: id, code: newCode }).then(() => {
this.codes[key] = newCode;
2018-01-29 09:05:20 +00:00
return newCode;
});
});
});
}
/**
* Get a notification component code to be used.
* If it's the first time this component is used to send notifications, create a new code for it.
*
* @param {string} component Component name.
* @return {Promise<number>} Promise resolved when the component code is retrieved.
*/
2018-01-29 09:05:20 +00:00
protected getComponentCode(component: string): Promise<number> {
return this.requestCode(this.COMPONENTS_TABLE, component);
}
/**
* Get a site code to be used.
* If it's the first time this site is used to send notifications, create a new code for it.
*
* @param {string} siteId Site ID.
* @return {Promise<number>} Promise resolved when the site code is retrieved.
*/
2018-01-29 09:05:20 +00:00
protected getSiteCode(siteId: string): Promise<number> {
return this.requestCode(this.SITES_TABLE, siteId);
}
/**
* Create a unique notification ID, trying to prevent collisions. Generated ID must be a Number (Android).
* The generated ID shouldn't be higher than 2147483647 or it's going to cause problems in Android.
* This function will prevent collisions and keep the number under Android limit if:
* -User has used less than 21 sites.
* -There are less than 11 components.
* -The notificationId passed as parameter is lower than 10000000.
*
* @param {number} notificationId Notification ID.
* @param {string} component Component triggering the notification.
* @param {string} siteId Site ID.
* @return {Promise<number>} Promise resolved when the notification ID is generated.
*/
2018-01-29 09:05:20 +00:00
protected getUniqueNotificationId(notificationId: number, component: string, siteId: string): Promise<number> {
if (!siteId || !component) {
return Promise.reject(null);
}
return this.getSiteCode(siteId).then((siteCode) => {
return this.getComponentCode(component).then((componentCode) => {
// We use the % operation to keep the number under Android's limit.
return (siteCode * 100000000 + componentCode * 10000000 + notificationId) % 2147483647;
});
});
}
/**
* Returns whether local notifications plugin is installed.
*
* @return {boolean} Whether local notifications plugin is installed.
*/
2018-01-29 09:05:20 +00:00
isAvailable(): boolean {
const win = <any> window;
return this.appProvider.isDesktop() || !!(win.plugin && win.plugin.notification && win.plugin.notification.local);
}
/**
* Check if a notification has been triggered with the same trigger time.
*
* @param {CoreILocalNotification} notification Notification to check.
2018-01-29 09:05:20 +00:00
* @return {Promise<any>} Promise resolved with a boolean indicating if promise is triggered (true) or not.
*/
2018-01-29 09:05:20 +00:00
isTriggered(notification: CoreILocalNotification): Promise<any> {
return this.appDB.getRecord(this.TRIGGERED_TABLE, { id: notification.id }).then((stored) => {
return stored.at === notification.at.getTime() / 1000;
}).catch(() => {
return false;
});
}
/**
* Notify notification click to observers. Only the observers with the same component as the notification will be notified.
*
* @param {any} data Data received by the notification.
*/
2018-01-29 09:05:20 +00:00
notifyClick(data: any): void {
const component = data.component;
if (component) {
if (this.observables[component]) {
this.observables[component].next(data);
}
}
}
/**
* Process the next request in queue.
*/
2018-01-29 09:05:20 +00:00
protected processNextRequest(): void {
const nextKey = Object.keys(this.codeRequestsQueue)[0];
let request,
promise;
if (typeof nextKey == 'undefined') {
// No more requests in queue, stop.
return;
}
request = this.codeRequestsQueue[nextKey];
// Check if request is valid.
if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') {
// Get the code and resolve/reject all the promises of this request.
promise = this.getCode(request.table, request.id).then((code) => {
request.promises.forEach((p) => {
p.resolve(code);
});
}).catch((error) => {
request.promises.forEach((p) => {
p.reject(error);
});
});
} else {
promise = Promise.resolve();
}
// Once this item is treated, remove it and process next.
promise.finally(() => {
delete this.codeRequestsQueue[nextKey];
this.processNextRequest();
});
}
/**
* Register an observer to be notified when a notification belonging to a certain component is clicked.
*
* @param {string} component Component to listen notifications for.
* @param {Function} callback Function to call with the data received by the notification.
* @return {any} Object with an "off" property to stop listening for clicks.
*/
2018-01-29 09:05:20 +00:00
registerClick(component: string, callback: Function): any {
this.logger.debug(`Register observer '${component}' for notification click.`);
if (typeof this.observables[component] == 'undefined') {
// No observable for this component, create a new one.
this.observables[component] = new Subject<any>();
}
this.observables[component].subscribe(callback);
return {
2018-01-29 09:05:20 +00:00
off: (): void => {
this.observables[component].unsubscribe(callback);
}
};
}
/**
* Remove a notification from triggered store.
*
* @param {number} id Notification ID.
* @return {Promise<any>} Promise resolved when it is removed.
*/
2018-01-29 09:05:20 +00:00
removeTriggered(id: number): Promise<any> {
return this.appDB.deleteRecords(this.TRIGGERED_TABLE, { id: id });
}
/**
* Request a unique code. The request will be added to the queue and the queue is going to be started if it's paused.
*
* @param {string} table Table to search in local DB.
* @param {string} id ID of the element to get its code.
* @return {Promise} Promise resolved when the code is retrieved.
*/
2018-01-29 09:05:20 +00:00
protected requestCode(table: string, id: string): Promise<number> {
const deferred = this.utils.promiseDefer(),
key = table + '#' + id,
isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0;
if (typeof this.codeRequestsQueue[key] != 'undefined') {
// There's already a pending request for this store and ID, add the promise to it.
this.codeRequestsQueue[key].promises.push(deferred);
} else {
// Add a pending request to the queue.
this.codeRequestsQueue[key] = {
table: table,
id: id,
promises: [deferred]
2018-01-29 09:05:20 +00:00
};
}
if (isQueueEmpty) {
this.processNextRequest();
}
return deferred.promise;
}
/**
* Reschedule all notifications that are already scheduled.
*
* @return {Promise<any>} Promise resolved when all notifications have been rescheduled.
*/
2018-01-29 09:05:20 +00:00
rescheduleAll(): Promise<any> {
// Get all the scheduled notifications.
return this.localNotifications.getAllScheduled().then((notifications) => {
2018-01-29 09:05:20 +00:00
const promises = [];
notifications.forEach((notification) => {
// Convert some properties to the needed types.
notification.at = new Date(notification.at * 1000);
notification.data = notification.data ? JSON.parse(notification.data) : {};
promises.push(this.scheduleNotification(notification));
});
return Promise.all(promises);
});
}
/**
* Schedule a local notification.
*
* @param {CoreILocalNotification} notification Notification to schedule. Its ID should be lower than 10000000 and it should
* be unique inside its component and site.
* @param {string} component Component triggering the notification. It is used to generate unique IDs.
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when the notification is scheduled.
*/
2018-01-29 09:05:20 +00:00
schedule(notification: CoreILocalNotification, component: string, siteId: string): Promise<any> {
return this.getUniqueNotificationId(notification.id, component, siteId).then((uniqueId) => {
notification.id = uniqueId;
notification.data = notification.data || {};
notification.data.component = component;
notification.data.siteId = siteId;
if (this.platform.is('android')) {
notification.icon = notification.icon || 'res://icon';
notification.smallIcon = notification.smallIcon || 'res://icon';
notification.led = notification.led || 'FF9900';
notification.ledOnTime = notification.ledOnTime || 1000;
notification.ledOffTime = notification.ledOffTime || 1000;
}
return this.scheduleNotification(notification);
});
}
/**
* Helper function to schedule a notification object if it hasn't been triggered already.
*
* @param {CoreILocalNotification} notification Notification to schedule.
* @return {Promise<any>} Promise resolved when scheduled.
*/
2018-01-29 09:05:20 +00:00
protected scheduleNotification(notification: CoreILocalNotification): Promise<any> {
// Check if the notification has been triggered already.
return this.isTriggered(notification).then((triggered) => {
if (!triggered) {
// Check if sound is enabled for notifications.
return this.configProvider.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true).then((soundEnabled) => {
if (!soundEnabled) {
notification.sound = null;
} else {
delete notification.sound; // Use default value.
}
// Remove from triggered, since the notification could be in there with a different time.
this.removeTriggered(notification.id);
this.localNotifications.schedule(notification);
});
}
});
}
/**
* Show an in app notification popover.
*
* @param {CoreILocalNotification} notification Notification.
*/
2018-01-29 09:05:20 +00:00
showNotificationPopover(notification: CoreILocalNotification): void {
// @todo Improve it. For now, show Toast.
2018-01-29 09:05:20 +00:00
if (!notification || !notification.title || !notification.text) {
// Invalid data.
return;
}
this.domUtils.showToast(notification.text, false, 4000);
}
/**
* Function to call when a notification is triggered. Stores the notification so it's not scheduled again unless the
* time is changed.
*
* @param {CoreILocalNotification} notification Triggered notification.
* @return {Promise<any>} Promise resolved when stored, rejected otherwise.
*/
2018-01-29 09:05:20 +00:00
trigger(notification: CoreILocalNotification): Promise<any> {
if (this.platform.is('ios') && this.platform.version().num >= 10) {
// In iOS10 show in app notification.
this.showNotificationPopover(notification);
}
2018-01-29 09:05:20 +00:00
const entry = {
id: notification.id,
at: parseInt(notification.at, 10)
};
2018-01-29 09:05:20 +00:00
return this.appDB.insertOrUpdateRecord(this.TRIGGERED_TABLE, entry, { id: notification.id });
}
}