From 7a718a727c060c007d25955b9b94bcf2a414fa66 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 7 Apr 2022 11:05:20 +0200 Subject: [PATCH 1/7] MOBILE-3833 core: Fix open module in course --- src/core/features/mainmenu/classes/deep-link-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/features/mainmenu/classes/deep-link-manager.ts b/src/core/features/mainmenu/classes/deep-link-manager.ts index 5ba0bfbfa..4f86078f5 100644 --- a/src/core/features/mainmenu/classes/deep-link-manager.ts +++ b/src/core/features/mainmenu/classes/deep-link-manager.ts @@ -78,7 +78,7 @@ export class CoreMainMenuDeepLinkManager { if (!params?.course) { CoreCourseHelper.getAndOpenCourse(Number(coursePathMatches[1]), params); } else { - CoreCourse.openCourse(params.course, params); + CoreCourse.openCourse(params.course, navOptions); } } else { CoreNavigator.navigateToSitePath(path, { From 4aeababbf67531988e069033eb9ca26400641500 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 7 Apr 2022 12:51:49 +0200 Subject: [PATCH 2/7] MOBILE-3833 audio: Allow capture audio in app in Android 10+ This was disabled before because there was a bug in the Cordova plugin, but it seems to be fixed now --- .../features/fileuploader/services/fileuploader-helper.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index d8cdfb14f..f0b2287e2 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -550,10 +550,8 @@ export class CoreFileUploaderHelperProvider { media = medias[0]; // We used limit 1, we only want 1 media. } catch (error) { - if (isAudio && this.isNoAppError(error) && CoreApp.isMobile() && - (!Platform.is('android') || CoreApp.getPlatformMajorVersion() < 10)) { + if (isAudio && this.isNoAppError(error) && CoreApp.isMobile()) { // No app to record audio, fallback to capture it ourselves. - // In Android it will only be done in Android 9 or lower because there's a bug in the plugin. try { media = await CoreFileUploader.captureAudioInApp(); } catch (error) { From 15b6e12b04d1b1ab16e2feda37108053d56a189e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 7 Apr 2022 13:05:18 +0200 Subject: [PATCH 3/7] MOBILE-3833 capture: Fix discard in-app audio in device --- .../components/capture-media/capture-media.ts | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/core/features/emulator/components/capture-media/capture-media.ts b/src/core/features/emulator/components/capture-media/capture-media.ts index 4d7fabd8e..2345ade66 100644 --- a/src/core/features/emulator/components/capture-media/capture-media.ts +++ b/src/core/features/emulator/components/capture-media/capture-media.ts @@ -130,7 +130,27 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { * @return Promise resolved when ready. */ protected async initCordovaMediaPlugin(): Promise { + + try { + await this.createFileAndMediaInstance(); + + this.readyToCapture = true; + this.previewMedia = this.previewAudio?.nativeElement; + } catch (error) { + this.dismissWithError(-1, error.message || error); + } + } + + /** + * Create a file entry and the cordova media instance. + */ + protected async createFileAndMediaInstance(): Promise { this.filePath = this.getFilePath(); + + // First create the file. + this.fileEntry = await CoreFile.createFile(this.filePath); + + // Now create the media instance. let absolutePath = CoreText.concatenatePaths(CoreFile.getBasePathInstant(), this.filePath); if (Platform.is('ios')) { @@ -138,17 +158,21 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { absolutePath = absolutePath.replace(/^file:\/\//, ''); } - try { - // First create the file. - this.fileEntry = await CoreFile.createFile(this.filePath); + this.mediaFile = Media.create(absolutePath); + } - // Now create the media instance. - this.mediaFile = Media.create(absolutePath); - this.readyToCapture = true; - this.previewMedia = this.previewAudio?.nativeElement; - } catch (error) { - this.dismissWithError(-1, error.message || error); + /** + * Reset the file and the cordova media instance. + */ + protected async resetCordovaMediaCapture(): Promise { + if (this.filePath) { + // Remove old file, don't block the user for this. + CoreFile.removeFile(this.filePath); } + + this.mediaFile?.release(); + + await this.createFileAndMediaInstance(); } /** @@ -410,11 +434,15 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { /** * Discard the captured media. */ - discard(): void { + async discard(): Promise { this.previewMedia?.pause(); this.streamVideo?.nativeElement.play(); this.audioDrawer?.start(); + if (this.isCordovaAudioCapture) { + await this.resetCordovaMediaCapture(); + } + this.hasCaptured = false; this.isCapturing = false; this.resetChrono = true; From ef405e4309419cf83646c5b9026a8e8f296b968c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 7 Apr 2022 13:28:08 +0200 Subject: [PATCH 4/7] MOBILE-3833 capture: Fix non-null assertions and types --- .../components/capture-media/capture-media.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/core/features/emulator/components/capture-media/capture-media.ts b/src/core/features/emulator/components/capture-media/capture-media.ts index 2345ade66..b226d5b61 100644 --- a/src/core/features/emulator/components/capture-media/capture-media.ts +++ b/src/core/features/emulator/components/capture-media/capture-media.ts @@ -229,7 +229,8 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { return; } - if (!this.streamVideo) { + const streamVideo = this.streamVideo; + if (!streamVideo) { throw new CoreError('Video element not found.'); } @@ -245,7 +246,7 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { }, 10000); // Listen for stream ready to display the stream. - this.streamVideo.nativeElement.onloadedmetadata = (): void => { + streamVideo.nativeElement.onloadedmetadata = (): void => { if (hasLoaded) { // Already loaded or timeout triggered, stop. return; @@ -254,19 +255,13 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { hasLoaded = true; clearTimeout(waitTimeout); this.readyToCapture = true; - this.streamVideo!.nativeElement.onloadedmetadata = null; + streamVideo.nativeElement.onloadedmetadata = null; // Force change detection. Angular doesn't detect these async operations. this.changeDetectorRef.detectChanges(); }; // Set the stream as the source of the video. - if ('srcObject' in this.streamVideo.nativeElement) { - this.streamVideo.nativeElement.srcObject = this.localMediaStream; - } else { - // Fallback for old browsers. - // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject#Examples - this.streamVideo.nativeElement.src = window.URL.createObjectURL(this.localMediaStream); - } + streamVideo.nativeElement.srcObject = this.localMediaStream; } catch (error) { this.dismissWithError(-1, error.message || error); } @@ -399,7 +394,7 @@ export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { this.imgCanvas.nativeElement.getContext('2d').drawImage(this.streamVideo?.nativeElement, 0, 0, width, height); // Convert the image to blob and show it in an image element. - this.imgCanvas.nativeElement.toBlob((blob) => { + this.imgCanvas.nativeElement.toBlob((blob: Blob) => { loadingModal.dismiss(); this.mediaBlob = blob; From 8c41be35207a2ae415438ea956e123b863772b1e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 7 Apr 2022 10:05:28 +0200 Subject: [PATCH 5/7] MOBILE-3833 core: Block logout when a form is active --- scripts/langindex.json | 1 + .../mainmenu/components/user-menu/user-menu.ts | 14 +++++++++++++- src/core/lang.json | 1 + src/core/services/navigator.ts | 9 +++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 93ec25362..809db3959 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1465,6 +1465,7 @@ "core.cannotconnecttrouble": "local_moodlemobileapp", "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", + "core.cannotlogoutpageblocks": "local_moodlemobileapp", "core.cannotopeninapp": "local_moodlemobileapp", "core.cannotopeninappdownload": "local_moodlemobileapp", "core.captureaudio": "local_moodlemobileapp", diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.ts b/src/core/features/mainmenu/components/user-menu/user-menu.ts index e3cda789c..c20d16bce 100644 --- a/src/core/features/mainmenu/components/user-menu/user-menu.ts +++ b/src/core/features/mainmenu/components/user-menu/user-menu.ts @@ -28,7 +28,7 @@ import { import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { ModalController } from '@singletons'; +import { ModalController, Translate } from '@singletons'; import { Subscription } from 'rxjs'; /** @@ -172,6 +172,12 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { * @param event Click event */ async logout(event: Event): Promise { + if (CoreNavigator.currentRouteCanBlockLeave()) { + await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks')); + + return; + } + if (this.removeAccountOnLogout) { // Ask confirm. const siteName = this.siteName ? @@ -200,6 +206,12 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { * @param event Click event */ async switchAccounts(event: Event): Promise { + if (CoreNavigator.currentRouteCanBlockLeave()) { + await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks')); + + return; + } + const thisModal = await ModalController.getTop(); event.preventDefault(); diff --git a/src/core/lang.json b/src/core/lang.json index 7429c930b..a3e73bb03 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -18,6 +18,7 @@ "cannotconnecttrouble": "We're having trouble connecting to your site.", "cannotconnectverify": "Please check the address is correct.", "cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", + "cannotlogoutpageblocks": "Please save or discard your changes before continuing.", "cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", "cannotopeninappdownload": "This file may not work as expected on this device. Would you like to download it anyway?", "captureaudio": "Record audio", diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index e2613e579..a33abca58 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -658,6 +658,15 @@ export class CoreNavigatorService { } } + /** + * Check if the current route page can block leaving the route. + * + * @return Whether the current route page can block leaving the route. + */ + currentRouteCanBlockLeave(): boolean { + return !!this.getCurrentRoute().snapshot.routeConfig?.canDeactivate?.length; + } + } export const CoreNavigator = makeSingleton(CoreNavigatorService); From d4695dd845f772ba1a67b660b29df6a2c76be97c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 8 Apr 2022 10:23:18 +0200 Subject: [PATCH 6/7] MOBILE-3833 course: Display loading in course index This improves performance in old deviced or big courses --- .../components/course-index/course-index.html | 127 +++++++++--------- .../components/course-index/course-index.ts | 9 ++ 2 files changed, 75 insertions(+), 61 deletions(-) diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html index 8de92a89b..7fca7f10e 100644 --- a/src/core/features/course/components/course-index/course-index.html +++ b/src/core/features/course/components/course-index/course-index.html @@ -11,74 +11,79 @@ - - - - -

- - -

-
-
- - - - - + + + +

- {{highlighted}} - - -
-
- - - - - - - - - - - -

- - -

-
- -
+ + + + + + +

+ + +

+
+ {{highlighted}} + + + +
+
+ + + + + + + + + + + +

+ + +

+
+ +
+
- -
+
+
-
-
+ +
diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index f2a66fdde..3e9ee1499 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -21,6 +21,7 @@ import { import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { CoreUtils } from '@services/utils/utils'; import { ModalController } from '@singletons'; import { CoreDom } from '@singletons/dom'; @@ -41,6 +42,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { allSectionId = CoreCourseProvider.ALL_SECTIONS_ID; highlighted?: string; sectionsToRender: CourseIndexSection[] = []; + loaded = false; constructor( protected elementRef: ElementRef, @@ -109,6 +111,13 @@ export class CoreCourseCourseIndexComponent implements OnInit { this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); + // Wait a bit to render the data, otherwise the modal takes a while to appear in big courses or slow devices. + await CoreUtils.wait(400); + + this.loaded = true; + + await CoreUtils.nextTick(); + CoreDom.scrollToElement( this.elementRef.nativeElement, '.item.item-current', From 0e74f735493f7fdb9eeb50af5311ce4724395665 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 8 Apr 2022 12:19:54 +0200 Subject: [PATCH 7/7] MOBILE-3833 core: Save public config in cache --- src/core/classes/site.ts | 83 ++++++++++++++++++- .../login/pages/reconnect/reconnect.ts | 6 +- .../features/login/services/handlers/cron.ts | 6 +- src/core/services/sites.ts | 4 +- 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 693830b19..3420106dc 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -41,7 +41,7 @@ import { CoreLogger } from '@singletons/logger'; import { Translate } from '@singletons'; import { CoreIonLoadingElement } from './ion-loading'; import { CoreLang } from '@services/lang'; -import { CoreSites } from '@services/sites'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CoreDatabaseTable } from './database/database-table'; import { CoreDatabaseCachingStrategy } from './database/database-table-proxy'; @@ -1389,9 +1389,88 @@ export class CoreSite { /** * Get the public config of this site. * + * @param options Options. * @return Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax. */ - async getPublicConfig(): Promise { + async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise { + if (!this.db) { + return this.requestPublicConfig(); + } + + const method = 'tool_mobile_get_public_config'; + const cacheId = this.getCacheId(method, {}); + const cachePreSets: CoreSiteWSPreSets = { + getFromCache: true, + saveToCache: true, + emergencyCache: true, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), + }; + + if (this.offlineDisabled) { + // Offline is disabled, don't use cache. + cachePreSets.getFromCache = false; + cachePreSets.saveToCache = false; + cachePreSets.emergencyCache = false; + } + + // Check for an ongoing identical request if we're not ignoring cache. + if (cachePreSets.getFromCache && this.ongoingRequests[cacheId]) { + const response = await this.ongoingRequests[cacheId]; + + return response; + } + + const promise = this.getFromCache(method, {}, cachePreSets, false).catch(async () => { + if (cachePreSets.forceOffline) { + // Don't call the WS, just fail. + throw new CoreError( + Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), + ); + } + + // Call the WS. + try { + const config = await this.requestPublicConfig(); + + if (cachePreSets.saveToCache) { + this.saveToCache(method, {}, config, cachePreSets); + } + + return config; + } catch (error) { + cachePreSets.omitExpires = true; + cachePreSets.getFromCache = true; + + try { + return await this.getFromCache(method, {}, cachePreSets, true); + } catch { + throw error; + } + } + }); + + this.ongoingRequests[cacheId] = promise; + + // Clear ongoing request after setting the promise (just in case it's already resolved). + try { + const response = await promise; + + // We pass back a clone of the original object, this may prevent errors if in the callback the object is modified. + return response; + } finally { + // Make sure we don't clear the promise of a newer request that ignores the cache. + if (this.ongoingRequests[cacheId] === promise) { + delete this.ongoingRequests[cacheId]; + } + } + } + + /** + * Perform a request to the server to get the public config of this site. + * + * @return Promise resolved with public config. + */ + protected async requestPublicConfig(): Promise { const preSets: CoreWSAjaxPreSets = { siteUrl: this.siteUrl, }; diff --git a/src/core/features/login/pages/reconnect/reconnect.ts b/src/core/features/login/pages/reconnect/reconnect.ts index f40c6fd05..c8a8acf37 100644 --- a/src/core/features/login/pages/reconnect/reconnect.ts +++ b/src/core/features/login/pages/reconnect/reconnect.ts @@ -16,7 +16,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CoreApp } from '@services/app'; -import { CoreSites } from '@services/sites'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreLoginHelper } from '@features/login/services/login-helper'; @@ -132,7 +132,9 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { * Get some data (like identity providers) from the site config. */ protected async checkSiteConfig(site: CoreSite): Promise { - this.siteConfig = await CoreUtils.ignoreErrors(site.getPublicConfig()); + this.siteConfig = await CoreUtils.ignoreErrors(site.getPublicConfig({ + readingStrategy: CoreSitesReadingStrategy.PREFER_NETWORK, + })); if (!this.siteConfig) { return; diff --git a/src/core/features/login/services/handlers/cron.ts b/src/core/features/login/services/handlers/cron.ts index 2add95869..3b6c53838 100644 --- a/src/core/features/login/services/handlers/cron.ts +++ b/src/core/features/login/services/handlers/cron.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreCronHandler } from '@services/cron'; -import { CoreSites } from '@services/sites'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -39,7 +39,9 @@ export class CoreLoginCronHandlerService implements CoreCronHandler { // Do not check twice in the same 10 minutes. const site = await CoreSites.getSite(siteId); - const config = await CoreUtils.ignoreErrors(site.getPublicConfig()); + const config = await CoreUtils.ignoreErrors(site.getPublicConfig({ + readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, + })); CoreUtils.ignoreErrors(CoreSites.checkApplication(config)); } diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 3666de2df..64c5af804 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -897,7 +897,9 @@ export class CoreSitesProvider { */ protected async getPublicConfigAndCheckApplication(site: CoreSite): Promise { try { - const config = await site.getPublicConfig(); + const config = await site.getPublicConfig({ + readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, + }); await this.checkApplication(config); } catch {