Android 应用内下载 APK Demo(Kotlin语言)

应用内下载更新包

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

效果

代码

第三方依赖(仅仅主要提供上下文对象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.URL

class DownLoadApk private constructor() {
    private lateinit var builder: NotificationCompat.Builder
    private lateinit var notification: Notification
    private lateinit var notificationManager: NotificationManager
    private var lastUpdateNoticeTime = 0L

    companion 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).name
        if (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
        // 获取渠道ID
        val channel = NotificationChannel(
            "app_channel_ID",
            "app_channel_NAME",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        notificationManager.createNotificationChannel(channel)
        // 获取应用图标
        val packageName = AppUtils.getAppPackageName()
        var iconId = 0
        try {
            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 / totalBytes
            tipText = "正在下载:$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 = currentTime
            notificationManager.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 = 0
            val inputStream = response.body!!.byteStream()
            val total: Long = response.body!!.contentLength()
            var sum = 0L
            val 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 += len
                    fileOutputStream.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 ->
            
}
  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值