当前位置: 首页 > news >正文

Android (Kotlin) 高版本 DownloadManager 封装工具类,支持 APK 断点续传与自动安装

以下是一个针对 Android 高版本的 DownloadManager 封装工具类,支持 断点续传自动安装 APK 功能。该工具类兼容 Android 10 及以上版本的文件存储策略,并适配了 FileProvider 和未知来源应用安装权限。


工具类:DownloadUtils

import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.FileProvider
import java.io.File

class DownloadUtils private constructor(context: Context) {

    companion object {
        private const val TAG = "DownloadUtils"
        private var instance: DownloadUtils? = null

        /**
         * 获取单例实例
         */
        fun getInstance(context: Context): DownloadUtils {
            return instance ?: synchronized(this) {
                instance ?: DownloadUtils(context.applicationContext).also { instance = it }
            }
        }
    }

    private val context: Context = context.applicationContext
    private val downloadManager: DownloadManager =
        context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    private var downloadId: Long = -1
    private var progressListener: DownloadProgressListener? = null
    private var downloadObserver: DownloadObserver? = null

    /**
     * 下载文件
     *
     * @param url 文件下载地址
     * @param fileName 保存的文件名
     * @param listener 下载进度监听器
     */
    fun downloadFile(url: String, fileName: String, listener: DownloadProgressListener) {
        this.progressListener = listener

        // 创建下载请求
        val request = DownloadManager.Request(Uri.parse(url)).apply {
            setTitle("文件下载")
            setDescription("正在下载文件...")
            setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)

            // 设置下载路径
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                // Android 10 及以上版本,使用应用专属目录
                setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName)
            } else {
                // Android 10 以下版本,使用公共下载目录
                setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
            }

            // 支持断点续传
            setAllowedOverMetered(true) // 允许使用移动网络
            setAllowedOverRoaming(true) // 允许漫游时下载
        }

        // 开始下载
        downloadId = downloadManager.enqueue(request)

        // 注册下载完成监听
        context.registerReceiver(downloadCompleteReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))

        // 注册下载进度监听
        if (progressListener != null) {
            downloadObserver = DownloadObserver(Handler(Looper.getMainLooper()), downloadId)
            context.contentResolver.registerContentObserver(
                Uri.parse("content://downloads/my_downloads"), true, downloadObserver!!
            )
        }
    }

    /**
     * 安装 APK 文件
     */
    private fun installApk(context: Context) {
        val apkFile = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10 及以上版本,使用应用专属目录
            File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app-update.apk")
        } else {
            // Android 10 以下版本,使用公共下载目录
            File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "app-update.apk")
        }

        if (!apkFile.exists()) {
            Log.e(TAG, "APK 文件不存在")
            return
        }

        // 使用 FileProvider 获取文件的 Uri
        val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile)

        // 创建安装 Intent
        val installIntent = Intent(Intent.ACTION_VIEW).apply {
            setDataAndType(apkUri, "application/vnd.android.package-archive")
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

            // 适配 Android 7.0 及以上版本
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }

            // 适配 Android 8.0 及以上版本,允许安装未知来源的应用
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                if (!context.packageManager.canRequestPackageInstalls()) {
                    // 跳转到设置页面,允许安装未知来源应用
                    val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
                        data = Uri.parse("package:${context.packageName}")
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    }
                    context.startActivity(intent)
                    return
                }
            }
        }

        context.startActivity(installIntent)

        // 注销广播接收器和内容观察者
        context.unregisterReceiver(downloadCompleteReceiver)
        downloadObserver?.let {
            context.contentResolver.unregisterContentObserver(it)
        }
    }

    /**
     * 下载完成广播接收器
     */
    private val downloadCompleteReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
            if (id == downloadId) {
                progressListener?.onDownloadComplete()
                installApk(context)
            }
        }
    }

    /**
     * 下载进度观察者
     */
    private inner class DownloadObserver(handler: Handler, private val downloadId: Long) : ContentObserver(handler) {
        override fun onChange(selfChange: Boolean) {
            super.onChange(selfChange)
            queryDownloadProgress()
        }

        private fun queryDownloadProgress() {
            val query = DownloadManager.Query().apply {
                setFilterById(downloadId)
            }

            context.contentResolver.query(
                Uri.parse("content://downloads/my_downloads"),
                null,
                null,
                null,
                null
            )?.use { cursor ->
                if (cursor.moveToFirst()) {
                    val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
                    val bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
                    val bytesTotal = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))

                    when (status) {
                        DownloadManager.STATUS_RUNNING -> {
                            if (bytesTotal > 0) {
                                val progress = ((bytesDownloaded * 100L) / bytesTotal).toInt()
                                progressListener?.onProgress(progress)
                            }
                        }
                        DownloadManager.STATUS_FAILED -> {
                            progressListener?.onError("下载失败")
                        }
                    }
                }
            }
        }
    }

    /**
     * 下载进度监听器
     */
    interface DownloadProgressListener {
        fun onProgress(progress: Int) // 下载进度(0-100)
        fun onError(message: String)  // 下载失败
        fun onDownloadComplete()      // 下载完成
    }
}

主要功能

  1. 断点续传

    • 支持网络中断后继续下载。
    • 通过 DownloadManagersetAllowedOverMeteredsetAllowedOverRoaming 方法实现。
  2. 自动安装 APK

    • 下载完成后自动触发安装流程。
    • 适配 Android 7.0 及以上版本的 FileProvider
    • 处理 Android 8.0 及以上版本的未知来源应用安装权限。
  3. 下载进度监听

    • 通过 ContentObserver 监听下载进度,实时回调进度值。
  4. 高版本兼容

    • 适配 Android 10 及以上版本的文件存储策略,使用应用专属目录。

使用方法

1. 初始化并下载文件

val downloadUtils = DownloadUtils.getInstance(context)
downloadUtils.downloadFile(
    "https://example.com/file.apk",
    "app-update.apk",
    object : DownloadUtils.DownloadProgressListener {
        override fun onProgress(progress: Int) {
            Log.d(TAG, "下载进度: $progress%")
        }

        override fun onError(message: String) {
            Log.e(TAG, "下载失败: $message")
        }

        override fun onDownloadComplete() {
            Log.d(TAG, "下载完成")
        }
    }
)

2. 配置 FileProvider

AndroidManifest.xml 中添加以下配置:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

res/xml/file_paths.xml 中定义文件路径:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="downloads" path="Download/" />
</paths>

3. 添加权限

AndroidManifest.xml 中添加以下权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

注意事项

  1. 存储权限

    • 在 Android 10 及以上版本中,使用应用专属目录无需申请 WRITE_EXTERNAL_STORAGE 权限。
    • 在 Android 10 以下版本中,需要动态申请 WRITE_EXTERNAL_STORAGE 权限。
  2. 未知来源应用安装

    • 在 Android 8.0 及以上版本中,需要引导用户开启“允许安装未知来源应用”权限。
  3. 文件路径

    • 确保 file_paths.xml 中定义的路径与代码中的路径一致。

通过以上工具类,你可以轻松实现 APK 下载、断点续传和自动安装功能,同时兼容 Android 高版本的文件存储策略和权限要求。

相关文章:

  • Python基于深度学习的多模态人脸情绪识别研究与实现
  • DeepSeek使用指南
  • 什么是物理信息神经网络PINN
  • LeetCode hot 100 每日一题(8)——438. 找到字符串中所有字母异位词
  • p5.js:绘制各种内置的几何体,还能旋转
  • 设计模式分类解析与JavaScript实现
  • Linux Redis安装部署、注册服务
  • 蓝桥杯专项复习——stl(stack、queue)
  • hadoop伪分布式搭建--启动过程中如果发现某个datanode出现问题,如何处理?
  • 24.策略模式实现日志
  • leetcode日记(101)填充每个节点的下一个右侧节点指针Ⅱ
  • Deepseek+QuickAPI:打造 MySQL AI 智能体入门篇(一)
  • CVE-2017-5645(使用 docker 搭建)
  • Java面试:集合框架体系
  • 【web逆向】优某愿 字体混淆
  • 提升fcp
  • 八、Prometheus 静态配置(Static Configuration)
  • 仿“东方甄选”直播商城小程序运营平台
  • Git的基本指令
  • 使用爬虫获取自定义API操作API接口
  • 三星“七天机”质保期内屏幕漏液被拒保,澎湃介入后已解决
  • 家国万里·时光故事会|科学家伉俪,用玉米书写家国情怀
  • 持续降雨存在落石风险,贵州黄果树景区水帘洞将封闭至6月初
  • AG600“鲲龙”批生产首架机完成生产试飞
  • 林诗栋/蒯曼混双取胜,国乒赢得多哈世乒赛开门红
  • 首次公布!我国空间站内发现微生物新物种