From dc88b83bbb57b40dbfb4d26fff03690133aae8a0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 16 Feb 2018 11:34:29 +0100 Subject: [PATCH] MOBILE-2333 siteaddons: Prefetch offlinefunctions --- .../mod/book/providers/prefetch-handler.ts | 16 -- .../course/classes/module-prefetch-handler.ts | 14 +- .../providers/module-prefetch-delegate.ts | 4 +- .../classes/module-prefetch-handler.ts | 224 ++++++++++++++++++ src/core/siteaddons/providers/helper.ts | 19 +- src/core/siteaddons/providers/siteaddons.ts | 70 +++++- src/providers/filepool.ts | 18 +- 7 files changed, 331 insertions(+), 34 deletions(-) create mode 100644 src/core/siteaddons/classes/module-prefetch-handler.ts diff --git a/src/addon/mod/book/providers/prefetch-handler.ts b/src/addon/mod/book/providers/prefetch-handler.ts index ca99a83fc..17aa4b636 100644 --- a/src/addon/mod/book/providers/prefetch-handler.ts +++ b/src/addon/mod/book/providers/prefetch-handler.ts @@ -78,22 +78,6 @@ export class AddonModBookPrefetchHandler extends CoreCourseModulePrefetchHandler return this.bookProvider.invalidateContent(moduleId, courseId); } - /** - * Invalidate WS calls needed to determine module status. - * - * @param {any} module Module. - * @param {number} courseId Course ID the module belongs to. - * @return {Promise} Promise resolved when invalidated. - */ - invalidateModule(module: any, courseId: number): Promise { - const promises = []; - - promises.push(this.bookProvider.invalidateBookData(courseId)); - promises.push(this.courseProvider.invalidateModule(module.id)); - - return Promise.all(promises); - } - /** * Whether or not the handler is enabled on a site level. * diff --git a/src/core/course/classes/module-prefetch-handler.ts b/src/core/course/classes/module-prefetch-handler.ts index 7fde10f08..c078e1b6a 100644 --- a/src/core/course/classes/module-prefetch-handler.ts +++ b/src/core/course/classes/module-prefetch-handler.ts @@ -133,10 +133,11 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * * @param {any} module The module object returned by WS. * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. * @return {Promise} Promise resolved when all content is downloaded. */ - download(module: any, courseId: number): Promise { - return this.downloadOrPrefetch(module, courseId, false); + download(module: any, courseId: number, dirPath?: string): Promise { + return this.downloadOrPrefetch(module, courseId, false, dirPath); } /** @@ -332,8 +333,8 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref } /** - * Invalidate WS calls needed to determine module status. It doesn't need to invalidate check updates. - * It should NOT invalidate files nor all the prefetched data. + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. * * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. @@ -409,10 +410,11 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. * @return {Promise} Promise resolved when done. */ - prefetch(module: any, courseId?: number, single?: boolean): Promise { - return this.downloadOrPrefetch(module, courseId, true); + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.downloadOrPrefetch(module, courseId, true, dirPath); } /** diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 76589a93e..dce46a02a 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -141,8 +141,8 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler { hasUpdates?(module: any, courseId: number, moduleUpdates: any[]): boolean | Promise; /** - * Invalidate WS calls needed to determine module status. It doesn't need to invalidate check updates. - * It should NOT invalidate files nor all the prefetched data. + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. * * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. diff --git a/src/core/siteaddons/classes/module-prefetch-handler.ts b/src/core/siteaddons/classes/module-prefetch-handler.ts new file mode 100644 index 000000000..3972f0d56 --- /dev/null +++ b/src/core/siteaddons/classes/module-prefetch-handler.ts @@ -0,0 +1,224 @@ +// (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 { Injector } from '@angular/core'; +import { CoreSiteAddonsProvider } from '../../siteaddons/providers/siteaddons'; +import { CoreCourseModulePrefetchHandlerBase } from '../../course/classes/module-prefetch-handler'; + +/** + * Handler to prefetch a site addon. + */ +export class CoreSiteAddonsModulePrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + protected ROOT_CACHE_KEY = 'CoreSiteAddonsModulePrefetchHandler:'; + + constructor(injector: Injector, protected siteAddonsProvider: CoreSiteAddonsProvider, component: string, modName: string, + protected handlerSchema: any) { + super(injector); + + this.component = component; + this.name = modName; + this.isResource = handlerSchema.isresource; + + if (handlerSchema.updatesnames) { + try { + this.updatesNames = new RegExp(handlerSchema.updatesnames); + } catch (ex) { + // Ignore errors. + } + } + } + + /** + * Download or prefetch the content. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files + * relative paths and make the package work in an iframe. Undefined to download the files + * in the filepool root folder. + * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable. + */ + downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, false, this.downloadOrPrefetchAddon.bind(this), undefined, prefetch, dirPath); + } + + /** + * Download or prefetch the addon, downloading the files and calling the needed WS. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + protected downloadOrPrefetchAddon(module: any, courseId: number, single?: boolean, siteId?: string, prefetch?: boolean, + dirPath?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + const promises = [], + args = { + courseid: courseId, + cmid: module.id, + userid: site.getUserId() + }; + + // Download the files (if any). + promises.push(this.downloadOrPrefetchFiles(site.id, module, courseId, prefetch, dirPath)); + + // Call all the offline functions. + for (const method in this.handlerSchema.offlinefunctions) { + if (site.wsAvailable(method)) { + // The method is a WS. + const paramsList = this.handlerSchema.offlinefunctions[method], + cacheKey = this.siteAddonsProvider.getCallWSCacheKey(method, args); + let params = {}; + + if (!paramsList.length) { + // No params defined, send the default ones. + params = args; + } else { + for (const i in paramsList) { + const paramName = paramsList[i]; + + if (typeof args[paramName] != 'undefined') { + params[paramName] = args[paramName]; + } else { + // The param is not one of the default ones. Try to calculate the param to use. + const value = this.getDownloadParam(module, courseId, paramName); + if (typeof value != 'undefined') { + params[paramName] = value; + } + } + } + } + + promises.push(this.siteAddonsProvider.callWS(method, params, {cacheKey: cacheKey})); + } else { + // It's a method to get content. + promises.push(this.siteAddonsProvider.getContent(this.component, method, args)); + } + } + + return Promise.all(promises); + }); + } + + /** + * Download or prefetch the addon files. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + protected downloadOrPrefetchFiles(siteId: string, module: any, courseId: number, prefetch?: boolean, dirPath?: string) + : Promise { + // Load module contents (ignore cache so we always have the latest data). + return this.loadContents(module, courseId, true).then(() => { + // Get the intro files. + return this.getIntroFiles(module, courseId); + }).then((introFiles) => { + const contentFiles = this.getContentDownloadableFiles(module), + promises = []; + + if (dirPath) { + // Download intro files in filepool root folder. + promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, introFiles, prefetch, false, + this.component, module.id)); + + // Download content files inside dirPath. + promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, contentFiles, prefetch, false, + this.component, module.id, dirPath)); + } else { + // No dirPath, download everything in filepool root folder. + const files = introFiles.concat(contentFiles); + promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, files, prefetch, false, + this.component, module.id)); + } + + return Promise.all(promises); + }); + } + + /** + * Get the value of a WS param for prefetch. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} paramName Name of the param as defined by the handler. + * @return {any} The value. + */ + protected getDownloadParam(module: any, courseId: number, paramName: string): any { + switch (paramName) { + case 'courseids': + // The WS needs the list of course IDs. Create the list. + return [courseId]; + + case this.component + 'id': + // The WS needs the instance id. + return module.instance; + + default: + // No more params supported for now. + } + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + const promises = [], + currentSite = this.sitesProvider.getCurrentSite(), + siteId = currentSite.getId(), + args = { + courseid: courseId, + cmid: moduleId, + userid: currentSite.getUserId() + }; + + // Invalidate files and the module. + promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, this.component, moduleId)); + promises.push(this.courseProvider.invalidateModule(moduleId, siteId)); + + // Also invalidate all the WS calls. + for (const method in this.handlerSchema.offlinefunctions) { + if (currentSite.wsAvailable(method)) { + // The method is a WS. + promises.push(currentSite.invalidateWsCacheForKey(this.siteAddonsProvider.getCallWSCacheKey(method, args))); + } else { + // It's a method to get content. + promises.push(this.siteAddonsProvider.invalidateContent(this.component, method, args)); + } + } + + return this.utils.allPromises(promises); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/core/siteaddons/providers/helper.ts b/src/core/siteaddons/providers/helper.ts index 5e326759c..463f5b079 100644 --- a/src/core/siteaddons/providers/helper.ts +++ b/src/core/siteaddons/providers/helper.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { CoreLangProvider } from '../../../providers/lang'; import { CoreLoggerProvider } from '../../../providers/logger'; @@ -22,10 +22,12 @@ import { CoreMainMenuDelegate, CoreMainMenuHandler, CoreMainMenuHandlerData } fr import { CoreCourseModuleDelegate, CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../../../core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '../../../core/course/providers/module-prefetch-delegate'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '../../../core/user/providers/user-delegate'; import { CoreDelegateHandler } from '../../../classes/delegate'; import { CoreSiteAddonsModuleIndexComponent } from '../components/module-index/module-index'; import { CoreSiteAddonsProvider } from './siteaddons'; +import { CoreSiteAddonsModulePrefetchHandler } from '../classes/module-prefetch-handler'; /** * Helper service to provide functionalities regarding site addons. It basically has the features to load and register site @@ -37,10 +39,10 @@ import { CoreSiteAddonsProvider } from './siteaddons'; export class CoreSiteAddonsHelperProvider { protected logger; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private injector: Injector, private mainMenuDelegate: CoreMainMenuDelegate, private moduleDelegate: CoreCourseModuleDelegate, private userDelegate: CoreUserDelegate, private langProvider: CoreLangProvider, - private siteAddonsProvider: CoreSiteAddonsProvider) { + private siteAddonsProvider: CoreSiteAddonsProvider, private prefetchDelegate: CoreCourseModulePrefetchDelegate) { this.logger = logger.getInstance('CoreSiteAddonsHelperProvider'); } @@ -240,7 +242,8 @@ export class CoreSiteAddonsHelperProvider { // Create the base handler. const modName = addon.component.replace('mod_', ''), - baseHandler = this.getBaseHandler(modName); + baseHandler = this.getBaseHandler(modName), + hasOfflineFunctions = !!(handlerSchema.offlinefunctions && Object.keys(handlerSchema.offlinefunctions).length); let moduleHandler: CoreCourseModuleHandler; // Store the handler data. @@ -257,7 +260,7 @@ export class CoreSiteAddonsHelperProvider { title: module.name, icon: handlerSchema.displaydata.icon, class: handlerSchema.displaydata.class, - showDownloadButton: handlerSchema.offlinefunctions && handlerSchema.offlinefunctions.length, + showDownloadButton: hasOfflineFunctions, action: (event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void => { event.preventDefault(); event.stopPropagation(); @@ -279,6 +282,12 @@ export class CoreSiteAddonsHelperProvider { } }); + if (hasOfflineFunctions) { + // Register the prefetch handler. + this.prefetchDelegate.registerHandler(new CoreSiteAddonsModulePrefetchHandler( + this.injector, this.siteAddonsProvider, addon.component, modName, handlerSchema)); + } + this.moduleDelegate.registerHandler(moduleHandler); } diff --git a/src/core/siteaddons/providers/siteaddons.ts b/src/core/siteaddons/providers/siteaddons.ts index f4b4a0ea5..fdd3dff6d 100644 --- a/src/core/siteaddons/providers/siteaddons.ts +++ b/src/core/siteaddons/providers/siteaddons.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreLangProvider } from '../../../providers/lang'; import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSite } from '../../../classes/site'; +import { CoreSite, CoreSiteWSPreSets } from '../../../classes/site'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreUtilsProvider } from '../../../providers/utils/utils'; import { CoreConfigConstants } from '../../../configconstants'; @@ -58,6 +58,45 @@ export class CoreSiteAddonsProvider { this.logger = logger.getInstance('CoreUserProvider'); } + /** + * Call a WS for a site addon. + * + * @param {string} method WS method to use. + * @param {any} data Data to send to the WS. + * @param {CoreSiteWSPreSets} [preSets] Extra options. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + callWS(method: string, data: any, preSets?: CoreSiteWSPreSets, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + preSets = preSets || {}; + preSets.cacheKey = preSets.cacheKey || this.getCallWSCacheKey(method, data); + + return site.read(method, data, preSets); + }); + } + + /** + * Get cache key for a WS call. + * + * @param {string} method Name of the method. + * @param {any} data Data to identify the WS call. + * @return {string} Cache key. + */ + getCallWSCacheKey(method: string, data: any): string { + return this.getCallWSCommonCacheKey(method) + ':' + this.utils.sortAndStringify(data); + } + + /** + * Get common cache key for a WS call. + * + * @param {string} method Name of the method. + * @return {string} Cache key. + */ + protected getCallWSCommonCacheKey(method: string): string { + return this.ROOT_CACHE_KEY + method; + } + /** * Get a certain content for a site addon. * @@ -103,7 +142,7 @@ export class CoreSiteAddonsProvider { * @return {string} Cache key. */ protected getContentCacheKey(component: string, method: string, args: any): string { - return this.ROOT_CACHE_KEY + 'content:' + component + ':' + method + ':' + JSON.stringify(args); + return this.ROOT_CACHE_KEY + 'content:' + component + ':' + method + ':' + this.utils.sortAndStringify(args); } /** @@ -116,6 +155,33 @@ export class CoreSiteAddonsProvider { return this.moduleSiteAddons[modName]; } + /** + * Invalidate all WS call to a certain method. + * + * @param {string} method WS method to use. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllCallWSForMethod(method: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getCallWSCommonCacheKey(method)); + }); + } + + /** + * Invalidate a WS call. + * + * @param {string} method WS method to use. + * @param {any} data Data to send to the WS. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateCallWS(method: string, data: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCallWSCacheKey(method, data)); + }); + } + /** * Invalidate a page content. * diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index cc677fef9..259ba617f 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -961,10 +961,12 @@ export class CoreFilepoolProvider { * @param {boolean} [ignoreStale] True if 'stale' should be ignored. Only if prefetch=false. * @param {string} [component] The component to link the file to. * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {string} [dirPath] Name of the directory where to store the files (inside filepool dir). If not defined, store + * the files directly inside the filepool folder. * @return {Promise} Resolved on success. */ downloadOrPrefetchFiles(siteId: string, files: any[], prefetch: boolean, ignoreStale?: boolean, component?: string, - componentId?: string | number): Promise { + componentId?: string | number, dirPath?: string): Promise { const promises = []; // Download files. @@ -975,13 +977,23 @@ export class CoreFilepoolProvider { isexternalfile: file.isexternalfile, repositorytype: file.repositorytype }; + let path; + + if (dirPath) { + // Calculate the path to the file. + path = file.filename; + if (file.filepath !== '/') { + path = file.filepath.substr(1) + path; + } + path = this.textUtils.concatenatePaths(dirPath, path); + } if (prefetch) { promises.push(this.addToQueueByUrl( - siteId, url, component, componentId, timemodified, undefined, undefined, 0, options)); + siteId, url, component, componentId, timemodified, path, undefined, 0, options)); } else { promises.push(this.downloadUrl( - siteId, url, ignoreStale, component, componentId, timemodified, undefined, undefined, options)); + siteId, url, ignoreStale, component, componentId, timemodified, path, undefined, options)); } });