Merge pull request #1902 from marxjohnson/MOBILE-2939_integration

MOBILE-2939 offline: Improve download confirm and error messages.
main
Juan Leyva 2019-05-13 12:06:19 +02:00 committed by GitHub
commit 4eeb3de749
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 89 additions and 27 deletions

View File

@ -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';

View File

@ -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 <strong>at least</strong> {{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 <strong>at least</strong> {{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.",

View File

@ -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];
}

View File

@ -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<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;
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(this.translate.instant(unknownMessage));
return this.showConfirm(wifiPrefix + this.translate.instant(unknownMessage, {availableSpace: availableSpace}));
} else if (!size.total) {
// 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 ||
(this.appProvider.isNetworkAccessLimited() && size.size >= limitedThreshold)) {
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();
});
}
/**

View File

@ -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');
}