Android 利用socket 来实现 自动升级apk
1.创建socket
- 创建解析的bean
/**
* 返回数据响应的bean
*
* json内容 升级内容:{"code":200,"data":{"downloadApkUrl":"http://192.168.131.233:58060/pda/update?version=1.0.0-1","version":"1.0.0-1"},"message":"","type":99999}
*
* 普通刷新 {"code":200,"data":"e","message":"","type":205}
*
* 适配不同data 类型
*/
data class WebSocketResponseBaseBean<T>(
/**
* code 值
*/
val code: Int,
/**
* 消息内容
*/
val message: String,
val type: Int,
val data: T?,
)
/**
* 心跳发送请求的bean
*/
data class WebSocketRequestBaseBean(
/**
* code 值
*/
val code: Int,
/**
* 消息内容
*/
val message: String,
val type: Int,
val data: String
)
data class Data(
/**
* 下载地址
*/
val downloadApkUrl: String,
/**
* 版本号
*/
val version: String
)
2.创建manager
import com.elvishew.xlog.XLog
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
class WebSocketManager private constructor() {
companion object {
@Volatile
private var instance: WebSocketManager? = null
fun getBloodBankPadManager(): WebSocketManager {
return instance ?: synchronized(this) {
instance ?: WebSocketManager().also { instance = it }
}
}
}
/**
* url 地址
*/
private var baseUrl: String = ""
/**
* 连接
*/
private var webSocket: WebSocket? = null
/**
* client
*/
private var okHttpClient: OkHttpClient? = null
/**
* 回调
*/
private var hccxWebSocketListener: HccxWebSocketListener? = null
/**
* 设备唯一标识
*/
private var deviceId: String = ""
/**
* 初始化baseUrl
* @param baseUrl url地址
*/
fun init(baseUrl: String): WebSocketManager {
this.baseUrl = baseUrl
this.okHttpClient = OkHttpClient()
this.hccxWebSocketListener = HccxWebSocketListener()
return this
}
fun setDeviceId(deviceId: Int): WebSocketManager {
return setDeviceId(deviceId.toString())
}
/**
* 设置设备唯一标识
*/
fun setDeviceId(deviceId: String): WebSocketManager {
this.deviceId = deviceId
return this
}
/**
* 回调
*/
fun callback(callback: HccxWebSocketCallback): WebSocketManager {
hccxWebSocketListener?.setHccxWebSocketCallback(callback)
return this
}
/**
* 连接
*/
fun connect() {
disConnect()
val request = Request.Builder().url(createUrl(baseUrl, deviceId)).build()
webSocket = okHttpClient?.newWebSocket(request, hccxWebSocketListener)
}
/**
* 清空 websocket
*/
fun disConnect(){
webSocket?.cancel()
webSocket = null
}
private fun createUrl(baseUrl: String, deviceId: String): String {
XLog.e("当前baseUrl----$baseUrl-------deviceId----$deviceId")
return "$baseUrl$deviceId"
}
}
- 创建listener回调
import com.elvishew.xlog.XLog
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
class HccxWebSocketListener: WebSocketListener() {
companion object {
/**
* 心跳时间
*/
private const val PING_PONG_INTERVAL_TIME = 10_000L // 10 秒
/**
* 重连时间
*/
private const val RE_CONNECT_INTERVAL_TIME = 60_000L // 60 秒
/**
* 重连间隔时间
*/
private const val RE_CONNECT_DELAY_TIME = 10_000L // 10 秒
}
@Volatile
private var mWebSocket: WebSocket? = null
private var hccxWebSocketCallback: HccxWebSocketCallback? = null
/**
* 心跳机制
*/
private var pingJob: Job? = null
/**
* 重连
*/
private var reconnectJob: Job? = null
/**
* 初始化socketcallback
*/
fun setHccxWebSocketCallback(callback: HccxWebSocketCallback) {
this.hccxWebSocketCallback = callback
}
override fun onOpen(webSocket: WebSocket, response: Response) {
XLog.e("onOpen")
this.mWebSocket = webSocket
stopReconnect()
startPingPong()
hccxWebSocketCallback?.onOpen()
}
/**
* 消息回调
*/
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
hccxWebSocketCallback?.onMessage(text)
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
webSocket.close(1000, reason)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
XLog.e("onClosed")
stopPingPong()
startReconnect()
hccxWebSocketCallback?.onClosed(code, "app closed")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
XLog.e("onFailure")
stopPingPong()
startReconnect()
hccxWebSocketCallback?.onFailure(t)
}
/**
* 开始心跳机制
*/
private fun startPingPong() {
if (pingJob?.isActive == true) return
pingJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
sendPingPongMessage()
delay(PING_PONG_INTERVAL_TIME)
}
}
XLog.e("start -- PingPong")
}
/**
* 关闭心跳
*/
private fun stopPingPong() {
pingJob?.cancel()
XLog.e("stop -- PingPong")
}
private fun sendPingPongMessage() {
XLog.e("pingpong------$mWebSocket")
val pingPongMessage = WebSocketRequestBaseBean(200, "", 100, "")
val pingPongMessageJson = Gson().toJson(pingPongMessage)
mWebSocket?.send(pingPongMessageJson)
}
/**
* 开始重连
*/
private fun startReconnect() {
if (reconnectJob?.isActive == true) return
reconnectJob = CoroutineScope(Dispatchers.IO).launch {
delay(RE_CONNECT_DELAY_TIME)
while (isActive) {
XLog.e("reconnect")
WebSocketManager.getBloodBankPadManager().connect()
delay(RE_CONNECT_INTERVAL_TIME)
}
}
XLog.e("start -- ReConnectServer")
}
/**
* 关闭重连
*/
private fun stopReconnect() {
reconnectJob?.cancel()
XLog.e("stop -- ReConnectServer")
}
}
- 数据解析类
import com.elvishew.xlog.XLog
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
class WebSocketParserChain {
private val parsers = mutableListOf<WebSocketDataParser>()
fun addParser(parser: WebSocketDataParser): WebSocketParserChain {
parsers.add(parser)
return this
}
fun parse(text: String): WebSocketResponseBaseBean<*>? {
for (parser in parsers) {
val result = parser.parse(text)
if (result != null) {
return result
}
}
return null
}
}
/**
* 解析的接口
*/
interface WebSocketDataParser {
fun parse(text: String): WebSocketResponseBaseBean<*>?
}
/**
* 对象解析
*/
class DataParser : WebSocketDataParser {
override fun parse(text: String): WebSocketResponseBaseBean<*>? {
return try {
val objectType = object : TypeToken<WebSocketResponseBaseBean<Data>>() {}.type
val webSocketBaseBean: WebSocketResponseBaseBean<*> = Gson().fromJson(text, objectType)
// 如果解析后的 data 是对象(Data),直接返回结果
if (webSocketBaseBean.data != null && webSocketBaseBean.data is Data) {
webSocketBaseBean
} else {
XLog.e("DataParser----json解析对应的bean---${webSocketBaseBean.data}")
null // 如果 data 不是对象,返回 null 传递给下一个解析器
}
} catch (e: JsonSyntaxException) {
XLog.e("DataParser----json解析异常---${e.message}")
null // 如果解析失败,返回 null
}
}
}
/**
* string 解析
*/
class StringParser: WebSocketDataParser {
override fun parse(text: String): WebSocketResponseBaseBean<*>? {
return try {
val stringType = object : TypeToken<WebSocketResponseBaseBean<String>>() {}.type
Gson().fromJson(text, stringType)
} catch (e: JsonSyntaxException) {
XLog.e("StringParser-----json异常---${e.message}")
null
}
}
}
/**
* object 解析 默认解析 添加一个兜底方案 避免返回的data 既不是字符串 又不是对象
*/
class ObjectParser: WebSocketDataParser {
override fun parse(text: String): WebSocketResponseBaseBean<*>? {
return try {
val stringType = object : TypeToken<WebSocketResponseBaseBean<Any>>() {}.type
Gson().fromJson(text, stringType)
} catch (e: JsonSyntaxException) {
XLog.e("ObjectParser-----json异常---${e.message}")
null
}
}
}
- listener 实现类
import com.elvishew.xlog.XLog
class HccxWebSocketCallbackIml : HccxWebSocketCallback {
override fun onMessage(text: String) {
XLog.e("onMessage----text---$text")
try {
val parserChain = WebSocketParserChain()
.addParser(DataParser()) // 尝试解析为对象类型
.addParser(StringParser()) // 如果失败则尝试解析为字符串类型
.addParser(ObjectParser()) // 如果失败则尝试解析为Any类型 兜底方案
val webSocketBaseBean = parserChain.parse(text)
XLog.e("当前websocket数据为--$webSocketBaseBean------mListener---$mListener")
if (webSocketBaseBean==null) {
return
}
// 判断 `code` 是否为 200
if (webSocketBaseBean.code != 200) {
return
}
// 回调是否初始化
if (mListener == null) {
return
}
when (webSocketBaseBean.data) {
is Data -> {
val updateInfo = webSocketBaseBean.data as Data
mListener!!.downloadData(updateInfo)
}
is String -> {
when (webSocketBaseBean.type) {
}
val downloadUrl = webSocketBaseBean.data as String
XLog.e("------downloadUrl----$downloadUrl")
mListener!!.defaultRefresh()
}
else -> {
XLog.e("解析的data---${webSocketBaseBean.data}")
}
}
} catch (e: Exception) {
XLog.e("解析异常: ${e.message}")
}
}
override fun onClosed(code: Int, reason: String) {
XLog.e("当前websocket onClosed--code----$code---------reason----$reason")
}
override fun onOpen() {
}
override fun onFailure(t: Throwable) {
XLog.e("当前websocket onFailure--Throwable----" + t.message)
}
private var mListener: WebSocketCallbackListener? = null
/**
* 设置回调
*/
fun setWebSocketCallbackListener(listener: WebSocketCallbackListener) {
this.mListener = listener
}
interface WebSocketCallbackListener {
/**
* 升级回调
* @param data 下载返回的bean
*/
fun downloadData(data:Data) {}
/**
* 默认刷新
*/
fun defaultRefresh(){}
}
}
/**
* socket 信息回调接口
*/
interface HccxWebSocketCallback {
/**
* 获取推送的消息
* @param text
*/
fun onMessage(text: String)
/**
* 关闭连接
* @param code
* @param reason
*/
fun onClosed(code: Int, reason: String)
/**
* 打开连接
*/
fun onOpen()
/**
* 连接失败
* @param t
*/
fun onFailure(t: Throwable)
}
- 使用
val hccxWebSocketCallbackIml = HccxWebSocketCallbackIml()
hccxWebSocketCallbackIml.setWebSocketCallbackListener(object :
HccxWebSocketCallbackIml.WebSocketCallbackListener {
override fun downloadData(data: Data) {
super.downloadData(data)
runOnUiThread {
toast("刷新数据了-----${data.downloadApkUrl}")
val intent = Intent(this@MainActivity, InstallActivity::class.java)
intent.putExtra(Constant.DOWNLOAD_URL, data.downloadApkUrl)
intent.putExtra(Constant.DOWNLOAD_FILE_NAME, data.version)
startActivity(intent)
}
}
})
WebSocketManager.getBloodBankPadManager()
.init("url")
.callback(hccxWebSocketCallbackIml).connect()
2. 自动安装 activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.elvishew.xlog.XLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.hgj.jetpackmvvm.ext.download.OnDownLoadListener
import java.io.File
class InstallActivity : AppCompatActivity() {
private var mBinding: ActivityInstallBinding? = null
/**
* 下载地址
*/
@Volatile
private var downLoadUrl: String = ""
/**
* 下载名称
*/
@Volatile
private var downLoadFileName: String = ""
/**
* 重新申请权限次数 用于超过重新申请次数之后 给出文案提示,
*/
@Volatile
private var againNumber = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityInstallBinding.inflate(layoutInflater)
setContentView(mBinding!!.root)
val intent = intent
downLoadUrl = intent.getStringExtra(Constant.DOWNLOAD_URL)
?: "https"
downLoadFileName = intent.getStringExtra(DOWNLOAD_FILE_NAME)?.run {
if (isNotEmpty() && !endsWith(".apk")) "$this.apk" else this
} ?: DOWNLOAD_DEFAULT_FILE_NAME
XLog.e("当前下载名称为$downLoadFileName-------下载地址---$downLoadUrl")
if (downLoadUrl.isEmpty()) {
toast("下载地址不能为空")
return
}
if (downLoadFileName.isEmpty()) {
toast("下载文件名不合法,请重新输入")
return
}
}
override fun onResume() {
super.onResume()
// 判断权限是否都申请完
if (RequestPermissionUtil.checkPermissionState(this)) {
downloadApk(downLoadFileName, downLoadUrl)
} else {
// 申请权限可以 重试两次 ,两次失败之后给出提示
againNumber++
if (againNumber > 2) {
toast(RequestPermissionUtil.permissionTip(this))
finish()
return
}
startActivity(Intent(this, PermissionActivity::class.java))
}
}
/**
* 下载apk
* @param downLoadFileName 下载的文件名称
* @param downLoadUrl 下载url 地址
*/
private fun downloadApk(downLoadFileName: String, downLoadUrl: String) {
val checkUpdateParentFile = checkUpdateParentFile(this)
val file = File(checkUpdateParentFile.path + "/update/")
if (!file.exists()) {
file.mkdirs()
}
toast("正在下载中请稍后")
lifecycleScope.launch(Dispatchers.IO) {
DownLoadManager.downLoad("NoHeader",
downLoadUrl,
file.path,
downLoadFileName,
true,
object : OnDownLoadListener {
override fun onDownLoadPrepare(key: String) {
}
override fun onDownLoadError(key: String, throwable: Throwable) {
toast(throwable.message.toString())
XLog.e("下载失败---地址为---$downLoadUrl")
}
override fun onDownLoadSuccess(key: String, path: String, size: Long) {
XLog.e("文件路径为----$path")
if (File(path).length() > 0) {
toast("下载完成")
installAPK(this@InstallActivity, path)
finish()
} else {
XLog.e("安装失败---下载地址为---$downLoadUrl")
toast("文件包安装失败,请查看下载地址是否正常")
finish()
}
}
override fun onDownLoadPause(key: String) {
}
override fun onUpdate(
key: String,
progress: Int,
read: Long,
count: Long,
done: Boolean
) {
runOnUiThread {
mBinding!!.abuPermission.text = "当前进度为$progress"
}
XLog.e("当前进度为-----$progress-----$read-----$count-----$done")
}
}
)
}
}
}
object Constant {
/**
* 下载地址
*/
const val DOWNLOAD_URL = "DOWNLOAD_URL"
/**
* 下载文件名称
*/
const val DOWNLOAD_FILE_NAME = "DOWNLOAD_FILE_NAME"
/**
* 下载文件默认名称名称
*/
const val DOWNLOAD_DEFAULT_FILE_NAME = "update_1.0.apk"
}
引入了权限申请 依赖
implementation 'com.github.getActivity:XXPermissions:20.0'
import android.Manifest
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.text.TextUtils
import android.text.TextUtils.SimpleStringSplitter
import android.util.Log
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
object RequestPermissionUtil {
/**
* 重试次数文案提示
*/
fun permissionTip(context: Context): String {
var permissionTipMessage =
StringBuilder("权限申请超过重试次数,请进入到系统设置界面,打开需要申请的权限\n")
appendPermissionTip(allMediaState(context), "(所有文件权限)", permissionTipMessage)
appendPermissionTip(installState(context), "(允许安装未知来源权限)", permissionTipMessage)
appendPermissionTip(
XXPermissions.isGranted(context, Permission.SYSTEM_ALERT_WINDOW),
"(悬浮窗权限)",
permissionTipMessage
)
appendPermissionTip(
XXPermissions.isGranted(
context,
Permission.READ_EXTERNAL_STORAGE,
Permission.WRITE_EXTERNAL_STORAGE
), "(读写文件权限)", permissionTipMessage
)
appendPermissionTip(
XXPermissions.isGranted(context, Permission.POST_NOTIFICATIONS),
"(通知栏权限)",
permissionTipMessage
)
appendPermissionTip(
isSettingOpen(AutoInstallService::class.java, context),
"(无障碍服务权限)",
permissionTipMessage
)
return permissionTipMessage.toString()
}
/**
* 拼接内容
*/
private fun appendPermissionTip(
condition: Boolean,
message: String,
permissionTipMessage: StringBuilder
) {
if (!condition) {
permissionTipMessage.append(message).append("\n")
}
}
/**
* 判断需要申请的权限是否都申请完毕
* 1.所有文件权限 Android 10以上需要申请
* 2.是否允许安装未知来源
* 3.悬浮窗权限
* 4.读写权限
* 5.系统通知栏权限
* 6.无障碍服务权限
*/
fun checkPermissionState(context: Context): Boolean {
return allMediaState(context) && installState(context)
&& XXPermissions.isGranted(context, Permission.SYSTEM_ALERT_WINDOW)
&& XXPermissions.isGranted(
context,
Permission.READ_EXTERNAL_STORAGE,
Permission.WRITE_EXTERNAL_STORAGE
)
&& XXPermissions.isGranted(context, Permission.POST_NOTIFICATIONS)
&& isSettingOpen(AutoInstallService::class.java, context)
}
/**
* 是否有安装未知来源权限
*/
private fun installState(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
XXPermissions.isGranted(context, Permission.REQUEST_INSTALL_PACKAGES)
} else {
true
}
}
/**
* Android 10以上 是否有所有文件权限
*/
private fun allMediaState(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
XXPermissions.isGranted(context, Permission.MANAGE_EXTERNAL_STORAGE)
} else {
true
}
}
/**
* 检查系统设置:是否开启辅助服务
* @param service 辅助服务
*/
fun isSettingOpen(service: Class<*>, cxt: Context): Boolean {
try {
val enable = Settings.Secure.getInt(
cxt.contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED,
0
)
if (enable != 1) return false
val services = Settings.Secure.getString(
cxt.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
if (!TextUtils.isEmpty(services)) {
val split = SimpleStringSplitter(':')
split.setString(services)
while (split.hasNext()) { // 遍历所有已开启的辅助服务名
if (split.next()
.equals(cxt.packageName + "/" + service.name, ignoreCase = true)
) return true
}
}
} catch (e: Throwable) { //若出现异常,则说明该手机设置被厂商篡改了,需要适配
Log.e("", "isSettingOpen: " + e.message)
}
return false
}
/**
* 跳转到系统设置:开启辅助服务
*/
fun jumpToSetting(cxt: Context) {
try {
cxt.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
} catch (e: Throwable) { //若出现异常,则说明该手机设置被厂商篡改了,需要适配
try {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
cxt.startActivity(intent)
} catch (e2: Throwable) {
Log.e("", "jumpToSetting: " + e2.message)
}
}
}
/**
* 读写权限
* 适配android14
*/
fun permissionList(): MutableList<String> {
var permissionList = mutableListOf<String>()
permissionList.add(Manifest.permission.CAMERA)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
permissionList.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
permissionList.add(Manifest.permission.READ_MEDIA_IMAGES)
permissionList.add(Manifest.permission.READ_MEDIA_VIDEO)
permissionList.add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionList.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
permissionList.add(Manifest.permission.READ_MEDIA_IMAGES)
permissionList.add(Manifest.permission.READ_MEDIA_VIDEO)
} else {
permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE)
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
return permissionList
}
}
文件判断
fun createFileUri(context: Context, file: File): Uri {
var uri: Uri? = null
uri = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
FileProvider.getUriForFile(
context, context.packageName + ".fileprovider",
file
)
} else {
Uri.fromFile(file)
}
return uri
}
/**
* 下载安装包的存放路径 app 私有目录下面
*/
fun checkUpdateParentFile(context: Context): File {
val filesDir = context.filesDir
if (!filesDir.exists()) {
filesDir.mkdirs()
}
return filesDir
}
/**
* 自动安装 android 10一下可以实现自动安装并打开
*/
fun installAPK(context: Context, path: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(
createFileUri(context, File(path)),
"application/vnd.android.package-archive"
)
//设置静默安装标识
intent.putExtra("silence_install", true)
//设置安装完成是否启动标识
intent.putExtra("is_launch", true)
//设置后台中启动activity标识
intent.putExtra("allowed_Background", true)
if (context.packageManager.queryIntentActivities(intent, 0).size > 0) {
context.startActivity(intent)
}
}
下载类downloadmanager
import android.os.Looper
import com.elvishew.xlog.XLog
import com.hccx.lib_framwork.ext.loge
import com.hccx.lib_framwork.net.KtLoggingInterceptor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import me.hgj.jetpackmvvm.ext.download.DownLoadPool
import me.hgj.jetpackmvvm.ext.download.OnDownLoadListener
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.util.concurrent.TimeUnit
object DownLoadManager {
private val retrofitBuilder by lazy {
Retrofit.Builder()
.baseUrl("url/")
.client(
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.build()
).addConverterFactory(GsonConverterFactory.create())
.build()
}
/**
*开始下载
* @param tag String 标识
* @param url String 下载的url
* @param savePath String 保存的路径
* @param saveName String 保存的名字
* @param reDownload Boolean 如果文件已存在是否需要重新下载 默认不需要重新下载
* @param loadListener OnDownLoadListener
*/
suspend fun downLoad(
tag: String,
url: String,
savePath: String,
saveName: String,
reDownload: Boolean = false,
loadListener: OnDownLoadListener
) {
withContext(Dispatchers.IO) {
doDownLoad(tag, url, savePath, saveName, reDownload, loadListener, this)
}
}
/**
* 取消下载
* @param key String 取消的标识
*/
fun cancel(key: String) {
val path = DownLoadPool.getPathFromKey(key)
if (path != null) {
val file = File(path)
if (file.exists()) {
file.delete()
}
}
DownLoadPool.remove(key)
}
/**
* 暂停下载
* @param key String 暂停的标识
*/
fun pause(key: String) {
val listener = DownLoadPool.getListenerFromKey(key)
listener?.onDownLoadPause(key)
DownLoadPool.pause(key)
}
/**
* 取消所有下载
*/
fun doDownLoadCancelAll() {
DownLoadPool.getListenerMap().forEach {
cancel(it.key)
}
}
/**
* 暂停所有下载
*/
fun doDownLoadPauseAll() {
DownLoadPool.getListenerMap().forEach {
pause(it.key)
}
}
/**
*下载
* @param tag String 标识
* @param url String 下载的url
* @param savePath String 保存的路径
* @param saveName String 保存的名字
* @param reDownload Boolean 如果文件已存在是否需要重新下载 默认不需要重新下载
* @param loadListener OnDownLoadListener
* @param coroutineScope CoroutineScope 上下文
*/
private suspend fun doDownLoad(
tag: String,
url: String,
savePath: String,
saveName: String,
reDownload: Boolean,
loadListener: OnDownLoadListener,
coroutineScope: CoroutineScope
) {
//判断是否已经在队列中
val scope = DownLoadPool.getScopeFromKey(tag)
if (scope != null && scope.isActive) {
"已经在队列中".loge()
return
} else if (scope != null && !scope.isActive) {
"key $tag 已经在队列中 但是已经不再活跃 remove".loge()
DownLoadPool.removeExitSp(tag)
}
if (saveName.isEmpty()) {
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(tag, Throwable("save name is Empty"))
}
return
}
if (Looper.getMainLooper().thread == Thread.currentThread()) {
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(tag, Throwable("current thread is in main thread"))
}
return
}
val file = File("$savePath/$saveName")
val currentLength = if (!file.exists()) {
0L
} else {
ShareDownLoadUtil.getLong(tag, 0)
}
if (file.exists() && currentLength == 0L && !reDownload) {
//文件已存在了
loadListener.onDownLoadSuccess(tag, file.path, file.length())
return
}
"startDownLoad current $currentLength".loge()
try {
//添加到pool
DownLoadPool.add(tag, coroutineScope)
DownLoadPool.add(tag, "$savePath/$saveName")
DownLoadPool.add(tag, loadListener)
withContext(Dispatchers.Main) {
loadListener.onDownLoadPrepare(key = tag)
}
// 断点续传下载
val response = retrofitBuilder.create(DownLoadService::class.java)
.downloadFile("bytes=$currentLength-", url)
val acceptRanges = response.headers()["Accept-Ranges"]
val supportsResume = acceptRanges != null && acceptRanges.equals(
"bytes",
ignoreCase = true)
// 判断是否支持断点续传
if (!supportsResume) {
val responseBody = response.body()
if (responseBody == null) {
"responseBody is null".loge()
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(
key = tag,
throwable = Throwable("responseBody is null please check download url")
)
}
DownLoadPool.remove(tag)
return
}
downloadNoRange(tag,responseBody,file,loadListener)
return
}
val responseBody = response.body()
if (responseBody == null) {
"responseBody is null".loge()
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(
key = tag,
throwable = Throwable("responseBody is null please check download url")
)
}
DownLoadPool.remove(tag)
return
}
FileTool.downToFile(
tag,
savePath,
saveName,
currentLength,
responseBody,
loadListener
)
} catch (throwable: Throwable) {
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(key = tag, throwable = throwable)
}
DownLoadPool.remove(tag)
}
}
/**
* 不带有断点续传下载
* @param tag 下载标识
* @param url 下载链接
* @param apkFile 下载保存后的文件
* @param loadListener 监控回调
*/
private suspend fun downloadNoRange(
tag: String,
responseBody: ResponseBody,
apkFile: File,
loadListener: OnDownLoadListener
) {
try {
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
val fileReader = ByteArray(4096)
val fileSize = responseBody.contentLength()
var fileSizeDownloaded: Long = 0
XLog.e("-----------下载filesize---$fileSize")
inputStream = responseBody.byteStream()
outputStream = FileOutputStream(apkFile)
while (true) {
val read = inputStream.read(fileReader)
if (read == -1) break
outputStream.write(fileReader, 0, read)
fileSizeDownloaded += read
// 计算并更新进度
val progress = (fileSizeDownloaded*100 / fileSize).toInt()
loadListener.onUpdate(tag,progress,fileSizeDownloaded,fileSize,fileSizeDownloaded==fileSize)
}
outputStream.flush()
withContext(Dispatchers.Main) {
loadListener.onDownLoadSuccess(tag, apkFile.path,fileSizeDownloaded)
}
DownLoadPool.remove(tag)
apkFile
} finally {
inputStream?.close()
outputStream?.close()
}
} catch (e: Exception) {
loadListener.onDownLoadError(tag,Throwable(e.message))
null
}
}
}
object DownLoadPool {
private val scopeMap: ConcurrentHashMap<String, CoroutineScope> = ConcurrentHashMap()
//下载位置
private val pathMap: ConcurrentHashMap<String, String> = ConcurrentHashMap()
//监听
private val listenerHashMap: ConcurrentHashMap<String, OnDownLoadListener> = ConcurrentHashMap()
fun add(key: String, job: CoroutineScope) {
scopeMap[key] = job
}
//监听
fun add(key: String, loadListener: OnDownLoadListener) {
listenerHashMap[key] = loadListener
}
//下载位置
fun add(key: String, path: String) {
pathMap[key] = path
}
fun remove(key: String) {
pause(key)
scopeMap.remove(key)
listenerHashMap.remove(key)
pathMap.remove(key)
ShareDownLoadUtil.remove(key)
}
fun pause(key: String) {
val scope = scopeMap[key]
if (scope != null && scope.isActive) {
scope.cancel()
}
}
fun removeExitSp(key: String) {
scopeMap.remove(key)
}
fun getScopeFromKey(key: String): CoroutineScope? {
return scopeMap[key]
}
fun getListenerFromKey(key: String): OnDownLoadListener? {
return listenerHashMap[key]
}
fun getPathFromKey(key: String): String? {
return pathMap[key]
}
fun getListenerMap(): ConcurrentHashMap<String, OnDownLoadListener> {
return listenerHashMap
}
}
interface DownLoadProgressListener {
/**
* 下载进度
* @param key url
* @param progress 进度
* @param read 读取
* @param count 总共长度
* @param done 是否完成
*/
fun onUpdate( key: String,progress: Int, read: Long,count: Long,done: Boolean)
}
interface OnDownLoadListener : DownLoadProgressListener {
//等待下载
fun onDownLoadPrepare(key: String)
//下载失败
fun onDownLoadError(key: String, throwable: Throwable)
//下载成功
fun onDownLoadSuccess(key: String, path: String,size:Long)
//下载暂停
fun onDownLoadPause(key: String)
}
sealed class DownloadResultState {
companion object {
fun onPending(): DownloadResultState = Pending
fun onProgress(soFarBytes: Long, totalBytes: Long, progress: Int): DownloadResultState = Progress(soFarBytes, totalBytes,progress)
fun onSuccess(filePath: String,totalBytes:Long): DownloadResultState = Success(filePath,totalBytes)
fun onPause(): DownloadResultState = Pause
fun onError(errorMsg: String): DownloadResultState = Error(errorMsg)
}
object Pending : DownloadResultState()
data class Progress(val soFarBytes: Long, val totalBytes: Long,val progress: Int) : DownloadResultState()
data class Success(val filePath: String,val totalBytes:Long) : DownloadResultState()
object Pause : DownloadResultState()
data class Error(val errorMsg: String) : DownloadResultState()
}
interface DownLoadService {
/**
* 带有断点续传
*/
@Streaming
@GET
suspend fun downloadFile(
@Header("RANGE") start: String,
@Url url: String
): Response<ResponseBody>
}
object FileTool {
//定义GB的计算常量
private const val GB = 1024 * 1024 * 1024
//定义MB的计算常量
private const val MB = 1024 * 1024
//定义KB的计算常量
private const val KB = 1024
/**
* 下载文件到本地
* @param key String
* @param savePath String
* @param saveName String
* @param currentLength Long
* @param responseBody ResponseBody
* @param loadListener OnDownLoadListener
*/
suspend fun downToFile(
key: String,
savePath: String,
saveName: String,
currentLength: Long,
responseBody: ResponseBody,
loadListener: OnDownLoadListener
) {
val filePath = getFilePath(savePath, saveName)
try {
if (filePath == null) {
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(key, Throwable("mkdirs file [$savePath] error"))
}
DownLoadPool.remove(key)
return
}
//保存到文件
saveToFile(currentLength, responseBody, filePath, key, loadListener)
} catch (throwable: Throwable) {
withContext(Dispatchers.Main) {
loadListener.onDownLoadError(key, throwable)
}
DownLoadPool.remove(key)
}
}
/**
*
* @param currentLength Long
* @param responseBody ResponseBody
* @param filePath String
* @param key String
* @param loadListener OnDownLoadListener
*/
suspend fun saveToFile(
currentLength: Long,
responseBody: ResponseBody,
filePath: String,
key: String,
loadListener: OnDownLoadListener
) {
val fileLength =
getFileLength(currentLength, responseBody)
val inputStream = responseBody.byteStream()
val accessFile = RandomAccessFile(File(filePath), "rwd")
val channel = accessFile.channel
val mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE,
currentLength,
fileLength - currentLength
)
val buffer = ByteArray(1024 * 4)
var len = 0
var lastProgress = 0
var currentSaveLength = currentLength //当前的长度
while (inputStream.read(buffer).also { len = it } != -1) {
mappedBuffer.put(buffer, 0, len)
currentSaveLength += len
val progress = (currentSaveLength.toFloat() / fileLength * 100).toInt() // 计算百分比
if (lastProgress != progress) {
lastProgress = progress
//记录已经下载的长度
ShareDownLoadUtil.putLong(key, currentSaveLength)
withContext(Dispatchers.Main) {
loadListener.onUpdate(
key,
progress,
currentSaveLength,
fileLength,
currentSaveLength == fileLength
)
}
if (currentSaveLength == fileLength) {
withContext(Dispatchers.Main) {
loadListener.onDownLoadSuccess(key, filePath,fileLength)
}
DownLoadPool.remove(key)
}
}
}
inputStream.close()
accessFile.close()
channel.close()
}
/**
* 数据总长度
* @param currentLength Long
* @param responseBody ResponseBody
* @return Long
*/
fun getFileLength(
currentLength: Long,
responseBody: ResponseBody
) =
if (currentLength == 0L) responseBody.contentLength() else currentLength + responseBody.contentLength()
/**
* 获取下载地址
* @param savePath String
* @param saveName String
* @return String?
*/
fun getFilePath(savePath: String, saveName: String): String? {
if (!createFile(savePath)) {
return null
}
return "$savePath/$saveName"
}
/**
* 创建文件夹
* @param downLoadPath String
* @return Boolean
*/
fun createFile(downLoadPath: String): Boolean {
val file = File(downLoadPath)
if (!file.exists()) {
return file.mkdirs()
}
return true
}
/**
* 格式化小数
* @param bytes Long
* @return String
*/
fun bytes2kb(bytes: Long): String {
val format = DecimalFormat("###.0")
return when {
bytes / GB >= 1 -> {
format.format(bytes / GB) + "GB";
}
bytes / MB >= 1 -> {
format.format(bytes / MB) + "MB";
}
bytes / KB >= 1 -> {
format.format(bytes / KB) + "KB";
}
else -> {
"${bytes}B";
}
}
}
/**
* 获取App文件的根路径
* @return String
*/
fun getBasePath(): String {
var p: String? = appContext.getExternalFilesDir(null)?.path
val f: File? = appContext.getExternalFilesDir(null)
if (null != f) {
p = f.absolutePath
}
return p ?: ""
}
}
object ShareDownLoadUtil {
private var path = Build.BRAND + "_" + Build.MODEL + "_" + "download_sp"
private val sp: SharedPreferences
init {
sp = appContext.getSharedPreferences(path, Context.MODE_PRIVATE)
}
fun setPath(path: String) {
ShareDownLoadUtil.path = path
}
fun putBoolean(key: String, value: Boolean) {
sp.edit().putBoolean(key, value).apply()
}
fun getBoolean(key: String, defValue: Boolean): Boolean {
return sp.getBoolean(key, defValue)
}
fun putString(key: String, value: String) {
sp.edit().putString(key, value).apply()
}
fun getString(key: String, defValue: String): String? {
return sp.getString(key, defValue)
}
fun putInt(key: String, value: Int) {
sp.edit().putInt(key, value).apply()
}
fun getInt(key: String, defValue: Int): Int {
return sp.getInt(key, defValue)
}
fun putLong(key: String?, value: Long) {
sp.edit().putLong(key, value).apply()
}
fun getLong(key: String, defValue: Long): Long {
return sp.getLong(key, defValue)
}
fun remove(key: String) {
sp.edit().remove(key).apply()
}
fun clear() {
sp.edit().clear().apply()
}
}