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

news2025/3/18 0:56:06

以下是一个针对 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 高版本的文件存储策略和权限要求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2316895.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1.6、Java继承、构造方法、数组

子类可以增加字段、增加方法或覆盖父类方法&#xff0c;但继承不会删除任何字段和方法不恰当认为super 同 this 引用是类似的概念&#xff0c;其实super不是一个对象的引用&#xff0c;不能将值super赋给另一个对象变量&#xff0c;super只是一个指示 编译器调用父类方法的特殊…

通义万相 2.1 与蓝耘智算平台的深度协同,挖掘 AIGC 无限潜力并释放巨大未来价值

我的个人主页 我的专栏&#xff1a; 人工智能领域、java-数据结构、Javase、C语言&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01; 点赞&#x1f44d;收藏❤ 引言&#xff1a;AIGC 浪潮下的新机遇 在当今数字化飞速发展的时代&#xff0c;人工智能生成内容&…

专题|Python贝叶斯金融数据应用实例合集:随机波动率SV模型、逻辑回归、参数更新、绩效比较BEST分析亚马逊股票、普尔指数...

原文链接&#xff1a;https://tecdat.cn/?p41020 本专题合集系统梳理了贝叶斯方法在金融数据分析与分类建模中的前沿应用。合集聚焦于PyMC3概率编程框架&#xff0c;深度探讨了共轭先验参数更新、贝叶斯逻辑回归、贝叶斯夏普比率等核心算法在实际场景中的落地实践&#xff08;…

Linux调度器 --- 负载均衡的存在的问题

文章目录 前言一、简介二、Linux 调度器2.1 在单核系统上&#xff0c;CFS 非常简单2.2 在多核系统上&#xff0c;CFS 变得非常复杂2.2.1 负载均衡算法2.2.2 优化措施 三、Linux调度器负载均衡的存在的问题3.1 组负载不均衡问题&#xff08;Group Imbalance Bug&#xff09;3.2 …

从零开始用AI开发游戏(三)背景故事

《迷域回响》背景故事 第一章&#xff1a;失落的符文纪元 在远古的“艾瑟兰”大陆&#xff0c;掌握空间魔法的「筑界者文明」曾建造了连通万界的回响迷宫——这座迷宫既是试炼场&#xff0c;也是囚笼。文明巅峰时期&#xff0c;筑界者将禁忌知识刻入虚空符文&#xff0c;嵌于…

IXTUR气控永磁铁:以高精度气控和稳定磁场,为机器人应用提供稳定抓取力

在现代工业生产和物流领域&#xff0c;物料的抓取与搬运是影响生产效率和成本控制的重要环节。传统夹爪在面对不同材质、形状和重量的物体时&#xff0c;常常存在适应性差、抓取不稳定、操作复杂等问题&#xff0c;导致生产流程中频繁出现停机调整&#xff0c;增加了人工干预成…

硬件驱动——51单片机:寄存器、LED、动态数码管

目录 一、51单片机 1.寄存器 二、LED点灯 1.原理 2.封装函数 3.顺序点灯 4.特定位点灯 三、动态数码管 1.原理 2.封装函数 3.0~9跳变 4.顺序移位0~9跳变 一、51单片机 1.寄存器 51单片机共40个引脚&#xff0c;其中P0,P1,P2,P3是四个有8引脚的寄存器&#xff0…

2025 香港 Web3 嘉年华:全球 Web3 生态的年度盛会

自 2023 年首届香港 Web3 嘉年华成功举办以来&#xff0c;这一盛会已成为全球 Web3 领域规模最大、影响力最深远的行业活动之一。2025 年 4 月 6 日至 9 日&#xff0c;第三届香港 Web3 嘉年华将在香港盛大举行。本届活动由万向区块链实验室与 HashKey Group 联合主办、W3ME 承…

【MySQL】多表查询(笛卡尔积现象,联合查询、内连接、左外连接、右外连接、子查询)-通过练习快速掌握法

在DQL的基础查询中&#xff0c;我们已经学过了多表查询的一种&#xff1a;联合查询&#xff08;union&#xff09;。本文我们将系统的讲解多表查询。 笛卡尔积现象 首先&#xff0c;我们想要查询emp表和stu表两个表&#xff0c;按照我们之前的知识栈&#xff0c;我们直接使用…

Leetcode-132.Palindrome Partitioning II [C++][Java]

目录 一、题目描述 二、解题思路 【C】 【Java】 Leetcode-132.Palindrome Partitioning IIhttps://leetcode.com/problems/palindrome-partitioning-ii/description/132. 分割回文串 II - 力扣&#xff08;LeetCode&#xff09;132. 分割回文串 II - 给你一个字符串 s&…

在 macOS 上优化 Vim 用于开发

简介 这篇指南将带你通过一系列步骤&#xff0c;如何在 macOS 上优化 Vim&#xff0c;使其具备 代码补全、语法高亮、代码格式化、代码片段管理、目录树等功能。此外&#xff0c;我们还会解决在安装过程中可能遇到的常见错误。 1. 安装必备工具 在开始 Vim 配置之前&#xff…

SOME/IP-SD -- 协议英文原文讲解8

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 5.1.4.4 S…

【Agent实战】货物上架位置推荐助手(RAG方式+结构化prompt(CoT)+API工具结合ChatGPT4o能力Agent项目实践)

本文原创作者:姚瑞南 AI-agent 大模型运营专家,先后任职于美团、猎聘等中大厂AI训练专家和智能运营专家岗;多年人工智能行业智能产品运营及大模型落地经验,拥有AI外呼方向国家专利与PMP项目管理证书。(转载需经授权) 目录 结论 效果图示 1.prompt 2. API工具封…

PyTorch 深度学习实战(11):强化学习与深度 Q 网络(DQN)

在之前的文章中&#xff0c;我们介绍了神经网络、卷积神经网络&#xff08;CNN&#xff09;、循环神经网络&#xff08;RNN&#xff09;、Transformer 等多种深度学习模型&#xff0c;并应用于图像分类、文本分类、时间序列预测等任务。本文将介绍强化学习的基本概念&#xff0…

Python学习第十九天

Django-分页 后端分页 Django提供了Paginator类来实现后端分页。Paginator类可以将一个查询集&#xff08;QuerySet&#xff09;分成多个页面&#xff0c;每个页面包含指定数量的对象。 from django.shortcuts import render, redirect, get_object_or_404 from .models impo…

Windows环境下安装部署dzzoffice+onlyoffice的私有网盘和在线协同系统

安装前需要准备好Docker Desktop环境&#xff0c;可查看我的另一份亲测安装文章 https://blog.csdn.net/qq_43003203/article/details/146283915?spm1001.2014.3001.5501 1、安装配置onlyoffice 1、Docker 拉取onlyoffice容器镜像 管理员身份运行Windows PowerShell&#x…

ChatPromptTemplate的使用

ChatPromptTemplate 是 LangChain 中专门用于管理多角色对话结构的提示词模板工具。它的核心价值在于&#xff0c;开发者可以预先定义不同类型的对话角色消息&#xff08;如系统指令、用户提问、AI历史回复&#xff09;&#xff0c;并通过数据绑定动态生成完整对话上下文。 1.…

Blender插件NodeWrangler导入贴图报错解决方法

Blender用NodeWrangler插件 CtrlShiftT 导入贴图 直接报错 解决方法: 用CtrlshiftT打开需要导入的材质文件夹时&#xff0c;右边有一个默认勾选的相对路径&#xff0c;取消勾选就可以了。 开启node wrangler插件&#xff0c;然后在导入贴图是取消勾选"相对路径"&am…

java项目之基于ssm的药店药品信息管理系统(源码+文档)

项目简介 药店药品信息管理系统实现了以下功能&#xff1a; 个人信息管理 负责管理个人用户的信息。 员工管理 负责管理药店或药品管理机构的员工信息。 药品管理 负责管理药品的详细信息&#xff0c;可能包括药品名称、成分、剂量、价格、库存等。 进货管理 负责管理药品…

论文分享 | HE-Nav: 一种适用于复杂环境中空地机器人的高性能高效导航系统

阿木实验室始终致力于通过开源项目和智能无人机产品&#xff0c;为全球无人机开发者提供强有力的技术支持&#xff0c;并推出了开源项目校园赞助活动&#xff0c;助力高校学子在学术研究与技术创新中取得更大突破。近日&#xff0c;香港大学王俊铭同学&#xff0c;基于阿木实验…