diff --git a/src/addon/mod/quiz/providers/prefetch-handler.ts b/src/addon/mod/quiz/providers/prefetch-handler.ts index 08b69cf66..2c932a59a 100644 --- a/src/addon/mod/quiz/providers/prefetch-handler.ts +++ b/src/addon/mod/quiz/providers/prefetch-handler.ts @@ -154,6 +154,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandler * @return {boolean|Promise} Whether the module can be downloaded. The promise should never be rejected. */ isDownloadable(module: any, courseId: number): boolean | Promise { + if (this.sitesProvider.getCurrentSite().isOfflineDisabled()) { + // Don't allow downloading the quiz if offline is disabled to prevent wasting a lot of data when opening it. + return false; + } + const siteId = this.sitesProvider.getCurrentSiteId(); return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quiz) => { diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts index 859510c25..95161dc5e 100644 --- a/src/addon/mod/quiz/providers/quiz.ts +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -1492,7 +1492,8 @@ export class AddonModQuizProvider { * @return {boolean} Whether offline is enabled. */ isQuizOffline(quiz: any): boolean { - return !!quiz.allowofflineattempts; + // Don't allow downloading the quiz if offline is disabled to prevent wasting a lot of data when opening it. + return !!quiz.allowofflineattempts && !this.sitesProvider.getCurrentSite().isOfflineDisabled(); } /** diff --git a/src/addon/remotethemes/remotethemes.module.ts b/src/addon/remotethemes/remotethemes.module.ts index 3f9d8e8b0..8b7b179ba 100644 --- a/src/addon/remotethemes/remotethemes.module.ts +++ b/src/addon/remotethemes/remotethemes.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { AddonRemoteThemesProvider } from './providers/remotethemes'; import { CoreEventsProvider } from '@providers/events'; import { CoreInitDelegate } from '@providers/init'; +import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; @NgModule({ @@ -29,7 +30,9 @@ import { CoreSitesProvider } from '@providers/sites'; }) export class AddonRemoteThemesModule { constructor(initDelegate: CoreInitDelegate, remoteThemesProvider: AddonRemoteThemesProvider, eventsProvider: CoreEventsProvider, - sitesProvider: CoreSitesProvider) { + sitesProvider: CoreSitesProvider, loggerProvider: CoreLoggerProvider) { + + const logger = loggerProvider.getInstance('AddonRemoteThemesModule'); // Preload the current site styles. initDelegate.registerProcess({ @@ -53,7 +56,9 @@ export class AddonRemoteThemesModule { eventsProvider.on(CoreEventsProvider.SITE_ADDED, (data) => { addingSite = data.siteId; - remoteThemesProvider.addSite(data.siteId).finally(() => { + remoteThemesProvider.addSite(data.siteId).catch((error) => { + logger.error('Error adding site', error); + }).then(() => { if (addingSite == data.siteId) { addingSite = false; } @@ -68,7 +73,9 @@ export class AddonRemoteThemesModule { // Update styles when current site is updated. eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (data) => { if (data.siteId === sitesProvider.getCurrentSiteId()) { - remoteThemesProvider.load(data.siteId); + remoteThemesProvider.load(data.siteId).catch((error) => { + logger.error('Error loading site after site update', error); + }); } }); diff --git a/src/classes/site.ts b/src/classes/site.ts index 90124a1e8..549e4dcb9 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -203,6 +203,7 @@ export class CoreSite { protected db: SQLiteDB; protected cleanUnicode = false; protected lastAutoLogin = 0; + protected offlineDisabled = false; /** * Create a site. @@ -234,6 +235,7 @@ export class CoreSite { this.wsProvider = injector.get(CoreWSProvider); this.logger = logger.getInstance('CoreWSProvider'); + this.calculateOfflineDisabled(); if (this.id) { this.initDB(); @@ -376,6 +378,7 @@ export class CoreSite { setConfig(config: any): void { config.tool_mobile_disabledfeatures = this.textUtils.treatDisabledFeatures(config.tool_mobile_disabledfeatures); this.config = config; + this.calculateOfflineDisabled(); } /** @@ -530,6 +533,10 @@ export class CoreSite { const initialToken = this.token; data = data || {}; + if (!this.appProvider.isOnline() && this.offlineDisabled) { + return Promise.reject(this.wsProvider.createFakeWSError('core.errorofflinedisabled', true)); + } + // Check if the method is available, use a prefixed version if possible. // We ignore this check when we do not have the site info, as the list of functions is not loaded yet. if (this.getInfo() && !this.wsAvailable(method, false)) { @@ -560,6 +567,13 @@ export class CoreSite { wsPreSets.cleanUnicode = false; } + if (this.offlineDisabled) { + // Offline is disabled, don't use cache. + preSets.getFromCache = false; + preSets.saveToCache = false; + preSets.emergencyCache = false; + } + // Enable text filtering by default. data.moodlewssettingfilter = preSets.filter === false ? false : true; data.moodlewssettingfileurl = preSets.rewriteurls === false ? false : true; @@ -1351,6 +1365,22 @@ export class CoreSite { return !!disabledFeatures.match(regEx); } + /** + * Calculate if offline is disabled in the site. + */ + calculateOfflineDisabled(): void { + this.offlineDisabled = this.isFeatureDisabled('NoDelegate_CoreOffline'); + } + + /** + * Get whether offline is disabled in the site. + * + * @return {boolean} Whether it's disabled. + */ + isOfflineDisabled(): boolean { + return this.offlineDisabled; + } + /** * Check if the site version is greater than one or several versions. * This function accepts a string or an array of strings. If array, the last version must be the highest. diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 16a95b37b..496249479 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -13,7 +13,7 @@ - + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 27f983028..d5fcd1ad9 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -52,6 +52,7 @@ export class CoreCourseSectionPage implements OnDestroy { prefetchCourseIcon: 'spinner', title: 'core.course.downloadcourse' }; + downloadCourseEnabled: boolean; moduleId: number; displayEnableDownload: boolean; displayRefresher: boolean; @@ -75,6 +76,7 @@ export class CoreCourseSectionPage implements OnDestroy { // Get the title to display. We dont't have sections yet. this.title = courseFormatDelegate.getCourseTitle(this.course); this.displayEnableDownload = courseFormatDelegate.displayEnableDownload(this.course); + this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite(); this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => { if (data && data.courseId == this.course.id) { @@ -82,12 +84,14 @@ export class CoreCourseSectionPage implements OnDestroy { } }); - // Listen for changes in course status. - this.courseStatusObserver = eventsProvider.on(CoreEventsProvider.COURSE_STATUS_CHANGED, (data) => { - if (data.courseId == this.course.id) { - this.updateCourseStatus(data.status); - } - }, sitesProvider.getCurrentSiteId()); + if (this.downloadCourseEnabled) { + // Listen for changes in course status. + this.courseStatusObserver = eventsProvider.on(CoreEventsProvider.COURSE_STATUS_CHANGED, (data) => { + if (data.courseId == this.course.id) { + this.updateCourseStatus(data.status); + } + }, sitesProvider.getCurrentSiteId()); + } } /** @@ -103,6 +107,11 @@ export class CoreCourseSectionPage implements OnDestroy { this.loadData().finally(() => { this.dataLoaded = true; + if (!this.downloadCourseEnabled) { + // Cannot download the whole course, stop. + return; + } + // Determine the course prefetch status. this.determineCoursePrefetchIcon().then(() => { if (this.prefetchCourseData.prefetchCourseIcon == 'spinner') { diff --git a/src/core/courses/components/course-progress/course-progress.html b/src/core/courses/components/course-progress/course-progress.html index 2560cc0a8..73002477a 100644 --- a/src/core/courses/components/course-progress/course-progress.html +++ b/src/core/courses/components/course-progress/course-progress.html @@ -2,7 +2,7 @@

-
+
- + diff --git a/src/core/courses/pages/my-courses/my-courses.ts b/src/core/courses/pages/my-courses/my-courses.ts index f59d1051b..da0bd3dae 100644 --- a/src/core/courses/pages/my-courses/my-courses.ts +++ b/src/core/courses/pages/my-courses/my-courses.ts @@ -37,6 +37,7 @@ export class CoreCoursesMyCoursesPage implements OnDestroy { showFilter = false; coursesLoaded = false; prefetchCoursesData: any = {}; + downloadAllCoursesEnabled: boolean; protected prefetchIconInitialized = false; protected myCoursesObserver; @@ -53,6 +54,7 @@ export class CoreCoursesMyCoursesPage implements OnDestroy { */ ionViewDidLoad(): void { this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); this.fetchCourses().finally(() => { this.coursesLoaded = true; @@ -62,8 +64,17 @@ export class CoreCoursesMyCoursesPage implements OnDestroy { this.fetchCourses(); }, this.sitesProvider.getCurrentSiteId()); + // Refresh the enabled flags if site is updated. this.siteUpdatedObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { + const wasEnabled = this.downloadAllCoursesEnabled; + this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); + + if (!wasEnabled && this.downloadAllCoursesEnabled && this.coursesLoaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcon(); + } }, this.sitesProvider.getCurrentSiteId()); } @@ -176,7 +187,7 @@ export class CoreCoursesMyCoursesPage implements OnDestroy { * Initialize the prefetch icon for the list of courses. */ protected initPrefetchCoursesIcon(): void { - if (this.prefetchIconInitialized) { + if (this.prefetchIconInitialized || !this.downloadAllCoursesEnabled) { // Already initialized. return; } diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index a6a5e8481..8771c672a 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -71,7 +71,7 @@ {{ 'core.courses.past' | translate }} -
+
diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index b24c5b184..956267823 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -14,6 +14,7 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; @@ -64,20 +65,38 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { past: {}, future: {} }; + downloadAllCoursesEnabled: boolean; protected prefetchIconsInitialized = false; protected isDestroyed; + protected updateSiteObserver; constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider, private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider, - private siteHomeProvider: CoreSiteHomeProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate) { } + private siteHomeProvider: CoreSiteHomeProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, + private eventsProvider: CoreEventsProvider) { + } /** * View loaded. */ ionViewDidLoad(): void { this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); + + // Refresh the enabled flags if site is updated. + this.updateSiteObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { + const wasEnabled = this.downloadAllCoursesEnabled; + + this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); + + if (!wasEnabled && this.downloadAllCoursesEnabled && this.courses.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + }); // Decide which tab to load first. this.siteHomeProvider.isAvailable().then((enabled) => { @@ -378,7 +397,7 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { * Initialize the prefetch icon for selected courses. */ protected initPrefetchCoursesIcons(): void { - if (this.prefetchIconsInitialized) { + if (this.prefetchIconsInitialized || !this.downloadAllCoursesEnabled) { // Already initialized. return; } @@ -410,5 +429,6 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { */ ngOnDestroy(): void { this.isDestroyed = true; + this.updateSiteObserver && this.updateSiteObserver.off(); } } diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index 84a8ba485..8f825e8a3 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -120,6 +120,54 @@ export class CoreCoursesProvider { }); } + /** + * Check if download a whole course 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. + */ + isDownloadCourseDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isDownloadCoursesDisabledInSite(site); + }); + } + + /** + * Check if download a whole course is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isDownloadCourseDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('NoDelegate_CoreCourseDownload'); + } + + /** + * Check if download all courses 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. + */ + isDownloadCoursesDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isDownloadCoursesDisabledInSite(site); + }); + } + + /** + * Check if download all courses is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isDownloadCoursesDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('NoDelegate_CoreCoursesDownload'); + } + /** * Check if My Courses is disabled in a certain site. * diff --git a/src/lang/en.json b/src/lang/en.json index 0a5d19c35..9a60cd453 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -77,6 +77,7 @@ "errorinvalidform": "The form contains invalid data. Please make sure to fill all required fields and that the data is valid.", "errorinvalidresponse": "Invalid response received. Please contact your Moodle site administrator if the error persists.", "errorloadingcontent": "Error loading content.", + "errorofflinedisabled": "Offline browsing is disabled on your site. You need to be connected to the internet to use the app.", "erroropenfilenoapp": "Error opening the file: no app found to open this kind of file.", "erroropenfilenoextension": "Error opening the file: the file doesn't have extension.", "erroropenpopup": "This activity is trying to open a popup. This is not supported in this app.",