// (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 { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreConstants } from '@core/constants'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; /** * Service to handle calendar events. */ @Injectable() export class AddonCalendarProvider { static DAYS_INTERVAL = 30; static COMPONENT = 'AddonCalendarEvents'; static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME = 60; protected ROOT_CACHE_KEY = 'mmaCalendar:'; // Variables for database. static EVENTS_TABLE = 'addon_calendar_events'; protected siteSchema: CoreSiteSchema = { name: 'AddonCalendarProvider', version: 1, tables: [ { name: AddonCalendarProvider.EVENTS_TABLE, columns: [ { name: 'id', type: 'INTEGER', primaryKey: true }, { name: 'notificationtime', type: 'INTEGER' }, { name: 'name', type: 'TEXT', notNull: true }, { name: 'description', type: 'TEXT' }, { name: 'format', type: 'INTEGER' }, { name: 'eventtype', type: 'TEXT' }, { name: 'courseid', type: 'INTEGER' }, { name: 'timestart', type: 'INTEGER' }, { name: 'timeduration', type: 'INTEGER' }, { name: 'categoryid', type: 'INTEGER' }, { name: 'groupid', type: 'INTEGER' }, { name: 'userid', type: 'INTEGER' }, { name: 'instance', type: 'INTEGER' }, { name: 'modulename', type: 'TEXT' }, { name: 'timemodified', type: 'INTEGER' }, { name: 'repeatid', type: 'INTEGER' }, { name: 'visible', type: 'INTEGER' }, { name: 'uuid', type: 'TEXT' }, { name: 'sequence', type: 'INTEGER' }, { name: 'subscriptionid', type: 'INTEGER' } ] } ] }; protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) { this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } /** * Removes expired events from local DB. * * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved when done. */ cleanExpiredEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { let promise; // Cancel expired events notifications first. if (this.localNotificationsProvider.isAvailable()) { promise = site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart < ?', [this.timeUtils.timestamp()]).then((events) => { events.forEach((event) => { return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, site.getId()); }); }).catch(() => { // Ignore errors. }); } else { promise = Promise.resolve(); } return promise.then(() => { return site.getDb().deleteRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart < ?', [this.timeUtils.timestamp()]); }); }); } /** * Get all calendar events from local Db. * * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved with all the events. */ getAllEventsFromLocalDb(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getAllRecords(AddonCalendarProvider.EVENTS_TABLE); }); } /** * Get the configured default notification time. * * @param {string} [siteId] ID of the site. If not defined, use current site. * @return {Promise} Promise resolved with the default time. */ getDefaultNotificationTime(siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; return this.configProvider.get(key, AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME); } /** * Get a calendar event. If the server request fails and data is not cached, try to get it from local DB. * * @param {number} id Event ID. * @param {boolean} [refresh] True when we should update the event data. * @param {string} [siteId] ID of the site. If not defined, use current site. * @return {Promise} Promise resolved when the event data is retrieved. */ getEvent(id: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { cacheKey: this.getEventCacheKey(id) }, data = { options: { userevents: 0, siteevents: 0, }, events: { eventids: [ id ] } }; return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => { // The WebService returns all category events. Check the response to search for the event we want. const event = response.events.find((e) => { return e.id == id; }); return event || this.getEventFromLocalDb(id); }).catch(() => { return this.getEventFromLocalDb(id); }); }); } /** * Get a calendar event by ID. This function returns more data than getEvent, but it isn't available in all Moodles. * * @param {number} id Event ID. * @param {boolean} [refresh] True when we should update the event data. * @param {string} [siteId] ID of the site. If not defined, use current site. * @return {Promise} Promise resolved when the event data is retrieved. * @since 3.4 */ getEventById(id: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { cacheKey: this.getEventCacheKey(id) }, data = { eventid: id }; return site.read('core_calendar_get_calendar_event_by_id', data, preSets).then((response) => { return response.event; }); }); } /** * Get cache key for a single event WS call. * * @param {number} id Event ID. * @return {string} Cache key. */ protected getEventCacheKey(id: number): string { return this.ROOT_CACHE_KEY + 'events:' + id; } /** * Get a calendar event from local Db. * * @param {number} id Event ID. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved when the event data is retrieved. */ getEventFromLocalDb(id: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecord(AddonCalendarProvider.EVENTS_TABLE, { id: id }); }); } /** * Get event notification time. Always returns number of minutes (0 if disabled). * * @param {number} id Event ID. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Event notification time in minutes. 0 if disabled. */ getEventNotificationTime(id: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); return this.getEventNotificationTimeOption(id, siteId).then((time: number) => { if (time == -1) { return this.getDefaultNotificationTime(siteId); } return time; }); } /** * Get event notification time for options. Returns -1 for default time. * * @param {number} id Event ID. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise with event notification time in minutes. 0 if disabled, -1 if default time. */ getEventNotificationTimeOption(id: number, siteId?: string): Promise { return this.getEventFromLocalDb(id, siteId).then((e) => { console.error(e.notificationtime); return e.notificationtime || 0; }).catch(() => { return -1; }); } /** * Get the events in a certain period. The period is calculated like this: * start time: now + daysToStart * end time: start time + daysInterval * E.g. using provider.getEventsList(30, 30) is going to get the events starting after 30 days from now * and ending before 60 days from now. * * @param {number} [daysToStart=0] Number of days from now to start getting events. * @param {number} [daysInterval=30] Number of days between timestart and timeend. * @param {string} [siteId] Site to get the events from. If not defined, use current site. * @return {Promise} Promise to be resolved when the participants are retrieved. */ getEventsList(daysToStart: number = 0, daysInterval: number = AddonCalendarProvider.DAYS_INTERVAL, siteId?: string) : Promise { return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); const promises = []; let courses, groups; promises.push(this.coursesProvider.getUserCourses(false, siteId).then((data) => { courses = data; courses.push({ id: site.getSiteHomeId() }); // Add front page. })); promises.push(this.groupsProvider.getAllUserGroups(siteId).then((data) => { groups = data; })); return Promise.all(promises).then(() => { const now = this.timeUtils.timestamp(), start = now + (CoreConstants.SECONDS_DAY * daysToStart), end = start + (CoreConstants.SECONDS_DAY * daysInterval), data = { options: { userevents: 1, siteevents: 1, timestart: start, timeend: end }, events: { courseids: [], groupids: [] } }; data.events.courseids = courses.map((course) => { return course.id; }); data.events.groupids = groups.map((group) => { return group.id; }); // We need to retrieve cached data using cache key because we have timestamp in the params. const preSets = { cacheKey: this.getEventsListCacheKey(daysToStart, daysInterval), getCacheUsingCacheKey: true }; return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => { this.storeEventsInLocalDB(response.events, siteId); return response.events; }); }); }); } /** * Get prefix cache key for events list WS calls. * * @return {string} Prefix Cache key. */ protected getEventsListPrefixCacheKey(): string { return this.ROOT_CACHE_KEY + 'events:'; } /** * Get cache key for events list WS calls. * * @param {number} daysToStart Number of days from now to start getting events. * @param {number} daysInterval Number of days between timestart and timeend. * @return {string} Cache key. */ protected getEventsListCacheKey(daysToStart: number, daysInterval: number): string { return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval; } /** * Invalidates events list and all the single events and related info. * * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved when the list is invalidated. */ invalidateEventsList(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); const promises = []; promises.push(this.coursesProvider.invalidateUserCourses(siteId)); promises.push(this.groupsProvider.invalidateAllUserGroups(siteId)); promises.push(site.invalidateWsCacheForKeyStartingWith(this.getEventsListPrefixCacheKey())); return Promise.all(promises); }); } /** * Invalidates a single event. * * @param {number} eventId List of courses or course ids. * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved when the list is invalidated. */ invalidateEvent(eventId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getEventCacheKey(eventId)); }); } /** * Check if Calendar is disabled in a certain site. * * @param {CoreSite} [site] Site. If not defined, use current site. * @return {boolean} Whether it's disabled. */ isCalendarDisabledInSite(site?: CoreSite): boolean { site = site || this.sitesProvider.getCurrentSite(); return site.isFeatureDisabled('CoreMainMenuDelegate_AddonCalendar'); } /** * Check if Calendar is disabled in a certain site. * * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. */ isDisabled(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return this.isCalendarDisabledInSite(site); }); } /** * Check if the get event by ID WS is available. * * @return {boolean} Whether it's available. * @since 3.4 */ isGetEventByIdAvailable(): boolean { return this.sitesProvider.wsAvailableInCurrentSite('core_calendar_get_calendar_event_by_id'); } /** * Get the next events for all the sites and schedules their notifications. * If an event notification time is 0, cancel its scheduled notification (if any). * If local notification plugin is not enabled, resolve the promise. * * @return {Promise} Promise resolved when all the notifications have been scheduled. */ scheduleAllSitesEventsNotifications(): Promise { const notificationsEnabled = this.localNotificationsProvider.isAvailable(); return this.sitesProvider.getSitesIds().then((siteIds) => { const promises = []; siteIds.forEach((siteId) => { promises.push(this.cleanExpiredEvents(siteId).then(() => { if (notificationsEnabled) { // Check if calendar is disabled for the site. return this.isDisabled(siteId).then((disabled) => { if (!disabled) { // Get first events. return this.getEventsList(undefined, undefined, siteId).then((events) => { return this.scheduleEventsNotifications(events, siteId); }); } }); } })); }); return Promise.all(promises); }); } /** * Schedules an event notification. If time is 0, cancel scheduled notification if any. * If local notification plugin is not enabled, resolve the promise. * * @param {any} event Event to schedule. * @param {number} time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". * @param {string} [siteId] Site ID the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved when the notification is scheduled. */ scheduleEventNotification(event: any, time: number, siteId?: string): Promise { if (this.localNotificationsProvider.isAvailable()) { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (time === 0) { // Cancel if it was scheduled. return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId); } // If time is -1, get event default time. const promise = time == -1 ? this.getDefaultNotificationTime(siteId) : Promise.resolve(time); return promise.then((time) => { const timeEnd = (event.timestart + event.timeduration) * 1000; if (timeEnd <= new Date().getTime()) { // The event has finished already, don't schedule it. return Promise.resolve(); } const notification: ILocalNotification = { id: event.id, title: event.name, text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true), trigger: { at: new Date((event.timestart - (time * 60)) * 1000) }, data: { eventid: event.id, siteid: siteId } }; return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); }); } else { return Promise.resolve(); } } /** * Schedules the notifications for a list of events. * If an event notification time is 0, cancel its scheduled notification (if any). * If local notification plugin is not enabled, resolve the promise. * * @param {any[]} events Events to schedule. * @param {string} [siteId] ID of the site the events belong to. If not defined, use current site. * @return {Promise} Promise resolved when all the notifications have been scheduled. */ scheduleEventsNotifications(events: any[], siteId?: string): Promise { const promises = []; if (this.localNotificationsProvider.isAvailable()) { siteId = siteId || this.sitesProvider.getCurrentSiteId(); events.forEach((e) => { promises.push(this.getEventNotificationTime(e.id, siteId).then((time) => { return this.scheduleEventNotification(e, time, siteId); })); }); } return Promise.all(promises); } /** * Set the default notification time. * * @param {number} time New default time. * @param {string} [siteId] ID of the site. If not defined, use current site. * @return {Promise} Promise resolved when stored. */ setDefaultNotificationTime(time: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; return this.configProvider.set(key, time); } /** * Store an event in local DB as it is. * * @param {any} event Event to store. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved when stored. */ storeEventInLocalDb(event: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, event); }); } /** * Store events in local DB. * * @param {any[]} events Events to store. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved when the events are stored. */ protected storeEventsInLocalDB(events: any[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); const promises = [], db = site.getDb(); events.forEach((event) => { // Don't override event notification time if the user configured it. promises.push(this.getEventFromLocalDb(event.id, siteId).catch(() => { // Event not stored, return empty object. return {}; }).then((e) => { const eventRecord = { id: event.id, name: event.name, description: event.description, format: event.format, eventtype: event.eventtype, courseid: event.courseid, timestart: event.timestart, timeduration: event.timeduration, categoryid: event.categoryid, groupid: event.groupid, userid: event.userid, instance: event.instance, modulename: event.modulename, timemodified: event.timemodified, repeatid: event.repeatid, visible: event.visible, uuid: event.uuid, sequence: event.sequence, subscriptionid: event.subscriptionid, notificationtime: e.notificationtime || -1 }; return db.insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord); })); }); return Promise.all(promises); }); } /** * Updates an event notification time and schedule a new notification. * * @param {any} event Event to update its notification time. * @param {number} time New notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Promise resolved when the notification is updated. */ updateNotificationTime(event: any, time: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { if (!this.sitesProvider.isLoggedIn()) { // Not logged in, we can't get the site DB. User logged out or session expired while an operation was ongoing. return Promise.reject(null); } return site.getDb().updateRecords(AddonCalendarProvider.EVENTS_TABLE, {notificationtime: time}, {id: event.id}) .then(() => { if (time == 0) { // No notification, cancel it. return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, site.getId()); } else { return this.scheduleEventNotification(event, time); } }); }); } }