MOBILE-2939 offline: Improve download confirm and error messages.
parent
b2d6e8df5e
commit
d3420d9be6
|
@ -23,6 +23,8 @@ export class CoreConstants {
|
||||||
static SECONDS_MINUTE = 60;
|
static SECONDS_MINUTE = 60;
|
||||||
static WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB.
|
static WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB.
|
||||||
static DOWNLOAD_THRESHOLD = 10485760; // 10MB.
|
static DOWNLOAD_THRESHOLD = 10485760; // 10MB.
|
||||||
|
static MINIMUM_FREE_SPACE = 10485760; // 10MB.
|
||||||
|
static IOS_FREE_SPACE_THRESHOLD = 524288000; // 500MB.
|
||||||
static DONT_SHOW_ERROR = 'CoreDontShowError';
|
static DONT_SHOW_ERROR = 'CoreDontShowError';
|
||||||
static NO_SITE_ID = 'NoSite';
|
static NO_SITE_ID = 'NoSite';
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,12 @@
|
||||||
"activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.",
|
"activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.",
|
||||||
"allsections": "All sections",
|
"allsections": "All sections",
|
||||||
"askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.",
|
"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?",
|
"confirmdeletemodulefiles": "Are you sure you want to delete these files?",
|
||||||
"confirmdownload": "You are about to download {{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. 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 <strong>at least</strong> {{size}}. Are you sure you want to continue?",
|
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
|
||||||
|
"confirmlimiteddownload": "You are not currently connected to WiFi. ",
|
||||||
"contents": "Contents",
|
"contents": "Contents",
|
||||||
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
|
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
|
||||||
"couldnotloadsections": "Could not load the sections. Please try again later.",
|
"couldnotloadsections": "Could not load the sections. Please try again later.",
|
||||||
|
@ -18,6 +20,8 @@
|
||||||
"errorgetmodule": "Error getting activity data.",
|
"errorgetmodule": "Error getting activity data.",
|
||||||
"hiddenfromstudents": "Hidden from students",
|
"hiddenfromstudents": "Hidden from students",
|
||||||
"hiddenoncoursepage": "Available but not shown on course page",
|
"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.",
|
"manualcompletionnotsynced": "Manual completion not synchronised.",
|
||||||
"nocontentavailable": "No content available at the moment.",
|
"nocontentavailable": "No content available at the moment.",
|
||||||
"overriddennotice": "Your final grade from this activity was manually adjusted.",
|
"overriddennotice": "Your final grade from this activity was manually adjusted.",
|
||||||
|
|
|
@ -1122,10 +1122,10 @@ export class CoreFilepoolProvider {
|
||||||
return Promise.all(promises).then(() => {
|
return Promise.all(promises).then(() => {
|
||||||
// Success prefetching, store package as downloaded.
|
// Success prefetching, store package as downloaded.
|
||||||
return this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra);
|
return this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra);
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
// Error downloading, go back to previous status and reject the promise.
|
// Error downloading, go back to previous status and reject the promise.
|
||||||
return this.setPackagePreviousStatus(siteId, component, componentId).then(() => {
|
return this.setPackagePreviousStatus(siteId, component, componentId).then(() => {
|
||||||
return Promise.reject(null);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2566,18 +2566,26 @@ export class CoreFilepoolProvider {
|
||||||
dropFromQueue = true;
|
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) {
|
if (dropFromQueue) {
|
||||||
this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject);
|
this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject);
|
||||||
|
|
||||||
return this.removeFromQueue(siteId, fileId).catch(() => {
|
return this.removeFromQueue(siteId, fileId).catch(() => {
|
||||||
// Consider this as a silent error, never reject the promise here.
|
// Consider this as a silent error, never reject the promise here.
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.treatQueueDeferred(siteId, fileId, false);
|
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
||||||
this.notifyFileDownloadError(siteId, fileId);
|
this.notifyFileDownloadError(siteId, fileId);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// We considered the file as legit but did not get it, failure.
|
// 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);
|
this.notifyFileDownloadError(siteId, fileId);
|
||||||
|
|
||||||
return Promise.reject(errorObject);
|
return Promise.reject(errorObject);
|
||||||
|
@ -2912,13 +2920,14 @@ export class CoreFilepoolProvider {
|
||||||
* @param {string} siteId The site ID.
|
* @param {string} siteId The site ID.
|
||||||
* @param {string} fileId The file ID.
|
* @param {string} fileId The file ID.
|
||||||
* @param {boolean} resolve True if promise should be resolved, false if it should be rejected.
|
* @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 (this.queueDeferreds[siteId] && this.queueDeferreds[siteId][fileId]) {
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
this.queueDeferreds[siteId][fileId].resolve();
|
this.queueDeferreds[siteId][fileId].resolve();
|
||||||
} else {
|
} else {
|
||||||
this.queueDeferreds[siteId][fileId].reject();
|
this.queueDeferreds[siteId][fileId].reject(error);
|
||||||
}
|
}
|
||||||
delete this.queueDeferreds[siteId][fileId];
|
delete this.queueDeferreds[siteId][fileId];
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { CoreTextUtilsProvider } from './text';
|
||||||
import { CoreAppProvider } from '../app';
|
import { CoreAppProvider } from '../app';
|
||||||
import { CoreConfigProvider } from '../config';
|
import { CoreConfigProvider } from '../config';
|
||||||
import { CoreUrlUtilsProvider } from './url';
|
import { CoreUrlUtilsProvider } from './url';
|
||||||
|
import { CoreFileProvider } from '@providers/file';
|
||||||
import { CoreConstants } from '@core/constants';
|
import { CoreConstants } from '@core/constants';
|
||||||
import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip';
|
import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip';
|
||||||
import { Md5 } from 'ts-md5/dist/md5';
|
import { Md5 } from 'ts-md5/dist/md5';
|
||||||
|
@ -66,7 +67,8 @@ export class CoreDomUtilsProvider {
|
||||||
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
|
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
|
||||||
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
|
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
|
||||||
private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider,
|
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.
|
// Check if debug messages should be displayed.
|
||||||
configProvider.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => {
|
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,
|
confirmDownloadSize(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number,
|
||||||
alwaysConfirm?: boolean): Promise<void> {
|
alwaysConfirm?: boolean): Promise<void> {
|
||||||
|
const readableSize = this.textUtils.bytesToSize(size.size, 2);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.translate.instant('core.course.availablespace', {available: availableSize});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return getAvailableSpace.then((availableSpace) => {
|
||||||
wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
|
wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
|
||||||
limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold;
|
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)) {
|
if (size.size < 0 || (size.size == 0 && !size.total)) {
|
||||||
// Seems size was unable to be calculated. Show a warning.
|
// Seems size was unable to be calculated. Show a warning.
|
||||||
unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize';
|
unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize';
|
||||||
|
|
||||||
return this.showConfirm(this.translate.instant(unknownMessage));
|
return this.showConfirm(wifiPrefix + this.translate.instant(unknownMessage, {availableSpace: availableSpace}));
|
||||||
} else if (!size.total) {
|
} else if (!size.total) {
|
||||||
// Filesize is only partial.
|
// Filesize is only partial.
|
||||||
const readableSize = this.textUtils.bytesToSize(size.size, 2);
|
|
||||||
|
|
||||||
return this.showConfirm(this.translate.instant('core.course.confirmpartialdownloadsize', { size: readableSize }));
|
return this.showConfirm(wifiPrefix + this.translate.instant('core.course.confirmpartialdownloadsize',
|
||||||
|
{ size: readableSize, availableSpace: availableSpace }));
|
||||||
} else if (alwaysConfirm || size.size >= wifiThreshold ||
|
} else if (alwaysConfirm || size.size >= wifiThreshold ||
|
||||||
(this.appProvider.isNetworkAccessLimited() && size.size >= limitedThreshold)) {
|
(this.appProvider.isNetworkAccessLimited() && size.size >= limitedThreshold)) {
|
||||||
message = message || 'core.course.confirmdownload';
|
message = message || 'core.course.confirmdownload';
|
||||||
const readableSize = this.textUtils.bytesToSize(size.size, 2);
|
|
||||||
|
|
||||||
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -113,7 +113,7 @@ export class CoreTextUtilsProvider {
|
||||||
*/
|
*/
|
||||||
bytesToSize(bytes: number, precision: number = 2): string {
|
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');
|
return this.translate.instant('core.notapplicable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue