2020-10-07 08:53:19 +00:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// 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 { Subject, Subscription } from 'rxjs';
|
|
|
|
import { ILocalNotification } from '@ionic-native/local-notifications';
|
|
|
|
|
2020-10-28 13:25:18 +00:00
|
|
|
import { CoreApp } from '@services/app';
|
2020-10-07 08:53:19 +00:00
|
|
|
import { CoreConfig } from '@services/config';
|
2020-10-22 10:48:23 +00:00
|
|
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
2020-10-07 08:53:19 +00:00
|
|
|
import { CoreTextUtils } from '@services/utils/text';
|
2020-10-28 13:25:18 +00:00
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
2020-10-07 08:53:19 +00:00
|
|
|
import { SQLiteDB } from '@classes/sqlitedb';
|
2020-10-14 14:38:24 +00:00
|
|
|
import { CoreSite } from '@classes/site';
|
2020-10-07 08:53:19 +00:00
|
|
|
import { CoreQueueRunner } from '@classes/queue-runner';
|
2020-10-14 14:38:24 +00:00
|
|
|
import { CoreError } from '@classes/errors/error';
|
2020-11-19 11:40:18 +00:00
|
|
|
import { CoreConstants } from '@/core/constants';
|
2020-11-06 14:32:00 +00:00
|
|
|
import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push } from '@singletons/core.singletons';
|
2020-10-07 08:53:19 +00:00
|
|
|
import { CoreLogger } from '@singletons/logger';
|
2020-10-28 13:25:18 +00:00
|
|
|
import {
|
|
|
|
APP_SCHEMA,
|
|
|
|
TRIGGERED_TABLE_NAME,
|
|
|
|
COMPONENTS_TABLE_NAME,
|
|
|
|
SITES_TABLE_NAME,
|
|
|
|
CodeRequestsQueueItem,
|
|
|
|
} from '@services/local-notifications.db';
|
2020-10-07 08:53:19 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Service to handle local notifications.
|
|
|
|
*/
|
2020-11-19 15:35:17 +00:00
|
|
|
@Injectable({ providedIn: 'root' })
|
2020-10-07 08:53:19 +00:00
|
|
|
export class CoreLocalNotificationsProvider {
|
2020-10-08 11:36:16 +00:00
|
|
|
|
2020-10-07 08:53:19 +00:00
|
|
|
protected logger: CoreLogger;
|
|
|
|
protected appDB: SQLiteDB;
|
2020-10-08 11:36:16 +00:00
|
|
|
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
|
2020-10-07 08:53:19 +00:00
|
|
|
protected codes: { [s: string]: number } = {};
|
2020-10-14 14:38:24 +00:00
|
|
|
protected codeRequestsQueue: {[key: string]: CodeRequestsQueueItem} = {};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
protected observables: {[eventName: string]: {[component: string]: Subject<any>}} = {};
|
2020-10-08 11:36:16 +00:00
|
|
|
|
2020-10-20 12:20:51 +00:00
|
|
|
protected triggerSubscription?: Subscription;
|
|
|
|
protected clickSubscription?: Subscription;
|
|
|
|
protected clearSubscription?: Subscription;
|
|
|
|
protected cancelSubscription?: Subscription;
|
|
|
|
protected addSubscription?: Subscription;
|
|
|
|
protected updateSubscription?: Subscription;
|
2020-10-21 14:32:27 +00:00
|
|
|
protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477).
|
2020-10-07 08:53:19 +00:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider');
|
|
|
|
this.queueRunner = new CoreQueueRunner(10);
|
|
|
|
this.appDB = CoreApp.instance.getDB();
|
2020-10-28 13:25:18 +00:00
|
|
|
this.dbReady = CoreApp.instance.createTablesFromSchema(APP_SCHEMA).catch(() => {
|
2020-10-07 08:53:19 +00:00
|
|
|
// Ignore errors.
|
|
|
|
});
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.init();
|
|
|
|
}
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
/**
|
|
|
|
* Init some properties.
|
|
|
|
*/
|
|
|
|
protected async init(): Promise<void> {
|
|
|
|
await Platform.instance.ready();
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
// Listen to events.
|
|
|
|
this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => {
|
|
|
|
this.trigger(notification);
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.handleEvent('trigger', notification);
|
|
|
|
});
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.clickSubscription = LocalNotifications.instance.on('click').subscribe((notification: ILocalNotification) => {
|
|
|
|
this.handleEvent('click', notification);
|
|
|
|
});
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.clearSubscription = LocalNotifications.instance.on('clear').subscribe((notification: ILocalNotification) => {
|
|
|
|
this.handleEvent('clear', notification);
|
|
|
|
});
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.cancelSubscription = LocalNotifications.instance.on('cancel').subscribe((notification: ILocalNotification) => {
|
|
|
|
this.handleEvent('cancel', notification);
|
|
|
|
});
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.addSubscription = LocalNotifications.instance.on('schedule').subscribe((notification: ILocalNotification) => {
|
|
|
|
this.handleEvent('schedule', notification);
|
|
|
|
});
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
this.updateSubscription = LocalNotifications.instance.on('update').subscribe((notification: ILocalNotification) => {
|
|
|
|
this.handleEvent('update', notification);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create the default channel for local notifications.
|
|
|
|
this.createDefaultChannel();
|
|
|
|
|
|
|
|
Translate.instance.onLangChange.subscribe(() => {
|
|
|
|
// Update the channel name.
|
|
|
|
this.createDefaultChannel();
|
2020-10-07 08:53:19 +00:00
|
|
|
});
|
|
|
|
|
2020-10-22 10:48:23 +00:00
|
|
|
CoreEvents.on(CoreEvents.SITE_DELETED, (site: CoreSite) => {
|
2020-10-07 08:53:19 +00:00
|
|
|
if (site) {
|
2020-10-21 14:32:27 +00:00
|
|
|
this.cancelSiteNotifications(site.id!);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cancel a local notification.
|
|
|
|
*
|
|
|
|
* @param id Notification id.
|
|
|
|
* @param component Component of the notification.
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @return Promise resolved when the notification is cancelled.
|
|
|
|
*/
|
|
|
|
async cancel(id: number, component: string, siteId: string): Promise<void> {
|
|
|
|
const uniqueId = await this.getUniqueNotificationId(id, component, siteId);
|
|
|
|
|
|
|
|
const queueId = 'cancel-' + uniqueId;
|
|
|
|
|
|
|
|
await this.queueRunner.run(queueId, () => LocalNotifications.instance.cancel(uniqueId), {
|
|
|
|
allowRepeated: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cancel all the scheduled notifications belonging to a certain site.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @return Promise resolved when the notifications are cancelled.
|
|
|
|
*/
|
|
|
|
async cancelSiteNotifications(siteId: string): Promise<void> {
|
|
|
|
if (!this.isAvailable()) {
|
|
|
|
return;
|
|
|
|
} else if (!siteId) {
|
|
|
|
throw new Error('No site ID supplied.');
|
|
|
|
}
|
|
|
|
|
|
|
|
const scheduled = await this.getAllScheduled();
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
const ids: number[] = [];
|
2020-10-07 08:53:19 +00:00
|
|
|
const queueId = 'cancelSiteNotifications-' + siteId;
|
|
|
|
|
|
|
|
scheduled.forEach((notif) => {
|
|
|
|
notif.data = this.parseNotificationData(notif.data);
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
if (notif.id && typeof notif.data == 'object' && notif.data.siteId === siteId) {
|
2020-10-07 08:53:19 +00:00
|
|
|
ids.push(notif.id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await this.queueRunner.run(queueId, () => LocalNotifications.instance.cancel(ids), {
|
|
|
|
allowRepeated: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether sound can be disabled for notifications.
|
|
|
|
*
|
|
|
|
* @return Whether sound can be disabled for notifications.
|
|
|
|
*/
|
|
|
|
canDisableSound(): boolean {
|
|
|
|
// Only allow disabling sound in Android 7 or lower. In iOS and Android 8+ it can easily be done with system settings.
|
2020-11-06 14:32:00 +00:00
|
|
|
return this.isAvailable() && CoreApp.instance.isAndroid() && CoreApp.instance.getPlatformMajorVersion() < 8;
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the default channel. It is used to change the name.
|
|
|
|
*
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
protected async createDefaultChannel(): Promise<void> {
|
2020-10-07 08:53:19 +00:00
|
|
|
if (!CoreApp.instance.isAndroid()) {
|
2020-10-08 11:36:16 +00:00
|
|
|
return;
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
2020-10-08 11:36:16 +00:00
|
|
|
await Push.instance.createChannel({
|
2020-10-07 08:53:19 +00:00
|
|
|
id: 'default-channel-id',
|
|
|
|
description: Translate.instance.instant('addon.calendar.calendarreminders'),
|
2020-10-08 11:36:16 +00:00
|
|
|
importance: 4,
|
2020-10-07 08:53:19 +00:00
|
|
|
}).catch((error) => {
|
|
|
|
this.logger.error('Error changing channel name', error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all scheduled notifications.
|
|
|
|
*
|
|
|
|
* @return Promise resolved with the notifications.
|
|
|
|
*/
|
|
|
|
protected getAllScheduled(): Promise<ILocalNotification[]> {
|
|
|
|
return this.queueRunner.run('allScheduled', () => LocalNotifications.instance.getAllScheduled());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a code to create unique notifications. If there's no code assigned, create a new one.
|
|
|
|
*
|
|
|
|
* @param table Table to search in local DB.
|
|
|
|
* @param id ID of the element to get its code.
|
|
|
|
* @return Promise resolved when the code is retrieved.
|
|
|
|
*/
|
|
|
|
protected async getCode(table: string, id: string): Promise<number> {
|
|
|
|
await this.dbReady;
|
|
|
|
|
|
|
|
const key = table + '#' + id;
|
|
|
|
|
|
|
|
// Check if the code is already in memory.
|
|
|
|
if (typeof this.codes[key] != 'undefined') {
|
|
|
|
return this.codes[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Check if we already have a code stored for that ID.
|
2020-10-14 14:38:24 +00:00
|
|
|
const entry = await this.appDB.getRecord<{id: string; code: number}>(table, { id: id });
|
|
|
|
|
2020-10-07 08:53:19 +00:00
|
|
|
this.codes[key] = entry.code;
|
|
|
|
|
|
|
|
return entry.code;
|
|
|
|
} catch (err) {
|
|
|
|
// No code stored for that ID. Create a new code for it.
|
2020-10-14 14:38:24 +00:00
|
|
|
const entries = await this.appDB.getRecords<{id: string; code: number}>(table, undefined, 'code DESC');
|
|
|
|
|
2020-10-07 08:53:19 +00:00
|
|
|
let newCode = 0;
|
|
|
|
if (entries.length > 0) {
|
|
|
|
newCode = entries[0].code + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.appDB.insertRecord(table, { id: id, code: newCode });
|
|
|
|
this.codes[key] = newCode;
|
|
|
|
|
|
|
|
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 component Component name.
|
|
|
|
* @return Promise resolved when the component code is retrieved.
|
|
|
|
*/
|
|
|
|
protected getComponentCode(component: string): Promise<number> {
|
2020-10-28 13:25:18 +00:00
|
|
|
return this.requestCode(COMPONENTS_TABLE_NAME, component);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 siteId Site ID.
|
|
|
|
* @return Promise resolved when the site code is retrieved.
|
|
|
|
*/
|
|
|
|
protected getSiteCode(siteId: string): Promise<number> {
|
2020-10-28 13:25:18 +00:00
|
|
|
return this.requestCode(SITES_TABLE_NAME, siteId);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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:
|
2020-10-08 11:36:16 +00:00
|
|
|
* - User has used less than 21 sites.
|
|
|
|
* - There are less than 11 components.
|
|
|
|
* - The notificationId passed as parameter is lower than 10000000.
|
2020-10-07 08:53:19 +00:00
|
|
|
*
|
|
|
|
* @param notificationId Notification ID.
|
|
|
|
* @param component Component triggering the notification.
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @return Promise resolved when the notification ID is generated.
|
|
|
|
*/
|
2020-10-20 12:20:51 +00:00
|
|
|
protected async getUniqueNotificationId(notificationId: number, component: string, siteId: string): Promise<number> {
|
2020-10-07 08:53:19 +00:00
|
|
|
if (!siteId || !component) {
|
2020-10-14 14:38:24 +00:00
|
|
|
return Promise.reject(new CoreError('Site ID or component not supplied.'));
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
2020-10-20 12:20:51 +00:00
|
|
|
const siteCode = await this.getSiteCode(siteId);
|
|
|
|
const componentCode = await this.getComponentCode(component);
|
|
|
|
|
|
|
|
// We use the % operation to keep the number under Android's limit.
|
|
|
|
return (siteCode * 100000000 + componentCode * 10000000 + notificationId) % 2147483647;
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle an event triggered by the local notifications plugin.
|
|
|
|
*
|
|
|
|
* @param eventName Name of the event.
|
|
|
|
* @param notification Notification.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
protected handleEvent(eventName: string, notification: ILocalNotification): void {
|
2020-10-07 08:53:19 +00:00
|
|
|
if (notification && notification.data) {
|
|
|
|
this.logger.debug('Notification event: ' + eventName + '. Data:', notification.data);
|
|
|
|
|
|
|
|
this.notifyEvent(eventName, notification.data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether local notifications plugin is installed.
|
|
|
|
*
|
|
|
|
* @return Whether local notifications plugin is installed.
|
|
|
|
*/
|
|
|
|
isAvailable(): boolean {
|
2020-10-21 14:32:27 +00:00
|
|
|
const win = <any> window; // eslint-disable-line @typescript-eslint/no-explicit-any
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-29 11:39:15 +00:00
|
|
|
return !!win.cordova?.plugins?.notification?.local;
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a notification has been triggered with the same trigger time.
|
|
|
|
*
|
|
|
|
* @param notification Notification to check.
|
|
|
|
* @param useQueue Whether to add the call to the queue.
|
|
|
|
* @return Promise resolved with a boolean indicating if promise is triggered (true) or not.
|
|
|
|
*/
|
|
|
|
async isTriggered(notification: ILocalNotification, useQueue: boolean = true): Promise<boolean> {
|
|
|
|
await this.dbReady;
|
|
|
|
|
|
|
|
try {
|
2020-10-20 12:20:51 +00:00
|
|
|
const stored = await this.appDB.getRecord<{ id: number; at: number }>(
|
2020-10-28 13:25:18 +00:00
|
|
|
TRIGGERED_TABLE_NAME,
|
2020-10-20 12:20:51 +00:00
|
|
|
{ id: notification.id },
|
|
|
|
);
|
2020-10-14 14:38:24 +00:00
|
|
|
|
2020-10-07 08:53:19 +00:00
|
|
|
let triggered = (notification.trigger && notification.trigger.at) || 0;
|
|
|
|
|
|
|
|
if (typeof triggered != 'number') {
|
|
|
|
triggered = triggered.getTime();
|
|
|
|
}
|
|
|
|
|
|
|
|
return stored.at === triggered;
|
|
|
|
} catch (err) {
|
|
|
|
if (useQueue) {
|
|
|
|
const queueId = 'isTriggered-' + notification.id;
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
return this.queueRunner.run(queueId, () => LocalNotifications.instance.isTriggered(notification.id!), {
|
2020-10-07 08:53:19 +00:00
|
|
|
allowRepeated: true,
|
|
|
|
});
|
|
|
|
} else {
|
2020-10-21 14:32:27 +00:00
|
|
|
return LocalNotifications.instance.isTriggered(notification.id || 0);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify notification click to observers. Only the observers with the same component as the notification will be notified.
|
|
|
|
*
|
|
|
|
* @param data Data received by the notification.
|
|
|
|
*/
|
2020-10-20 12:20:51 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-07 08:53:19 +00:00
|
|
|
notifyClick(data: any): void {
|
|
|
|
this.notifyEvent('click', data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify a certain event to observers. Only the observers with the same component as the notification will be notified.
|
|
|
|
*
|
|
|
|
* @param eventName Name of the event to notify.
|
|
|
|
* @param data Data received by the notification.
|
|
|
|
*/
|
2020-10-20 12:20:51 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-07 08:53:19 +00:00
|
|
|
notifyEvent(eventName: string, data: any): void {
|
|
|
|
// Execute the code in the Angular zone, so change detection doesn't stop working.
|
|
|
|
NgZone.instance.run(() => {
|
|
|
|
const component = data.component;
|
|
|
|
if (component) {
|
|
|
|
if (this.observables[eventName] && this.observables[eventName][component]) {
|
|
|
|
this.observables[eventName][component].next(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse some notification data.
|
|
|
|
*
|
|
|
|
* @param data Notification data.
|
|
|
|
* @return Parsed data.
|
|
|
|
*/
|
2020-10-20 12:20:51 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-07 08:53:19 +00:00
|
|
|
protected parseNotificationData(data: any): any {
|
|
|
|
if (!data) {
|
|
|
|
return {};
|
|
|
|
} else if (typeof data == 'string') {
|
|
|
|
return CoreTextUtils.instance.parseJSON(data, {});
|
|
|
|
} else {
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process the next request in queue.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
protected async processNextRequest(): Promise<void> {
|
2020-10-07 08:53:19 +00:00
|
|
|
const nextKey = Object.keys(this.codeRequestsQueue)[0];
|
|
|
|
|
|
|
|
if (typeof nextKey == 'undefined') {
|
|
|
|
// No more requests in queue, stop.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-08 11:36:16 +00:00
|
|
|
const request = this.codeRequestsQueue[nextKey];
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
try {
|
|
|
|
// Check if request is valid.
|
|
|
|
if (typeof request != 'object' || request.table === undefined || request.id === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:53:19 +00:00
|
|
|
// Get the code and resolve/reject all the promises of this request.
|
2020-10-21 14:32:27 +00:00
|
|
|
const code = await this.getCode(request.table, request.id);
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
request.deferreds.forEach((p) => {
|
|
|
|
p.resolve(code);
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
request.deferreds.forEach((p) => {
|
|
|
|
p.reject(error);
|
|
|
|
});
|
|
|
|
} finally {
|
|
|
|
// Once this item is treated, remove it and process next.
|
2020-10-07 08:53:19 +00:00
|
|
|
delete this.codeRequestsQueue[nextKey];
|
|
|
|
this.processNextRequest();
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register an observer to be notified when a notification belonging to a certain component is clicked.
|
|
|
|
*
|
|
|
|
* @param component Component to listen notifications for.
|
|
|
|
* @param callback Function to call with the data received by the notification.
|
|
|
|
* @return Object with an "off" property to stop listening for clicks.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
registerClick(component: string, callback: CoreLocalNotificationsClickCallback): CoreEventObserver {
|
2020-10-07 08:53:19 +00:00
|
|
|
return this.registerObserver('click', component, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register an observer to be notified when a certain event is fired for a notification belonging to a certain component.
|
|
|
|
*
|
|
|
|
* @param eventName Name of the event to listen to.
|
|
|
|
* @param component Component to listen notifications for.
|
|
|
|
* @param callback Function to call with the data received by the notification.
|
|
|
|
* @return Object with an "off" property to stop listening for events.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
registerObserver<T>(eventName: string, component: string, callback: CoreLocalNotificationsClickCallback): CoreEventObserver {
|
2020-10-07 08:53:19 +00:00
|
|
|
this.logger.debug(`Register observer '${component}' for event '${eventName}'.`);
|
|
|
|
|
|
|
|
if (typeof this.observables[eventName] == 'undefined') {
|
|
|
|
this.observables[eventName] = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof this.observables[eventName][component] == 'undefined') {
|
|
|
|
// No observable for this component, create a new one.
|
2020-10-08 11:36:16 +00:00
|
|
|
this.observables[eventName][component] = new Subject<T>();
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.observables[eventName][component].subscribe(callback);
|
|
|
|
|
|
|
|
return {
|
|
|
|
off: (): void => {
|
2020-10-14 14:38:24 +00:00
|
|
|
this.observables[eventName][component].unsubscribe();
|
2020-10-08 11:36:16 +00:00
|
|
|
},
|
2020-10-07 08:53:19 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a notification from triggered store.
|
|
|
|
*
|
|
|
|
* @param id Notification ID.
|
|
|
|
* @return Promise resolved when it is removed.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
async removeTriggered(id: number): Promise<void> {
|
2020-10-07 08:53:19 +00:00
|
|
|
await this.dbReady;
|
|
|
|
|
2020-10-28 13:25:18 +00:00
|
|
|
await this.appDB.deleteRecords(TRIGGERED_TABLE_NAME, { id: id });
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 table Table to search in local DB.
|
|
|
|
* @param id ID of the element to get its code.
|
|
|
|
* @return Promise resolved when the code is retrieved.
|
|
|
|
*/
|
|
|
|
protected requestCode(table: string, id: string): Promise<number> {
|
2020-10-08 11:36:16 +00:00
|
|
|
const deferred = CoreUtils.instance.promiseDefer<number>();
|
2020-10-20 12:20:51 +00:00
|
|
|
const key = table + '#' + id;
|
|
|
|
const isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0;
|
2020-10-07 08:53:19 +00:00
|
|
|
|
|
|
|
if (typeof this.codeRequestsQueue[key] != 'undefined') {
|
|
|
|
// There's already a pending request for this store and ID, add the promise to it.
|
2020-10-14 14:38:24 +00:00
|
|
|
this.codeRequestsQueue[key].deferreds.push(deferred);
|
2020-10-07 08:53:19 +00:00
|
|
|
} else {
|
|
|
|
// Add a pending request to the queue.
|
|
|
|
this.codeRequestsQueue[key] = {
|
|
|
|
table: table,
|
|
|
|
id: id,
|
2020-10-14 14:38:24 +00:00
|
|
|
deferreds: [deferred],
|
2020-10-07 08:53:19 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isQueueEmpty) {
|
|
|
|
this.processNextRequest();
|
|
|
|
}
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reschedule all notifications that are already scheduled.
|
|
|
|
*
|
|
|
|
* @return Promise resolved when all notifications have been rescheduled.
|
|
|
|
*/
|
|
|
|
async rescheduleAll(): Promise<void> {
|
|
|
|
// Get all the scheduled notifications.
|
|
|
|
const notifications = await this.getAllScheduled();
|
|
|
|
|
|
|
|
await Promise.all(notifications.map(async (notification) => {
|
|
|
|
// Convert some properties to the needed types.
|
|
|
|
notification.data = this.parseNotificationData(notification.data);
|
|
|
|
|
|
|
|
const queueId = 'schedule-' + notification.id;
|
|
|
|
|
|
|
|
await this.queueRunner.run(queueId, () => this.scheduleNotification(notification), {
|
|
|
|
allowRepeated: true,
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedule a local notification.
|
|
|
|
*
|
|
|
|
* @param notification Notification to schedule. Its ID should be lower than 10000000 and it should
|
|
|
|
* be unique inside its component and site.
|
|
|
|
* @param component Component triggering the notification. It is used to generate unique IDs.
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param alreadyUnique Whether the ID is already unique.
|
|
|
|
* @return Promise resolved when the notification is scheduled.
|
|
|
|
*/
|
|
|
|
async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise<void> {
|
|
|
|
if (!alreadyUnique) {
|
2020-10-21 14:32:27 +00:00
|
|
|
notification.id = await this.getUniqueNotificationId(notification.id || 0, component, siteId);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
notification.data = notification.data || {};
|
|
|
|
notification.data.component = component;
|
|
|
|
notification.data.siteId = siteId;
|
|
|
|
|
|
|
|
if (CoreApp.instance.isAndroid()) {
|
|
|
|
notification.icon = notification.icon || 'res://icon';
|
|
|
|
notification.smallIcon = notification.smallIcon || 'res://smallicon';
|
2020-10-21 15:56:01 +00:00
|
|
|
notification.color = notification.color || CoreConstants.CONFIG.notificoncolor;
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-08 11:36:16 +00:00
|
|
|
if (notification.led !== false) {
|
|
|
|
let ledColor = 'FF9900';
|
|
|
|
let ledOn = 1000;
|
|
|
|
let ledOff = 1000;
|
|
|
|
|
|
|
|
if (typeof notification.led === 'string') {
|
|
|
|
ledColor = notification.led;
|
|
|
|
} else if (Array.isArray(notification.led)) {
|
|
|
|
ledColor = notification.led[0] || ledColor;
|
|
|
|
ledOn = notification.led[1] || ledOn;
|
|
|
|
ledOff = notification.led[2] || ledOff;
|
|
|
|
} else if (typeof notification.led === 'object') {
|
|
|
|
ledColor = notification.led.color || ledColor;
|
|
|
|
ledOn = notification.led.on || ledOn;
|
|
|
|
ledOff = notification.led.off || ledOff;
|
|
|
|
}
|
|
|
|
|
|
|
|
notification.led = {
|
|
|
|
color: ledColor,
|
|
|
|
on: ledOn,
|
|
|
|
off: ledOff,
|
|
|
|
};
|
|
|
|
}
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const queueId = 'schedule-' + notification.id;
|
|
|
|
|
|
|
|
await this.queueRunner.run(queueId, () => this.scheduleNotification(notification), {
|
|
|
|
allowRepeated: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to schedule a notification object if it hasn't been triggered already.
|
|
|
|
*
|
|
|
|
* @param notification Notification to schedule.
|
|
|
|
* @return Promise resolved when scheduled.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
protected async scheduleNotification(notification: ILocalNotification): Promise<void> {
|
2020-10-07 08:53:19 +00:00
|
|
|
// Check if the notification has been triggered already.
|
2020-10-08 11:36:16 +00:00
|
|
|
const triggered = await this.isTriggered(notification, false);
|
|
|
|
|
|
|
|
// Cancel the current notification in case it gets scheduled twice.
|
|
|
|
LocalNotifications.instance.cancel(notification.id).finally(async () => {
|
|
|
|
if (!triggered) {
|
|
|
|
let soundEnabled: boolean;
|
|
|
|
|
|
|
|
// Check if sound is enabled for notifications.
|
|
|
|
if (!this.canDisableSound()) {
|
|
|
|
soundEnabled = true;
|
|
|
|
} else {
|
|
|
|
soundEnabled = await CoreConfig.instance.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
2020-10-08 11:36:16 +00:00
|
|
|
|
|
|
|
if (!soundEnabled) {
|
2020-10-21 14:32:27 +00:00
|
|
|
notification.sound = undefined;
|
2020-10-08 11:36:16 +00:00
|
|
|
} else {
|
|
|
|
delete notification.sound; // Use default value.
|
|
|
|
}
|
|
|
|
|
|
|
|
notification.foreground = true;
|
|
|
|
|
|
|
|
// Remove from triggered, since the notification could be in there with a different time.
|
2020-10-21 14:32:27 +00:00
|
|
|
this.removeTriggered(notification.id || 0);
|
2020-10-08 11:36:16 +00:00
|
|
|
LocalNotifications.instance.schedule(notification);
|
|
|
|
}
|
2020-10-07 08:53:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function to call when a notification is triggered. Stores the notification so it's not scheduled again unless the
|
|
|
|
* time is changed.
|
|
|
|
*
|
|
|
|
* @param notification Triggered notification.
|
|
|
|
* @return Promise resolved when stored, rejected otherwise.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
async trigger(notification: ILocalNotification): Promise<number> {
|
2020-10-07 08:53:19 +00:00
|
|
|
await this.dbReady;
|
|
|
|
|
|
|
|
const entry = {
|
|
|
|
id: notification.id,
|
2020-10-14 14:38:24 +00:00
|
|
|
at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(),
|
2020-10-07 08:53:19 +00:00
|
|
|
};
|
|
|
|
|
2020-10-28 13:25:18 +00:00
|
|
|
return this.appDB.insertRecord(TRIGGERED_TABLE_NAME, entry);
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update a component name.
|
|
|
|
*
|
|
|
|
* @param oldName The old name.
|
|
|
|
* @param newName The new name.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2020-10-08 11:36:16 +00:00
|
|
|
async updateComponentName(oldName: string, newName: string): Promise<void> {
|
2020-10-07 08:53:19 +00:00
|
|
|
await this.dbReady;
|
|
|
|
|
2020-10-28 13:25:18 +00:00
|
|
|
const oldId = COMPONENTS_TABLE_NAME + '#' + oldName;
|
|
|
|
const newId = COMPONENTS_TABLE_NAME + '#' + newName;
|
2020-10-07 08:53:19 +00:00
|
|
|
|
2020-10-28 13:25:18 +00:00
|
|
|
await this.appDB.updateRecords(COMPONENTS_TABLE_NAME, { id: newId }, { id: oldId });
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
2020-10-08 11:36:16 +00:00
|
|
|
|
2020-10-07 08:53:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {}
|
2020-10-08 11:36:16 +00:00
|
|
|
|
|
|
|
export type CoreLocalNotificationsClickCallback<T = unknown> = (value: T) => void;
|