Search code examples
androidandroid-download-manager

Android DownloadManager, backup files and canceled downloads


In my app, I am implementing a mechanism to download data files via DownloadManager. When users start a download, a Cancel button appears next to it, allowing the user to cancel a download in progress.

These data files periodically get updated on the server, so users will want to download the same file again from time to time. File names stay stable across updates.

Since the user may hit Cancel any time during the download, I want to keep the old version around until the download has completed successfully. To this end, I rename the existing file and only then initiate the download. If the user cancels the download (also if the download fails for some reason), I want to restore the backup file to its original location.

For the Cancel case, I originally added the following code to run when the Cancel button is clicked:

if (downloadManager.remove(reference) > 0) {
    if (destFile.exists())
        destFile.delete();
    backupFile.renameTo(destFile);
}

When I refresh a file, the old file gets renamed before the download starts. However, after I cancel the download, both the partial file and the backup are gone.

Since I already use a FileObserver to monitor download progress, I extended it to also watch for file deletion and generate a log message. In the logcat I see two deletion events for the same file, which indicates the partially downloaded file gets deleted, the backup gets renamed and then the renamed backup gets deleted as well.

Fair enough, I thought, apparently DownloadManager takes care of deletion in the background, so I need to watch for that to happen. So I modified the above event handler to just store the file path in a list and not do any file operations just yet. I then modified my FileObserver to compare all deleted files to the list: if it matches, rename the backup file. Additionally, I added log output for each operation.

However, the sequence of events is still effectively the same: now the partially downloaded file gets deleted by the download manager, triggering my FileObserver, which will in turn rename the backup file. After that, the backup file gets deleted.

It looks to me as if the download manager is overzealous: when a download is canceled, it deletes the downloaded file, then checks if it is really gone and retries the deletion if it still finds a file in that path.

How can I get around this and prevent the download manager from deleting files it didn't download?


Solution

  • I ended up working around the multiple delete issue by taking advantage of the fact that the Android download manager will never overwrite an existing file, renaming download targets on its own to a name which is still available.

    When a file is being downloaded again, I don't bother moving the old file out of the way. The download manager will detect that a file already exists, and pick a different name for the download. When the download finishes successfully, I delete the old file and rename the new one.

    The only challenge was to determine the file name the download manager has chosen, as Android seems to lack any explicit notification for this. No intent is fired when the download starts, thus I had to resort to the FileObserver again.

    Watching for FileObserver.CREATE seemed like the most straightforward way. However, when I query the list of downloads at this point, the query will return a null value for the local path.

    I therefore resorted to FileObserver.MODIFY, which fires on every modification to a file. I already use it to display download progress, and at this point there has to be a local file. The first time this event fires for a file which got renamed by the download manager, I will get a file name which is not yet in my list. I then run the following code:

            // File file: the file being downloaded
            // DownloadInfo info: information about a download in progress
            /* First progress report for a renamed file */
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterByStatus(~(DownloadManager.STATUS_FAILED | DownloadManager.STATUS_SUCCESSFUL));
            Cursor cursor = downloadManager.query(query);
            if (!cursor.moveToFirst()) {
                cursor.close();
                return;
            }
            do {
                Long reference = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
                String path = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
                if (file.equals(new File(path))) {
                    info = downloadsByReference.get(reference);
                    if (info != null) {
                        info.downloadFile = file;
                        downloadsByFile.put(info.downloadFile, info);
                    }
                }
            } while (cursor.moveToNext());
            cursor.close();
    

    Each download in progress is described by a DownloadInfo instance which, among others, has a reference both file names. I keep them in three Maps:

    • downloadsByReference uses the download manager's IDs as a key
    • downloadsByFile uses the local file as a key
    • downloadsByUri uses the URI as a key

    When a download finishes, I look up its ID in downloadsByReference to get the two file names. If they differ, I delete the old file, then rename the new one.