Android DownloadManager、备份文件和取消的下载

Android DownloadManager, backup files and canceled downloads

在我的应用程序中,我实现了一种通过 DownloadManager 下载数据文件的机制。当用户开始下载时,旁边会出现一个取消按钮,允许用户取消正在进行的下载。

这些数据文件会在服务器上定期更新,因此用户会不时地再次下载相同的文件。文件名在更新过程中保持稳定。

由于用户可能在下载过程中随时点击取消,我想保留旧版本直到下载成功完成。为此,我重命名了现有文件,然后才开始下载。如果用户取消下载(或者由于某种原因下载失败),我想将备份文件恢复到原来的位置。

对于取消的情况,我最初在点击取消按钮时将以下代码添加到运行:

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

当我刷新文件时,旧文件在下载开始前被重命名。但是,我取消下载后,部分文件和备份都没有了。

由于我已经使用 FileObserver 来监控下载进度,我将其扩展为还可以监控文件删除并生成日志消息。在 logcat 中,我看到同一文件有两个删除事件,这表明部分下载的文件被删除,备份被重命名,然后重命名的备份也被删除。

很公平,我想,显然 DownloadManager 会在后台负责删除,所以我需要注意它的发生。所以我修改了上面的事件处理程序,只将文件路径存储在一个列表中,而不做任何文件操作。然后我修改了 FileObserver 以将所有已删除的文件与列表进行比较:如果匹配,则重命名备份文件。此外,我为每个操作添加了日志输出。

但是,事件的顺序实际上仍然是相同的:现在部分下载的文件被下载管理器删除,触发我的 FileObserver,这将反过来重命名备份文件。之后,备份文件被删除。

在我看来,下载管理器似乎过于热心:当下载被取消时,它会删除下载的文件,然后检查它是否真的消失了,如果它仍然在该路径中找到文件,则重试删除。

我怎样才能解决这个问题并防止下载管理器删除它没有下载的文件?

我最终解决了多重删除问题,方法是利用 Android 下载管理器永远不会覆盖现有文件这一事实,自行将下载目标重命名为仍然可用的名称。

再次下载文件时,我不会费心将旧文件移开。下载管理器将检测到文件已经存在,并为下载选择一个不同的名称。下载成功完成后,我删除旧文件并重命名新文件。

唯一的挑战是确定下载管理器选择的文件名,因为 Android 似乎对此没有任何明确的通知。下载开始时没有意图被触发,因此我不得不再次求助于 FileObserver

观看 FileObserver.CREATE 似乎是最直接的方式。但是,当我此时查询下载列表时,查询将 return 本地路径的空值。

因此我求助于 FileObserver.MODIFY,它会在每次修改文件时触发。我已经用它来显示下载进度了,此时必须有一个本地文件。第一次为下载管理器重命名的文件触发此事件时,我将获得一个尚未在我的列表中的文件名。然后我 运行 下面的代码:

        // 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();

每个正在进行的下载都由一个 DownloadInfo 实例描述,其中包括两个文件名的引用。我将它们保存在三个 Map 中:

  • downloadsByReference 使用下载管理器的 ID 作为密钥
  • downloadsByFile使用本地文件作为key
  • downloadsByUri 使用 URI 作为键

下载完成后,我在 downloadsByReference 中查找其 ID 以获取两个文件名。如果它们不同,我删除旧文件,然后重命名新文件。