配置
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
//协程的
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
//协程的(在viewmodel 使用的)
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
//网络的
api 'com.squareup.retrofit2:retrofit:2.9.0'
api 'com.squareup.retrofit2:converter-moshi:2.9.0'
}
配置2
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<!--用于写入缓存数据到扩展存储卡-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<!-- android 11 的要加这句话-->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
>
....
调用
download(mContext!!)
实现:1
fun download(context: Context) {
val time = System.currentTimeMillis()
LogUtil.d("time1:${time}")
val url = "https://xxxx"
val file2 = getTargetParentFile(context)
viewModelScope.launch {
DownloadGO.download(
url = url,
targetParent = file2.path
).collect { downloadStatus ->
when (downloadStatus) {
is DownloadStatus.onProgress -> {
LogUtil.d("currentThread:${Thread.currentThread().name}")
LogUtil.d("progress:${downloadStatus.progress}")
}
is DownloadStatus.onSuccess -> {
LogUtil.d("耗时:${System.currentTimeMillis() - time}")
LogUtil.d("currentThread:${Thread.currentThread().name}")
LogUtil.d("onSuccess file:${downloadStatus.file?.path}")
}
is DownloadStatus.onFail -> {
LogUtil.d("下载失败 error:${downloadStatus?.error}")
}
else -> {
LogUtil.d("下载失败")
}
}
}
LogUtil.d("处理完成")
}
}
fun getTargetParentFile(context: Context): File {
val file0 = Environment.getExternalStorageDirectory().absolutePath
val file1 = File("${file0}/MyDownload")
if (!file1.exists() || !file1.isDirectory) {
file1.mkdirs()
}
return file1
}
下载的逻辑
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import okhttp3.*
import retrofit2.HttpException
import java.io.*
import java.net.UnknownHostException
/**
* 作者:Lambert
* 时间:2022-09-08
*/
object DownloadGO {
/**
* @param url 文件下载连接
* @param targetParent 需要保存到指定文件夹目录(tips: 是文件夹)
*/
fun download(url: String, targetParent: String): Flow<DownloadStatus> {
var bufferedInputStream: BufferedInputStream? = null
var outputStream: FileOutputStream? = null
var inputStream: InputStream? = null
return flow {
//下载前先检查 是否已经存在本地了
val localFile = checkDownloadFile(url, targetParent)
if (localFile != null) {
//这里表示本地已经存在了已经下载好的文件
LogUtil.d("文件已经存在")
emit(DownloadStatus.onSuccess(localFile))
} else {
//执行下载
val request = Request.Builder().url(url).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
val code = response.code
LogUtil.d("download: code :${code}")
//网络有响应成功,且body 不为空
LogUtil.d("download: response.isSuccessful :${response.isSuccessful}")
if (response.isSuccessful ) {
saveFile(url, targetParent, response).collect {
emit(it)
}
} else {
//请求失败的情况
if (code == 404) {
emit(DownloadStatus.onUrlUnkownFail())
}
emit(DownloadStatus.onFail(null))
}
}
}.catch { error ->
//捕获到异常了
LogUtil.d("catch:error:${error}")
if (error is UnknownHostException) {
//无网络
emit(DownloadStatus.onNetFail())
}
emit(DownloadStatus.onFail(null))
}.onCompletion {
bufferedInputStream?.close()
outputStream?.close()
inputStream?.close()
}.flowOn(Dispatchers.IO)
}
fun saveFile(
url: String,
targetParent: String,
response: Response,
): Flow<DownloadStatus> {
var bufferedInputStream: BufferedInputStream? = null
var outputStream: FileOutputStream? = null
var inputStream: InputStream? = null
LogUtil.d("1-2")
return flow {
val body = response.body
val contentLength:Long = body!!.contentLength()
inputStream = body!!.byteStream()
LogUtil.d("contentLength:${contentLength}")
//使用临时文件保存
val tmpFile = getFileTmpName(url)
val file = File(targetParent, tmpFile)
outputStream = FileOutputStream(file)
val bufferSize = 1024 * 8
val buffer = ByteArray(bufferSize)
bufferedInputStream = BufferedInputStream(inputStream, bufferSize)
var readLength: Int
var currentLength = 0L
var oldProgress = 0L
while (bufferedInputStream!!.read(buffer, 0, bufferSize)
.also { readLength = it } != -1
) {
outputStream!!.write(buffer, 0, readLength)
currentLength += readLength
val currentProgress = currentLength * 100 / contentLength
// LogUtil.d("currentProgress:${currentProgress} \n currentLength:${currentLength}")
if (currentProgress - oldProgress >= 1) {
oldProgress = currentProgress
emit(DownloadStatus.onProgress(currentProgress))
}
}
emit(DownloadStatus.onProgress(100L))
//修改文件名字
val newFile = updataFile(file, targetParent, url)
//返回下载成功
emit(DownloadStatus.onSuccess(newFile))
}.onCompletion {
bufferedInputStream?.close()
outputStream?.close()
inputStream?.close()
}.catch { error ->
LogUtil.d("error:${error}")
}
}
fun updataFile(oldFile: File, targetPath: String, downloadUrl: String): File {
val fileType = downloadUrl.substring(downloadUrl.lastIndexOf("."))
LogUtil.d("fileType:${fileType}")
var fileName = Md5.md5String(downloadUrl);
fileName = "${fileName}${fileType}"
//修改文件名为正式的
val newPathFile = File(targetPath, fileName)
oldFile.renameTo(newPathFile)
return if (newPathFile.exists()) {
newPathFile
} else {
oldFile
}
}
//检查要下载的文件是否已经存在本地了
private fun checkDownloadFile(fileUrl: String, outputFile: String): File? {
val fileType = fileUrl.substring(fileUrl.lastIndexOf("."))
var fileName = Md5.md5String(fileUrl);
fileName = "${fileName}${fileType}"
val localFile = File(outputFile, fileName)
return if (localFile.exists()) {
localFile
} else {
null
}
}
//临时文件
private fun getFileTmpName(fileUrl: String): String {
return "${Md5.md5String(fileUrl)}.tmp"
}
}
sealed class DownloadStatus {
/**
* 下载进度
*/
data class onProgress(val progress: Long?) : DownloadStatus()
/**
* 下载成功
*/
data class onSuccess(val file: File?) : DownloadStatus()
/**
* 网络错误(网络断开或未连接)
*/
class onNetFail() : DownloadStatus()
/**
* 下载地址错误
*/
class onUrlUnkownFail() : DownloadStatus()
/**
* 下载错误
*/
data class onFail(val error: Throwable?) : DownloadStatus()
}
/**
* 作者 : Lambert
* 时间:2022-09-08
*/
class Md5 {
companion object{
fun md5String(text: String): String {
try {
//获取md5加密对象
val instance: MessageDigest = MessageDigest.getInstance("MD5")
//对字符串加密,返回字节数组
val digest:ByteArray = instance.digest(text.toByteArray())
var sb = StringBuffer()
for (b in digest) {
//获取低八位有效值
var i :Int = b.toInt() and 0xff
//将整数转化为16进制
var hexString = Integer.toHexString(i)
if (hexString.length < 2) {
//如果是一位的话,补0
hexString = "0$hexString"
}
sb.append(hexString)
}
return sb.toString()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
return ""
}
}
}
LogUtil
class LogUtil {
companion object{
private const val TAG = "日志"
fun d(content:Any){
d(TAG,content)
}
fun d(tag:String,content:Any){
if (BuildConfig.DEBUG) {
Log.d(tag, content as String)
}
}
@JvmStatic
fun e(content:Any){
e(TAG,content)
}
fun e(tag:String,content:Any){
if (BuildConfig.DEBUG) {
Log.e(tag, content as String)
}
}
}
}
首次下载
再次下载同一个文件的时候