javascriptandroidcordovaxmlhttprequestfetch-api

How can I prevent Android downloads from failing?


I am using Cordova 12 to modify an existing Android application in order to try and make it compatible with Android API 34 (Android 14). The app must download thousands of small images before being usable, as the point of the app is to be used when totally offline. Therefore, linking directly to online images is not possible.

The problem is that some of the files will be saved to disk with a size of 0 byte. The specific files vary between runs. There is no error during the download or file saving process, ot at least the error callback is not triggered. I have tried adding a delay between file downloads, but it's not clear if it helped or not.

Here is the FileUtility class, that is used for downloading and saving the files to disk

import ErrorLog from "./error-log.cls";
const { log } = ErrorLog;
const { logError } = ErrorLog;

export default class FileUtility {
    static DEFAULT_ERROR_MESSAGE = 'UNKNOWN_ERROR';
    static ErrorCodesStrings = {
        1: 'NOT_FOUND_ERR',
        2: 'SECURITY_ERR',
        3: 'ABORT_ERR',
        4: 'NOT_READABLE_ERR',
        5: 'ENCODING_ERR',
        6: 'NO_MODIFICATION_ALLOWED_ERR',
        7: 'INVALID_STATE_ERR',
        8: 'SYNTAX_ERR',
        9: 'INVALID_MODIFICATION_ERR',
        10: 'QUOTA_EXCEEDED_ERR',
        11: 'TYPE_MISMATCH_ERR',
        12: 'PATH_EXISTS_ERR',
    };

    constructor() {}

    static getErrorText(errorCode) {
        if(errorCode && Number.isInteger(errorCode) && errorCode > 1 && errorCode < 13) {
            return FileUtility.ErrorCodesStrings[errorCode] + ` (code ${errorCode})`;
        }

        return FileUtility.DEFAULT_ERROR_MESSAGE + ` (code ${errorCode})`;
    }

    static createDirectory(directoriesToCreate, callback, onError) {
        const directoriesChain = directoriesToCreate.trim('/').split('/');

        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (fileSystem) => {
            const currentPathArray = [];
            let currentDirectoryCount = 0;
            directoriesChain.forEach(currentDirectory => {
                currentDirectoryCount++;
                currentPathArray.push(currentDirectory);
                fileSystem.root.getDirectory(currentPathArray.join('/'), { create: true }, (directoryEntry) => {
                    if(callback && currentDirectoryCount == directoriesChain.length) { callback(directoryEntry); }
                }, onError);
            });
        }, onError);
    }

    static createFile(filename, content, callback, onError) {
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (fileSystem) => {
            fileSystem.root.getFile(filename, { create: true, exclusive: false }, (fileEntry) => {
                fileEntry.createWriter((fileWriter) => {
                    fileWriter.onwriteend = () => {
                        if (callback) { callback(fileEntry) };
                    };
                    fileWriter.onerror = (fileWriterError) => {
                        if (onError) {
                            onError(fileWriterError);
                        } else {
                            logError(`Could not create file ${filename}: ${FileUtility.getErrorText(fileWriterError.code)}.`);
                        }
                    };
                    fileWriter.write(content);
                });
            }, (fileCreationError) => {
                if (onError) {
                    onError(fileCreationError);
                } else {
                    logError(`Could not create ${filename}: ${FileUtility.getErrorText(fileCreationError.code)}.`);
                }
            });
        }, (fileSystemError) => {
            if (onError) {
                onError(fileSystemError);
            }else {
                logError(`downloadFile: Could not get the file system access required to create file ${filename}: ${FileUtility.getErrorText(fileSystemError.code)}.`);
            }
        });
    }

    static downloadFile(sourceURI, destinationPath, onComplete, onError) {
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (fileSystem) => {
            fileSystem.root.getFile(destinationPath, { create: true, exclusive: false }, (dataFile) => {
                /* Using XMLHttpRequest gives the same end result.
                const fileRequest = new XMLHttpRequest();
                fileRequest.open("GET", encodeURI(sourceURI), true);
                fileRequest.responseType = 'blob';
                fileRequest.onload = (loadEvent) => {
                    const responseData = fileRequest.response;

                    if(responseData) {
                        dataFile.createWriter((fileWriter) => {
                            fileWriter.onwriteend = (fileWriteEvent) => {
                                log(`downloadFile: File ${dataFile.fullPath} was downloaded and saved.`);
                                if(onComplete) { onComplete(dataFile); }
                            };

                            fileWriter.onerror = (downloadError) => {
                                logError(`downloadFile: Could not download file ${sourceURI} to ${dataFile.fullPath}: ${FileUtility.getErrorText(downloadError.code)}.`);
                                if(onError) { onError(downloadError); }
                            };

                            fileWriter.write(responseData);
                        });
                    }
                };
                fileRequest.onerror = (downloadError) => {
                    logError(`downloadFile: Could not download file ${sourceURI} to ${dataFile.fullPath}: ${FileUtility.getErrorText(downloadError.code)}.`);
                    if(onError) { onError(downloadError); }
                }
                fileRequest.send(null);
                */
                fetch(encodeURI(sourceURI), {
                    method: "get",
                }).then(response => {
                    if(response.ok) {
                        response.blob().then((blobData) => {
                            dataFile.createWriter((fileWriter) => {
                                fileWriter.onwriteend = (fileWriteEvent) => {
                                    log(`downloadFile: File ${dataFile.fullPath} was downloaded and saved.`);
                                    if(onComplete) { onComplete(dataFile); }
                                };

                                fileWriter.onerror = (downloadError) => {
                                    logError(`downloadFile: Could not download file ${sourceURI} to ${dataFile.fullPath}: ${FileUtility.getErrorText(downloadError.code)}.`);
                                    if(onError) { onError(downloadError); }
                                };

                                fileWriter.write(blobData);
                            });
                        }).catch((responseError) => {
                            logError(`downloadFile: Could not download file ${sourceURI}: ${responseError.message}.`);
                        });
                    } else {
                        logError(`downloadFile: Could not download file ${sourceURI}: ${response.status}.`);
                    }
                }).catch(responseError => {
                    logError(`downloadFile: Could not download file ${sourceURI}: ${responseError.message}.`);
                });
            }, (fileCreationError) => {
                logError(`downloadFile: Could not create the file ${destinationPath}: ${FileUtility.getErrorText(fileCreationError.code)}.`);
                if(onError) { onError(fileCreationError); }
            });
        }, (fileSystemError) => {
            logError(`downloadFile: Could not get the file system access required to create file ${destinationPath}: ${FileUtility.getErrorText(fileSystemError.code)}.`);
            if(onError) { onError(fileSystemError); }
        });
    }

    static readTextFile(filePath, onRead, onError) {
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (fileSystem) => {
            fileSystem.root.getFile(filePath, { create: true, exclusive: false }, (fileToRead) => {
                fileToRead.file((fileEntry) => {
                    const reader = new FileReader();
                    reader.onloadend = () => { if (onRead) onRead(reader.result); };
                    reader.readAsText(fileEntry);
                }, (fileReadError) => {
                    logError(`readTextFile: Could not read file ${filePath} (${JSON.stringify(fileReadError)}).`);
                    if(onError) { onError(fileReadError); }
                });
            });
        }, (fileReadError) => {
            logError(`readTextFile: Could not read file ${filePath} (${JSON.stringify(fileReadError)}).`);
            if(onError) { onError(fileReadError); }
        });
    }

    static deleteFile(filePath, onSuccess, onError) {
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (fileSystem) => {
            fileSystem.root.getFile(filePath, { create: false, exclusive: false }, (fileToDelete) => {
                fileToDelete.remove((fileDeleted) => {
                    log(`The file ${filePath} was successfully deleted.`, ErrorLog.ERROR_LEVEL.WARNING);
                    if(onSuccess) { onSuccess(fileDeleted); }
                }, (fileDeletionError) => {
                    logError(`deleteFile: Could not deleted file ${filePath}: ${FileUtility.getErrorText(fileDeletionError.code)}).`);
                    if(onError) { onError(fileDeletionError); }
                });
            }, (fileGetError) => {
                logError(`deleteFile: Could not deleted file ${filePath}: ${FileUtility.getErrorText(fileGetError.code)}).`);
                if(onError) { onError(fileGetError); }
            });
        }, (fileSystemError) => {
            logError(`deleteFile: Could not deleted file ${filePath}: ${FileUtility.getErrorText(fileSystemError.code)}).`);
            if(onError) { onError(fileSystemError); }
        });
    }

    static readDirectory(directoryPath, onSuccess, onError) {
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (fileSystem) => {
            fileSystem.root.getDirectory(directoryPath, { create: false, exclusive: false }, (directoryEntry) => {
                onSuccess(directoryEntry);
            }, (readDirectoryError) => {
                logError(`readDirectory: Could not read directory ${directoryPath}: ${FileUtility.getErrorText(readDirectoryError.code)}.`);
                if(onError) { onError(readDirectoryError); }
            })
        }, (fileSystemError) => {
            logError(`readDirectory: Could not get the file system access to read directory ${directoryPath}: ${FileUtility.getErrorText(fileSystemError.code)}.`);
            if(onError) { onError(fileSystemError); }
        });
    }
};

Here is the part of the code responsible for downloading the images:

/*
    this.listToDownload is an array of objects with the following structure:
        path: the remote path of the image on the server, which should also be used, after some tweaking, as a local path.
        state: the string "missing", "ok", "update" or "delete", in order to know what to do with the image.
        version: the version of the image, in order to check if that specific image must be downloaded or if it's already up to date.
*/

export default class ApplicationSynchronisation {
    [...]

    downloadNextElement() {
        if(this.nextToDownload) {
            // Display the progress percentage on the main window.
            this.onProgress(this._('updating'), parseFloat((1 - (this.listToDownload.length / this.listOriginalSize)).toFixed(4)));
            const action = this.nextToDownload.state != "delete" ? 'download' : 'delete';
            let path = this.nextToDownload.path.replace(/{lang}/gi, this.language);
            path = path.startsWith('./') ? path.substr(2) : path;
            path = path.lastIndexOf('|') != -1 ? path.substring(0, path.lastIndexOf('|')) : path;
            let localPath = path;

            if(localPath.endsWith('.json')) {
                localPath = localPath.replace(`/${this.language}/`, '/');
            }

            if(localPath.startsWith('dist/')) {
                localPath = localPath.substr(5);
            }

            if(this.listToDownload.length) {
                this.nextToDownload = this.listToDownload.pop();
            } else {
                delete this.nextToDownload;
            }

            if(action == 'download') {
                FileUtility.createDirectory(localPath.substring(0, localPath.lastIndexOf('/')), () => {
                    FileUtility.downloadFile(this.application.remoteUrl + path, localPath, () => this.downloadNextElement(), (downloadError) => {
                        logError(`Error while downloading file ${this.application.remoteUrl + path} to local path ${localPath}: ${FileUtility.getErrorText(downloadError.code)}.`);
                        setTimeout(() => this.downloadNextElement, 250);
                    });
                }, (directoryCreationError) => {
                    logError(`Could not create the following complete path: ${localPath.substring(0, localPath.lastIndexOf('/'))}: ${FileUtility.getErrorText(directoryCreationError.code)}`);
                    this.downloadNextElement();
                });
            } else {
                FileUtility.deleteFile(localPath, (() => this.downloadNextElement), (deleteError) => {
                    logError(`Error while deleting ${localPath}: ${FileUtility.getErrorText(deleteError.code)}.`);
                    this.downloadNextElement();
                });
            }
        } else {
            this.onComplete();
        }
    }

    onProgress(textToShow, percentCompletion) {
        log(`${textToShow} ${(percentCompletion * 100).toFixed(2)}%`);
        if(this.application.onSyncProgress) { this.application.onSyncProgress(textToShow, percentCompletion) };
    }

    onComplete() {
        if(! this.synchronised) {
            this.synchronised = true;
            log("App synchronised.");
            this.onProgress(this._('uptodate'), 1);
            // Process sync callback and switch to main app.
            [...]
        }
    }

    [...]
}

As mentioned in the code, I have tried using the fetch API or XMLHttpRequest. Both give the same result. Adding a delay (through setTimeout) seems to help, although I have not yet formerly calculated the percentage of files that are empty with different delays so I cannot say for sure.

How can I change the download process to ensure that all the files are properly downloaded? Should I just put back on the stack any file that is downloaded and that is 0 byte, even if don't know exactly why it was saved as en empty file?


Solution

  • So, as mentioned by @mplungjan, it turns out that my problem was linked to an error in the code. Instead of using (() => this.downloadNextElement) or (() => this.downloadNextElement), I should have used () => setTimeout(() => this.downloadNextElement(), 250). I even reduced the delay between downloads, without any issue. So the code ends up being:

    [...]
                FileUtility.downloadFile(this.application.remoteUrl + path, localPath, () => setTimeout(() => this.downloadNextElement(), 100), (downloadError) => {
                    logError(`Error while downloading file ${this.application.remoteUrl + path} to local path ${localPath}: ${FileUtility.getErrorText(downloadError.code)}.`);
                    setTimeout(() => this.downloadNextElement(), 100);
                });
    [...]
        } else {
            FileUtility.deleteFile(localPath, () => setTimeout(() => this.downloadNextElement(), 100), (deleteError) => {
                logError(`Error while deleting ${localPath}: ${FileUtility.getErrorText(deleteError.code)}.`);
                setTimeout(() => this.downloadNextElement(), 100);
            });
        }
    [...]