MOBILE-2333 siteaddons: Prefetch offlinefunctions

main
Dani Palou 2018-02-16 11:34:29 +01:00
parent c3ca95e838
commit dc88b83bbb
7 changed files with 331 additions and 34 deletions

View File

@ -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<any>} Promise resolved when invalidated.
*/
invalidateModule(module: any, courseId: number): Promise<any> {
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.
*

View File

@ -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<any>} Promise resolved when all content is downloaded.
*/
download(module: any, courseId: number): Promise<any> {
return this.downloadOrPrefetch(module, courseId, false);
download(module: any, courseId: number, dirPath?: string): Promise<any> {
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<any>} Promise resolved when done.
*/
prefetch(module: any, courseId?: number, single?: boolean): Promise<any> {
return this.downloadOrPrefetch(module, courseId, true);
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
return this.downloadOrPrefetch(module, courseId, true, dirPath);
}
/**

View File

@ -141,8 +141,8 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler {
hasUpdates?(module: any, courseId: number, moduleUpdates: any[]): boolean | Promise<boolean>;
/**
* 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.

View File

@ -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<any>} Promise resolved when all content is downloaded. Data returned is not reliable.
*/
downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> {
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<any>} Promise resolved when done.
*/
protected downloadOrPrefetchAddon(module: any, courseId: number, single?: boolean, siteId?: string, prefetch?: boolean,
dirPath?: string): Promise<any> {
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<any>} Promise resolved when done.
*/
protected downloadOrPrefetchFiles(siteId: string, module: any, courseId: number, prefetch?: boolean, dirPath?: string)
: Promise<any> {
// 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<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number): Promise<any> {
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<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
}

View File

@ -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);
}

View File

@ -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<any>} Promise resolved with the response.
*/
callWS(method: string, data: any, preSets?: CoreSiteWSPreSets, siteId?: string): Promise<any> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateAllCallWSForMethod(method: string, siteId?: string): Promise<any> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateCallWS(method: string, data: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getCallWSCacheKey(method, data));
});
}
/**
* Invalidate a page content.
*

View File

@ -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<any>} Resolved on success.
*/
downloadOrPrefetchFiles(siteId: string, files: any[], prefetch: boolean, ignoreStale?: boolean, component?: string,
componentId?: string | number): Promise<any> {
componentId?: string | number, dirPath?: string): Promise<any> {
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));
}
});