DownloadManager 是 Android 2.3(API 级别 9)引入的系统级服务,专门用于处理长时间运行的下载操作。它提供了一个健壮的后台下载机制,能够自动处理 HTTP 连接、在连接更改或系统重启后恢复下载,并管理重试逻辑。

博主博客

核心特性

  • 后台服务:在系统后台执行下载,应用无需保持活跃状态
  • 自动恢复:处理网络中断、连接变更和系统重启后的下载恢复
  • 通知集成:自动显示下载进度通知
  • 下载队列:支持多个下载任务的排队管理
  • 内容提供者接口:通过 ContentProvider 暴露下载状态信息

获取 DownloadManager 实例

// 方式一:使用 Class 参数(推荐,类型安全)
DownloadManager downloadManager = getSystemService(DownloadManager.class);

// 方式二:使用 String 参数
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);

权限声明

AndroidManifest.xml 中添加必要权限:

<!-- 必需权限:网络访问 -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- Android 10 之前需要的外部存储权限 -->
<!-- 注意:从 Android 10 (API 29) 开始,Scoped Storage 限制了对外部存储的直接访问 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" 
    android:maxSdkVersion="28" />

注意:从 Android 6.0 (API 23) 开始,需要在运行时请求危险权限(如 WRITE_EXTERNAL_STORAGE)。

核心类与方法

1. DownloadManager.Request

包含下载请求的配置信息。

// 创建下载请求
Uri uri = Uri.parse("https://example.com/file.apk");
DownloadManager.Request request = new DownloadManager.Request(uri);

// 设置下载路径(Android 10+ 注意存储权限限制)
request.setDestinationInExternalPublicDir(
    Environment.DIRECTORY_DOWNLOADS, 
    "myapp.apk"
);

// 可选配置
request.setTitle("应用更新")                    // 通知标题
      .setDescription("正在下载最新版本")        // 通知描述
      .setNotificationVisibility(              // 通知可见性
          DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
      .setAllowedNetworkTypes(                // 允许的网络类型
          DownloadManager.Request.NETWORK_WIFI | 
          DownloadManager.Request.NETWORK_MOBILE)
      .setAllowedOverRoaming(false)           // 是否允许漫游时下载
      .setVisibleInDownloadsUi(true)          // 在系统下载UI中显示
      .setMimeType("application/vnd.android.package-archive"); // MIME类型

2. DownloadManager.Query

用于查询下载状态和信息。

3. 主要方法

  • enqueue(Request request):将下载请求加入队列,返回唯一的下载ID
  • query(Query query):查询下载信息
  • remove(long... ids):删除下载记录并取消下载
  • openDownloadedFile(long id):打开已下载的文件
  • getUriForDownloadedFile(long id):获取已下载文件的Uri

完整使用示例

1. 初始化下载

public class DownloadHelper {
    private DownloadManager downloadManager;
    private Context context;
    
    public DownloadHelper(Context context) {
        this.context = context.getApplicationContext();
        this.downloadManager = (DownloadManager) 
            context.getSystemService(Context.DOWNLOAD_SERVICE);
    }
    
    public long startDownload(String url, String fileName) {
        // 检查下载管理器是否可用
        if (!isDownloadManagerEnabled(context)) {
            showDownloadManagerDisabledDialog();
            return -1;
        }
        
        Uri uri = Uri.parse(url);
        DownloadManager.Request request = new DownloadManager.Request(uri);
        
        // 设置下载路径
        request.setDestinationInExternalPublicDir(
            Environment.DIRECTORY_DOWNLOADS, 
            fileName
        );
        
        // 配置请求
        configureRequest(request, fileName);
        
        // 加入下载队列
        return downloadManager.enqueue(request);
    }
    
    private void configureRequest(DownloadManager.Request request, String fileName) {
        request.setTitle(fileName)
              .setDescription("文件下载中...")
              .setNotificationVisibility(
                  DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
              .setAllowedNetworkTypes(
                  DownloadManager.Request.NETWORK_WIFI | 
                  DownloadManager.Request.NETWORK_MOBILE)
              .setAllowedOverRoaming(false);
        
        // 根据文件类型设置MIME类型
        if (fileName.endsWith(".apk")) {
            request.setMimeType("application/vnd.android.package-archive");
        }
    }
}

2. 监听下载进度

DownloadManager 不提供直接的进度回调,但可以通过 ContentObserver 监听数据库变化:

public class DownloadProgressObserver {
    private static final Uri CONTENT_URI = 
        Uri.parse("content://downloads/my_downloads");
    
    private Context context;
    private DownloadManager downloadManager;
    private ContentObserver contentObserver;
    private Handler handler;
    private long downloadId;
    
    public DownloadProgressObserver(Context context, long downloadId) {
        this.context = context;
        this.downloadId = downloadId;
        this.downloadManager = (DownloadManager) 
            context.getSystemService(Context.DOWNLOAD_SERVICE);
        this.handler = new Handler(Looper.getMainLooper());
        
        initContentObserver();
    }
    
    private void initContentObserver() {
        contentObserver = new ContentObserver(handler) {
            @Override
            public void onChange(boolean selfChange) {
                super.onChange(selfChange);
                queryDownloadProgress();
            }
        };
        
        context.getContentResolver().registerContentObserver(
            CONTENT_URI, 
            true, 
            contentObserver
        );
    }
    
    private void queryDownloadProgress() {
        DownloadManager.Query query = new DownloadManager.Query()
            .setFilterById(downloadId);
        
        try (Cursor cursor = downloadManager.query(query)) {
            if (cursor != null && cursor.moveToFirst()) {
                int status = cursor.getInt(cursor.getColumnIndexOrThrow(
                    DownloadManager.COLUMN_STATUS));
                long downloaded = cursor.getLong(cursor.getColumnIndexOrThrow(
                    DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                long total = cursor.getLong(cursor.getColumnIndexOrThrow(
                    DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                
                // 更新UI
                updateProgress(status, downloaded, total);
            }
        }
    }
    
    private void updateProgress(int status, long downloaded, long total) {
        switch (status) {
            case DownloadManager.STATUS_RUNNING:
                // 下载中,更新进度
                int progress = (total > 0) ? 
                    (int) ((downloaded * 100) / total) : 0;
                // 发送进度到UI线程
                break;
            case DownloadManager.STATUS_SUCCESSFUL:
                // 下载完成
                break;
            case DownloadManager.STATUS_FAILED:
                // 下载失败
                break;
        }
    }
    
    public void unregisterObserver() {
        if (contentObserver != null) {
            context.getContentResolver().unregisterContentObserver(contentObserver);
        }
    }
}

替代方案:定时查询(适用于快速下载或避免频繁回调)

public class ScheduledProgressChecker {
    private ScheduledExecutorService executor = 
        Executors.newScheduledThreadPool(1);
    private Runnable checkTask;
    
    public void startChecking(long downloadId, long intervalSeconds) {
        checkTask = () -> queryDownloadProgress(downloadId);
        executor.scheduleAtFixedRate(
            checkTask, 
            0, 
            intervalSeconds, 
            TimeUnit.SECONDS
        );
    }
    
    public void stopChecking() {
        if (executor != null && !executor.isShutdown()) {
            executor.shutdown();
        }
    }
}

3. 监听下载完成

public class DownloadCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
            long downloadId = intent.getLongExtra(
                DownloadManager.EXTRA_DOWNLOAD_ID, -1);
            
            if (downloadId != -1) {
                handleDownloadComplete(context, downloadId);
            }
        }
    }
    
    private void handleDownloadComplete(Context context, long downloadId) {
        DownloadManager dm = (DownloadManager) 
            context.getSystemService(Context.DOWNLOAD_SERVICE);
        DownloadManager.Query query = new DownloadManager.Query()
            .setFilterById(downloadId);
        
        try (Cursor cursor = dm.query(query)) {
            if (cursor != null && cursor.moveToFirst()) {
                int status = cursor.getInt(cursor.getColumnIndex(
                    DownloadManager.COLUMN_STATUS));
                
                if (status == DownloadManager.STATUS_SUCCESSFUL) {
                    // 下载成功
                    String uriString = cursor.getString(cursor.getColumnIndex(
                        DownloadManager.COLUMN_LOCAL_URI));
                    // 处理下载的文件
                } else if (status == DownloadManager.STATUS_FAILED) {
                    // 下载失败
                    int reason = cursor.getInt(cursor.getColumnIndex(
                        DownloadManager.COLUMN_REASON));
                    // 处理失败原因
                }
            }
        }
    }
}

// 在Activity/Fragment中注册
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
    IntentFilter filter = new IntentFilter(
        DownloadManager.ACTION_DOWNLOAD_COMPLETE);
    
    // 注意:Android 8.0+ 需要显式注册
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        registerReceiver(receiver, filter, 
            Context.RECEIVER_EXPORTED);
    } else {
        registerReceiver(receiver, filter);
    }
}

4. 处理通知栏点击

public class NotificationClickReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.getAction())) {
            // 获取点击的下载ID数组
            long[] ids = intent.getLongArrayExtra(
                DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
            
            // 打开下载管理界面或执行自定义操作
            Intent viewDownloads = new Intent(
                DownloadManager.ACTION_VIEW_DOWNLOADS);
            viewDownloads.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(viewDownloads);
        }
    }
}

工作原理解析

DownloadManager 内部流程

  1. enqueue() 方法调用
public long enqueue(Request request) {
    ContentValues values = request.toContentValues(mPackageName);
    Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
    return Long.parseLong(downloadUri.getLastPathSegment());
}
  1. 数据流
    • 将 Request 转换为 ContentValues
    • 通过 ContentResolver 插入到 DownloadProvider
    • 启动 DownloadService 处理下载任务
    • DownloadThread 执行实际下载(使用 HttpURLConnection)

注意:从 Android N (API 24) 开始,实现方式有所变化,但对外接口保持一致。

常见问题与解决方案

1. DownloadManager 不可用或禁用

/**
 * 检查下载管理器是否可用
 */
public static boolean isDownloadManagerEnabled(Context context) {
    int state = context.getPackageManager()
        .getApplicationEnabledSetting("com.android.providers.downloads");
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
        return !(state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
                state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER ||
                state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
    } else {
        return !(state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
                state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER);
    }
}

/**
 * 引导用户启用下载管理器
 */
public static void enableDownloadManager(Context context) {
    try {
        // 打开下载管理器的应用详情页
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(Uri.parse("package:com.android.providers.downloads"));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    } catch (ActivityNotFoundException e) {
        // 回退到应用管理页面
        Intent intent = new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
}

2. Android 10+ 存储权限问题

// Android 10+ 推荐将文件下载到应用私有目录
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    // 不需要 WRITE_EXTERNAL_STORAGE 权限
    request.setDestinationInExternalFilesDir(
        context,
        Environment.DIRECTORY_DOWNLOADS,
        fileName
    );
} else {
    // Android 9 及以下
    request.setDestinationInExternalPublicDir(
        Environment.DIRECTORY_DOWNLOADS,
        fileName
    );
}

3. 断点续传机制

DownloadManager 通过以下方式实现断点续传:

  • 监听网络连接变化广播(ConnectivityManager.CONNECTIVITY_ACTION
  • 网络恢复时重启 DownloadService
  • DownloadThread 支持 Range 请求,从断点处继续下载

最佳实践

  1. 权限处理

    • 动态请求运行时权限(Android 6.0+)
    • 处理权限拒绝情况
  2. 错误处理

    • 检查网络状态
    • 处理存储空间不足
    • 监控下载失败原因
  3. 状态管理

    • 保存下载ID以便恢复状态
    • 清理已完成或取消的下载记录
  4. 用户体验

    • 提供取消下载选项
    • 显示清晰的进度和状态
    • 处理安装/打开下载的文件

参考资源