diff --git a/src/core/constants.ts b/src/core/constants.ts
index 2fc8bfc79..ea038eece 100644
--- a/src/core/constants.ts
+++ b/src/core/constants.ts
@@ -23,6 +23,8 @@ export class CoreConstants {
static SECONDS_MINUTE = 60;
static WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB.
static DOWNLOAD_THRESHOLD = 10485760; // 10MB.
+ static MINIMUM_FREE_SPACE = 10485760; // 10MB.
+ static IOS_FREE_SPACE_THRESHOLD = 524288000; // 500MB.
static DONT_SHOW_ERROR = 'CoreDontShowError';
static NO_SITE_ID = 'NoSite';
diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json
index 0a2229b22..314f3a81c 100644
--- a/src/core/course/lang/en.json
+++ b/src/core/course/lang/en.json
@@ -4,10 +4,12 @@
"activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.",
"allsections": "All sections",
"askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.",
+ "availablespace": " You currently have about {{available}} free space.",
"confirmdeletemodulefiles": "Are you sure you want to delete these files?",
- "confirmdownload": "You are about to download {{size}}. Are you sure you want to continue?",
- "confirmdownloadunknownsize": "It was not possible to calculate the size of the download. Are you sure you want to continue?",
- "confirmpartialdownloadsize": "You are about to download at least {{size}}. Are you sure you want to continue?",
+ "confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?",
+ "confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?",
+ "confirmpartialdownloadsize": "You are about to download at least {{size}}.{{availableSpace}} Are you sure you want to continue?",
+ "confirmlimiteddownload": "You are not currently connected to WiFi. ",
"contents": "Contents",
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
"couldnotloadsections": "Could not load the sections. Please try again later.",
@@ -18,6 +20,8 @@
"errorgetmodule": "Error getting activity data.",
"hiddenfromstudents": "Hidden from students",
"hiddenoncoursepage": "Available but not shown on course page",
+ "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
+ "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
"manualcompletionnotsynced": "Manual completion not synchronised.",
"nocontentavailable": "No content available at the moment.",
"overriddennotice": "Your final grade from this activity was manually adjusted.",
diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts
index d148c7f72..7b3725174 100644
--- a/src/providers/filepool.ts
+++ b/src/providers/filepool.ts
@@ -1122,10 +1122,10 @@ export class CoreFilepoolProvider {
return Promise.all(promises).then(() => {
// Success prefetching, store package as downloaded.
return this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra);
- }).catch(() => {
+ }).catch((error) => {
// Error downloading, go back to previous status and reject the promise.
return this.setPackagePreviousStatus(siteId, component, componentId).then(() => {
- return Promise.reject(null);
+ return Promise.reject(error);
});
});
@@ -2566,18 +2566,26 @@ export class CoreFilepoolProvider {
dropFromQueue = true;
}
+ let errorMessage = null;
+ // Some Android devices restrict the amount of usable storage using quotas.
+ // If this quota would be exceeded by the download, it throws an exception.
+ // We catch this exception here, and report a meaningful error message to the user.
+ if (errorObject instanceof FileTransferError && errorObject.exception.includes('EDQUOT')) {
+ errorMessage = 'core.course.insufficientavailablequota';
+ }
+
if (dropFromQueue) {
this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject);
return this.removeFromQueue(siteId, fileId).catch(() => {
// Consider this as a silent error, never reject the promise here.
}).then(() => {
- this.treatQueueDeferred(siteId, fileId, false);
+ this.treatQueueDeferred(siteId, fileId, false, errorMessage);
this.notifyFileDownloadError(siteId, fileId);
});
} else {
// We considered the file as legit but did not get it, failure.
- this.treatQueueDeferred(siteId, fileId, false);
+ this.treatQueueDeferred(siteId, fileId, false, errorMessage);
this.notifyFileDownloadError(siteId, fileId);
return Promise.reject(errorObject);
@@ -2912,13 +2920,14 @@ export class CoreFilepoolProvider {
* @param {string} siteId The site ID.
* @param {string} fileId The file ID.
* @param {boolean} resolve True if promise should be resolved, false if it should be rejected.
+ * @param {string} error String identifier for error message, if rejected.
*/
- protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean): void {
+ protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean, error?: string): void {
if (this.queueDeferreds[siteId] && this.queueDeferreds[siteId][fileId]) {
if (resolve) {
this.queueDeferreds[siteId][fileId].resolve();
} else {
- this.queueDeferreds[siteId][fileId].reject();
+ this.queueDeferreds[siteId][fileId].reject(error);
}
delete this.queueDeferreds[siteId][fileId];
}
diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts
index 912b0dc49..1af9102c2 100644
--- a/src/providers/utils/dom.ts
+++ b/src/providers/utils/dom.ts
@@ -23,6 +23,7 @@ import { CoreTextUtilsProvider } from './text';
import { CoreAppProvider } from '../app';
import { CoreConfigProvider } from '../config';
import { CoreUrlUtilsProvider } from './url';
+import { CoreFileProvider } from '@providers/file';
import { CoreConstants } from '@core/constants';
import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip';
import { Md5 } from 'ts-md5/dist/md5';
@@ -66,7 +67,8 @@ export class CoreDomUtilsProvider {
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider,
- private modalCtrl: ModalController, private sanitizer: DomSanitizer, private popoverCtrl: PopoverController) {
+ private modalCtrl: ModalController, private sanitizer: DomSanitizer, private popoverCtrl: PopoverController,
+ private fileProvider: CoreFileProvider) {
// Check if debug messages should be displayed.
configProvider.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => {
@@ -128,28 +130,73 @@ export class CoreDomUtilsProvider {
*/
confirmDownloadSize(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number,
alwaysConfirm?: boolean): Promise {
- wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
- limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold;
+ const readableSize = this.textUtils.bytesToSize(size.size, 2);
- if (size.size < 0 || (size.size == 0 && !size.total)) {
- // Seems size was unable to be calculated. Show a warning.
- unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize';
+ const getAvailableBytes = new Promise((resolve): void => {
+ if (this.appProvider.isDesktop()) {
+ // Free space calculation is not supported on desktop.
+ resolve(null);
+ } else {
+ this.fileProvider.calculateFreeSpace().then((availableBytes) => {
+ if (this.platform.is('android')) {
+ return availableBytes;
+ } else {
+ // Space calculation is not accurate on iOS, but it gets more accurate when space is lower.
+ // We'll only use it when space is <500MB, or we're downloading more than twice the reported space.
+ if (availableBytes < CoreConstants.IOS_FREE_SPACE_THRESHOLD || size.size > availableBytes / 2) {
+ return availableBytes;
+ } else {
+ return null;
+ }
+ }
+ }).then((availableBytes) => {
+ resolve(availableBytes);
+ });
+ }
+ });
- return this.showConfirm(this.translate.instant(unknownMessage));
- } else if (!size.total) {
- // Filesize is only partial.
- const readableSize = this.textUtils.bytesToSize(size.size, 2);
+ const getAvailableSpace = getAvailableBytes.then((availableBytes: number) => {
+ if (availableBytes === null) {
+ return '';
+ } else {
+ const availableSize = this.textUtils.bytesToSize(availableBytes, 2);
+ if (this.platform.is('android') && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) {
+ return Promise.reject(this.translate.instant('core.course.insufficientavailablespace', { size: readableSize }));
+ }
- return this.showConfirm(this.translate.instant('core.course.confirmpartialdownloadsize', { size: readableSize }));
- } else if (alwaysConfirm || size.size >= wifiThreshold ||
+ return this.translate.instant('core.course.availablespace', {available: availableSize});
+ }
+ });
+
+ return getAvailableSpace.then((availableSpace) => {
+ wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
+ limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold;
+
+ let wifiPrefix = '';
+ if (this.appProvider.isNetworkAccessLimited()) {
+ wifiPrefix = this.translate.instant('core.course.confirmlimiteddownload');
+ }
+
+ if (size.size < 0 || (size.size == 0 && !size.total)) {
+ // Seems size was unable to be calculated. Show a warning.
+ unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize';
+
+ return this.showConfirm(wifiPrefix + this.translate.instant(unknownMessage, {availableSpace: availableSpace}));
+ } else if (!size.total) {
+ // Filesize is only partial.
+
+ return this.showConfirm(wifiPrefix + this.translate.instant('core.course.confirmpartialdownloadsize',
+ { size: readableSize, availableSpace: availableSpace }));
+ } else if (alwaysConfirm || size.size >= wifiThreshold ||
(this.appProvider.isNetworkAccessLimited() && size.size >= limitedThreshold)) {
- message = message || 'core.course.confirmdownload';
- const readableSize = this.textUtils.bytesToSize(size.size, 2);
+ message = message || 'core.course.confirmdownload';
- return this.showConfirm(this.translate.instant(message, { size: readableSize }));
- }
+ return this.showConfirm(wifiPrefix + this.translate.instant(message,
+ { size: readableSize, availableSpace: availableSpace }));
+ }
- return Promise.resolve();
+ return Promise.resolve();
+ });
}
/**
diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts
index 5bdb70ca4..7c30d9ec9 100644
--- a/src/providers/utils/text.ts
+++ b/src/providers/utils/text.ts
@@ -113,7 +113,7 @@ export class CoreTextUtilsProvider {
*/
bytesToSize(bytes: number, precision: number = 2): string {
- if (typeof bytes == 'undefined' || bytes < 0) {
+ if (typeof bytes == 'undefined' || bytes === null || bytes < 0) {
return this.translate.instant('core.notapplicable');
}