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

news/2025/3/17 22:12:11/

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


工具类:DownloadUtils

kotlin">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.Fileclass 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.applicationContextprivate val downloadManager: DownloadManager =context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManagerprivate var downloadId: Long = -1private var progressListener: DownloadProgressListener? = nullprivate 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 获取文件的 Urival apkUri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile)// 创建安装 Intentval 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. 初始化并下载文件

kotlin">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 中添加以下配置:

<providerandroid:name="androidx.core.content.FileProvider"android:authorities="${applicationId}.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid: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.ppmy.cn/news/1579924.html

相关文章

【Go学习】04-4-Gorm框架-增删改查事务钩子

【Go学习】04-4-Gorm框架-增删改查 增删改查插入数据用指定的字段创建忽略字段批量插入map创建sql表达式使用原生sql语句 更新数据保存数据更新单个列更新多列更新选定的字段表达式子查询更新 删除数据查询数据查询函数whereselectorder分页count分组直接执行sql语句 事务和Hoo…

207、【图论】孤岛的总面积

题目 思路 相比于 206、【图论】岛屿数量&#xff0c;就是在这个代码的基础上。先遍历边界&#xff0c;将边界连接的岛屿变为0&#xff0c;然后再计算一遍当前为1的岛屿面积。 代码实现 import collectionsn, m list(map(int, input().split())) graph []for _ in range(n…

奇安信二面

《网安面试指南》https://mp.weixin.qq.com/s/RIVYDmxI9g_TgGrpbdDKtA?token1860256701&langzh_CN 5000篇网安资料库https://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247486065&idx2&snb30ade8200e842743339d428f414475e&chksmc0e4732df793fa3bf39…

C++算法学习2:二分算法精讲

一、实数二分法回顾 1.1问题背景 在1~2的范围内找到一个x&#xff0c;使得式子5x2 -9x 1 的绝对值<10-9&#xff08;即无限接近0&#xff09; 要求&#xff1a;x精确到小数点后9位。 换句话说也就是求&#xff1a;就是求方程 5x2- 9x 1 0 在1~2内的近似解 1.2怎么找到…

python爬虫笔记(一)

文章目录 html基础标签和下划线无序列表和有序列表表格加边框 html的属性a标签&#xff08;网站&#xff09;target属性换行线和水平分割线 图片设置宽高width&#xff0c;height html区块——块元素与行内元素块元素与行内元素块元素举例行内元素举例 表单from标签type属性pla…

【STM32】USART串口协议串口外设-学习笔记

串口协议 通信接口 通信的目的&#xff1a;将一个设备的数据传送到另一个设备&#xff0c;扩展硬件系统。比如STM32芯片内部集成了很多功能模块&#xff0c;像定时器计数、PWM输出、AD采集等等。这些都是芯片内部的电路&#xff0c;这些电路的配置寄存器&#xff0c;数据寄存…

【DeepSeek应用】DeepSeek模型本地化部署方案及Python实现

DeepSeek实在是太火了,虽然经过扩容和调整,但反应依旧不稳定,甚至小圆圈转半天最后却提示“服务器繁忙,请稍后再试。” 故此,本文通过讲解在本地部署 DeepSeek并配合python代码实现,让你零成本搭建自己的AI助理,无惧任务提交失败的压力。 一、环境准备 1. 安装依赖库 …

RustDesk自建远程桌面服务教程

原文&#xff1a;https://www.dong-blog.fun/post/1676 有几家非要收钱&#xff0c;不收钱就慢得要死&#xff0c;自建一个自己用肯定就快了。 买服务器 首先去这里买一台服务器&#xff1a;https://acck.io/shop/ 五块钱一台。 然后去服务器装docker compose&#xff1a;h…