diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index fe583dd04..fb2243153 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -352,14 +352,13 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo } const modal = this.domUtils.showModalLoading('core.sending', true); - this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then(() => { + this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then((online) => { // Success! // Check completion since it could be configured to complete once the user answers the choice. this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); this.domUtils.scrollToTop(this.content); - // Let's refresh the data. - return this.refreshContent(false); + return this.dataUpdated(online); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); }).finally(() => { @@ -377,7 +376,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.choiceProvider.deleteResponses(this.choice.id, this.choice.name, this.courseId).then(() => { this.domUtils.scrollToTop(this.content); - // Success! Let's refresh the data. + // Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated. return this.refreshContent(false); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); @@ -389,6 +388,28 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo }); } + /** + * Function to call when some data has changed. It will refresh/prefetch data. + * + * @param {boolean} online Whether the data was sent to server or stored in offline. + * @return {Promise} Promise resolved when done. + */ + protected dataUpdated(online: boolean): Promise { + if (online && this.isPrefetched()) { + // The choice is downloaded, update the data. + return this.choiceSync.prefetchAfterUpdate(this.module, this.courseId).then(() => { + // Update the view. + this.showLoadingAndFetch(false, false); + }).catch(() => { + // Prefetch failed, refresh the data. + return this.refreshContent(false); + }); + } else { + // Not downloaded, refresh the data. + return this.refreshContent(false); + } + } + /** * Performs the sync of the activity. * diff --git a/src/addon/mod/choice/providers/choice.ts b/src/addon/mod/choice/providers/choice.ts index 8845aed49..b94e92b9c 100644 --- a/src/addon/mod/choice/providers/choice.ts +++ b/src/addon/mod/choice/providers/choice.ts @@ -19,6 +19,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModChoiceOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for choices. @@ -68,9 +69,9 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID the choice belongs to. * @param {number[]} [responses] IDs of the answers. If not defined, delete all the answers of the current user. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the options are deleted. + * @return {Promise} Promise resolved with boolean: true if response was sent to server, false if stored in device. */ - deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise { + deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); responses = responses || []; @@ -173,20 +174,29 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {string} key Name of the property to check. * @param {any} value Value to search. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache: boolean = false) - : Promise { + protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, + ignoreCache?: boolean): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceDataCacheKey(courseId), omitExpires: forceCache }; + if (forceCache) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choices_by_courses', params, preSets).then((response) => { if (response && response.choices) { const currentChoice = response.choices.find((choice) => choice[key] == value); @@ -206,11 +216,12 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {number} cmId Course module ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - getChoice(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache); + getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); } /** @@ -219,29 +230,36 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {number} choiceId Choice ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache); + getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); } /** * Get choice options. * - * @param {number} choiceId Choice ID. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} choiceId Choice ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with choice options. */ - getOptions(choiceId: number, siteId?: string): Promise { + getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceOptionsCacheKey(choiceId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choice_options', params, preSets).then((response) => { if (response.options) { return response.options; @@ -255,19 +273,25 @@ export class AddonModChoiceProvider { /** * Get choice results. * - * @param {number} choiceId Choice ID. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} choiceId Choice ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with choice results. */ - getResults(choiceId: number, siteId?: string): Promise { + getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceResultsCacheKey(choiceId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choice_results', params, preSets).then((response) => { if (response.options) { return response.options; diff --git a/src/addon/mod/choice/providers/prefetch-handler.ts b/src/addon/mod/choice/providers/prefetch-handler.ts index fb3680e4c..0b2f74eba 100644 --- a/src/addon/mod/choice/providers/prefetch-handler.ts +++ b/src/addon/mod/choice/providers/prefetch-handler.ts @@ -23,7 +23,6 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonModChoiceProvider } from './choice'; -import { AddonModChoiceSyncProvider } from './sync'; /** * Handler to prefetch choices. @@ -38,7 +37,7 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan constructor(translate: TranslateService, appProvider: CoreAppProvider, utils: CoreUtilsProvider, courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider, domUtils: CoreDomUtilsProvider, protected choiceProvider: AddonModChoiceProvider, - protected syncProvider: AddonModChoiceSyncProvider, protected userProvider: CoreUserProvider) { + protected userProvider: CoreUserProvider) { super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); } @@ -66,12 +65,12 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan * @return {Promise} Promise resolved when done. */ protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.choiceProvider.getChoice(courseId, module.id, siteId).then((choice) => { + return this.choiceProvider.getChoice(courseId, module.id, siteId, false, true).then((choice) => { const promises = []; // Get the options and results. - promises.push(this.choiceProvider.getOptions(choice.id, siteId)); - promises.push(this.choiceProvider.getResults(choice.id, siteId).then((options) => { + promises.push(this.choiceProvider.getOptions(choice.id, true, siteId)); + promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => { // If we can see the users that answered, prefetch their profile and avatar. const subPromises = []; options.forEach((option) => { diff --git a/src/addon/mod/choice/providers/sync.ts b/src/addon/mod/choice/providers/sync.ts index be612049a..606ffc385 100644 --- a/src/addon/mod/choice/providers/sync.ts +++ b/src/addon/mod/choice/providers/sync.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreSitesProvider } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -26,13 +25,16 @@ import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { CoreSyncProvider } from '@providers/sync'; +import { AddonModChoicePrefetchHandler } from './prefetch-handler'; /** * Service to sync choices. */ @Injectable() -export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { +export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_choice_autom_synced'; protected componentTranslate: string; @@ -42,9 +44,11 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { private eventsProvider: CoreEventsProvider, private choiceProvider: AddonModChoiceProvider, translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider, - private logHelper: CoreCourseLogHelperProvider) { + private logHelper: CoreCourseLogHelperProvider, prefetchHandler: AddonModChoicePrefetchHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate) { + super('AddonModChoiceSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('choice'); } @@ -195,16 +199,8 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (courseId) { - const promises = [ - this.choiceProvider.invalidateChoiceData(courseId), - choiceId ? this.choiceProvider.invalidateOptions(choiceId) : Promise.resolve(), - choiceId ? this.choiceProvider.invalidateResults(choiceId) : Promise.resolve(), - ]; - - // Data has been sent to server, update choice data. - return Promise.all(promises).then(() => { - return this.choiceProvider.getChoiceById(courseId, choiceId, siteId); - }).catch(() => { + // Data has been sent to server, prefetch choice if needed. + return this.prefetchAfterUpdate(module, courseId, undefined, siteId).catch(() => { // Ignore errors. }); } diff --git a/src/addon/mod/scorm/providers/scorm-sync.ts b/src/addon/mod/scorm/providers/scorm-sync.ts index 861dce5dc..683cfd450 100644 --- a/src/addon/mod/scorm/providers/scorm-sync.ts +++ b/src/addon/mod/scorm/providers/scorm-sync.ts @@ -25,7 +25,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'; import { AddonModScormOfflineProvider } from './scorm-offline'; import { AddonModScormPrefetchHandler } from './prefetch-handler'; @@ -57,7 +57,7 @@ export interface AddonModScormSyncResult { * Service to sync SCORMs. */ @Injectable() -export class AddonModScormSyncProvider extends CoreSyncBaseProvider { +export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_scorm_autom_synced'; @@ -67,12 +67,12 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, private scormProvider: AddonModScormProvider, private scormOfflineProvider: AddonModScormOfflineProvider, - private prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider, - private prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider, + prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider, private logHelper: CoreCourseLogHelperProvider) { super('AddonModScormSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('scorm'); } @@ -196,7 +196,7 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { if (updated) { // Update downloaded data. promise = this.courseProvider.getModuleBasicInfoByInstance(scorm.id, 'scorm', siteId).then((module) => { - return this.prefetchAfterUpdate(module, scorm.course); + return this.prefetchAfterUpdate(module, scorm.course, undefined, siteId); }).catch(() => { // Ignore errors. }); @@ -361,31 +361,6 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { }); } - /** - * Prefetch data after an update. It won't prefetch the data if the package file was updated. - * - * @param {any} module Module. - * @param {number} courseId Course ID. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when done. - */ - prefetchAfterUpdate(module: any, courseId: number, siteId?: string): Promise { - // Get the module updates to check if the package was updated or not. - return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { - - if (result && result.updates) { - // Only prefetch if the package file hasn't changed. - const fileChanged = !!result.updates.find((entry) => { - return entry.name == 'packagefiles'; - }); - - if (!fileChanged) { - return this.prefetchHandler.download(module, courseId); - } - } - }); - } - /** * Save a snapshot from a synchronization. * diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 2d1106973..9f62c72b4 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -46,9 +46,6 @@ export class CoreSyncBaseProvider { // Store sync promises. protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise } } = {}; - // List of services that will be injected using injector. - // It's done like this so subclasses don't have to send all the services to the parent in the constructor. - constructor(component: string, loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, diff --git a/src/core/course/classes/activity-sync.ts b/src/core/course/classes/activity-sync.ts new file mode 100644 index 000000000..2ccdb8552 --- /dev/null +++ b/src/core/course/classes/activity-sync.ts @@ -0,0 +1,67 @@ +// (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 { TranslateService } from '@ngx-translate/core'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler'; + +/** + * Base class to create activity sync providers. It provides some common functions. + */ +export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider { + + constructor(component: string, loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, + protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider, + protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, + protected timeUtils: CoreTimeUtilsProvider, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, + protected prefetchHandler: CoreCourseModulePrefetchHandlerBase) { + + super(component, loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); + } + + /** + * Conveniece function to refetch data after an update. + * + * @param {any} module Module. + * @param {number} courseId Course ID. + * @param {RegExp} [regex] RegExp to check if it should download data. Defaults to check files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdate(module: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$/; + + // Get the module updates to check if the data was updated or not. + return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { + + if (result && result.updates) { + // Only prefetch if files haven't changed. + const fileChanged = !!result.updates.find((entry) => { + return entry.match(regex); + }); + + if (!fileChanged) { + return this.prefetchHandler.download(module, courseId); + } + } + }); + } +} diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index 8bc986812..42727c13f 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -23,6 +23,7 @@ import { CoreCourseModuleMainComponent, CoreCourseModuleDelegate } from '@core/c import { CoreCourseSectionPage } from '@core/course/pages/section/section.ts'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonBlogProvider } from '@addon/blog/providers/blog'; +import { CoreConstants } from '@core/constants'; /** * Template class to easily create CoreCourseModuleMainComponent of resources (or activities without syncing). @@ -42,6 +43,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, description: string; // Module description. refreshIcon: string; // Refresh icon, normally spinner or refresh. prefetchStatusIcon: string; // Used when calling fillContextMenu. + prefetchStatus: string; // Used when calling fillContextMenu. prefetchText: string; // Used when calling fillContextMenu. size: string; // Used when calling fillContextMenu. @@ -222,6 +224,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.courseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); } + /** + * Check if the module is prefetched or being prefetched. To make it faster, just use the data calculated by fillContextMenu. + * This means that you need to call fillContextMenu to make this work. + */ + protected isPrefetched(): boolean { + return this.prefetchStatus != CoreConstants.NOT_DOWNLOADABLE && this.prefetchStatus != CoreConstants.NOT_DOWNLOADED; + } + /** * Expand the description. */ diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 6f05e157f..d9c7b621c 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -732,6 +732,7 @@ export class CoreCourseHelperProvider { return this.getModulePrefetchInfo(module, courseId, invalidateCache, component).then((moduleInfo) => { instance.size = moduleInfo.size > 0 ? moduleInfo.sizeReadable : 0; instance.prefetchStatusIcon = moduleInfo.statusIcon; + instance.prefetchStatus = moduleInfo.status; if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) { // Module is downloadable, get the text to display to prefetch.