应用内下载更新包
包含:
权限检测、通知栏生成进度、实时进度反馈、下载完成自动安装、通知栏点击安装
效果





代码
第三方依赖(仅仅主要提供上下文对象Context):
implementation 'com.blankj:utilcodex:1.30.6'
下载工具类(直接拷贝粘贴用,包的):
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context.NOTIFICATION_SERVICE
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.text.TextUtils
import androidx.core.app.NotificationCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.content.FileProvider
import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.PermissionUtils
import com.blankj.utilcode.util.PermissionUtils.SimpleCallback
import com.blankj.utilcode.util.ToastUtils
import com.blankj.utilcode.util.Utils
import com.fxyandtjh.voiceaccounting.R
import com.fxyandtjh.voiceaccounting.entity.ApkProgress
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.URLclass DownLoadApk private constructor() {private lateinit var builder: NotificationCompat.Builderprivate lateinit var notification: Notificationprivate lateinit var notificationManager: NotificationManagerprivate var lastUpdateNoticeTime = 0Lcompanion object {val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {DownLoadApk()}}fun download(url: String, callBack: (ApkProgress) -> Unit) {PermissionUtils.permission(Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE).callback(object : SimpleCallback {override fun onGranted() {startDownLoad(url, callBack)}override fun onDenied() {ToastUtils.showShort(Utils.getApp().getText(R.string.deny_permission_update))}}).request()}// 开始下载private fun startDownLoad(url: String, callBack: (ApkProgress) -> Unit) {if (TextUtils.isEmpty(url)) {return}// 获取文件名val fileName = File(URL(url).path).nameif (TextUtils.isEmpty(fileName)) {return}// 创建通知createNotification()// 下载外部公有目录: /storage/emulated/0/Android/data/包名/files/Download/val targetFile =Utils.getApp().getExternalFilesDir("${Environment.DIRECTORY_DOWNLOADS}${File.separator}$fileName")// 如果这个目录下 已经有同名文件了 那么要移除文件targetFile?.let {if (it.exists()) {it.delete()}}val a = targetFile?.absolutePath?.replace(fileName, "") ?: ""Downloader(a, fileName).downLoad(url, { curBytes, totalBytes ->callBack.invoke(ApkProgress(bytes = curBytes, totalBytes = totalBytes))// 更新通知进度updateNotificationProgress(curBytes, totalBytes)}, { file ->callBack.invoke(ApkProgress(path = file.path))// 点击通知进行安装installAPKWhenNoticeClicked(file)}, { _ ->callBack.invoke(ApkProgress(code = 400))})}// 生成通知栏private fun createNotification() {notificationManager =Utils.getApp().getSystemService(NOTIFICATION_SERVICE) as NotificationManager// 获取渠道IDval channel = NotificationChannel("app_channel_ID","app_channel_NAME",NotificationManager.IMPORTANCE_DEFAULT)notificationManager.createNotificationChannel(channel)// 获取应用图标val packageName = AppUtils.getAppPackageName()var iconId = 0try {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {iconId = Utils.getApp().packageManager.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0)).icon} else {iconId = Utils.getApp().packageManager.getApplicationInfo(packageName, 0).icon}} catch (e: Exception) {e.printStackTrace()iconId = R.mipmap.logo}builder = NotificationCompat.Builder(Utils.getApp(), "app_channel_ID").setSmallIcon(iconId).setContentTitle("新版本更新").setContentText("正在下载...").setProgress(100, 0, false)notification = builder.build()notificationManager.notify(1, notification)}// 更新通知进度private fun updateNotificationProgress(curBytes: Long, totalBytes: Long) {var tipText = ""if (curBytes == totalBytes && totalBytes != 0L) {// 下载完成tipText = "下载完成(点击安装)。"builder.setProgress(0, 0, false)} else if (totalBytes == 0L) {tipText = "正在下载: 0%"builder.setProgress(100, 0, false)} else {val progress = curBytes * 100 / totalBytestipText = "正在下载:$progress%"builder.setProgress(100, progress.toInt(), false)}builder.setContentText(tipText)// 下载过程中,进度反馈会非常频繁,频繁更新通知将会消耗大量内存 这里做一个限制 每250毫秒更新一次进度limitUpdateNotificationRate(curBytes != totalBytes && totalBytes != 0L)}// 限制更新通知进度的频率private fun limitUpdateNotificationRate(shouldLimit: Boolean) {val currentTime = System.currentTimeMillis()if (!shouldLimit || currentTime - lastUpdateNoticeTime > 250) {// 250 毫秒更新一次lastUpdateNoticeTime = currentTimenotificationManager.notify(1, builder.build())}}// 安装应用程序private fun installAPKWhenNoticeClicked(file: File, clickNotice: Boolean = false) {val intent = Intent(Intent.ACTION_VIEW)intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)val uri = FileProvider.getUriForFile(Utils.getApp(),AppUtils.getAppPackageName() + ".fileprovider",file)intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)intent.setDataAndType(uri, "application/vnd.android.package-archive")val stackBuilder = TaskStackBuilder.create(Utils.getApp())stackBuilder.addNextIntentWithParentStack(intent)val resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)builder.setContentIntent(resultPendingIntent)builder.setAutoCancel(true)notificationManager.notify(1, builder.build())}// 内部下载类private class Downloader(val targetPath: String, val targetName: String) {fun downLoad(url: String,onProgress: (Long, Long) -> Unit,onFinished: (File) -> Unit,onError: (Exception) -> Unit) {val client = OkHttpClient.Builder().protocols(listOf(Protocol.HTTP_1_1)).build()val request = Request.Builder().url(url).get().build()client.newCall(request).enqueue(object : Callback {override fun onFailure(call: Call, e: IOException) {onError.invoke(e)}override fun onResponse(call: Call, response: Response) {val file = saveToFile(response) { curByte, totalByte ->onProgress.invoke(curByte, totalByte)}file?.let {if (it.exists()) {onFinished(it)} else {onError(RuntimeException("Failed to save file"))}} ?: onError(RuntimeException("Failed to save file"))}})}private fun saveToFile(response: Response,onProgress: (Long, Long) -> Unit): File? {if (TextUtils.isEmpty(targetPath) || TextUtils.isEmpty(targetName) || response.body == null) {return null}// 设置缓冲大小val buff = ByteArray(40960)var len = 0val inputStream = response.body!!.byteStream()val total: Long = response.body!!.contentLength()var sum = 0Lval dir = File(targetPath)if (!dir.exists()) {dir.mkdirs()}val targetFile = File(dir, targetName)val fileOutputStream = FileOutputStream(targetFile)try {while (inputStream.read(buff).also { len = it } != -1) {sum += lenfileOutputStream.write(buff, 0, len)onProgress(sum, total)}} catch (e: IOException) {e.printStackTrace()}fileOutputStream.flush()fileOutputStream.close()inputStream.close()return targetFile}}
}
实体类:
data class ApkProgress(val bytes: Long = 0L, // 当前下载 字节数val totalBytes: Long = 0L, // 总共需要下载的字节数val path: String = "", // 下载完成后,文件保存的绝对路径,未下载完成时,为""空字符串val code: Int = 200 // 下载结果码 异常非 200
)
使用
downloadLink就是APK下载地址,apkProgess通过回调方式返回
DownLoadApk.instance.download(downloadLink) { apkProgress ->}