不积跬步,无以至千里;不积小流,无以成江海。要沉下心来,诗和远方的路费真的很贵!
【Kotlin】OkHttp框架实现网络下载
需求
-
对网络上的资源进行下载,下载的文件保存在本地mnt/sdcard/Download 文件夹。 -
显示下载的进度和下载是否成功的提示。 -
多线程下载,一个线程下载一张图片或者一个视频。 -
只有下载完成后,才可以显示和播放。
思路
-
总共分为三步:
-
检查权限。无权限,则进行权限申请;有权限,进行下一步。 -
有权限后,获取到网络资源,形成流文件。 -
将流文件写入磁盘,保存到本地。
实现
实现单线程下载功能
- 在
Manifest 文件中加入权限。
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- 配置网络请求资源的路径。
package com.example.appvideo
class Config {
object API {
const val URL_GET_DOWNLOAD_FILE = "https://img-blog.csdnimg.cn/20200614120050920.JPG"
const val URL_GET_DOWNLOAD_FILE1 = "https://img-blog.csdnimg.cn/20200616204912371.JPG"
const val URL_GET_DOWNLOAD_FILE2 = "https://img-blog.csdnimg.cn/20200614120120405.JPG"
const val URL_GET_DOWNLOAD_FILE3 = "https://img-blog.csdnimg.cn/20200614120120401.JPG"
const val URL_GET_DOWNLOAD_FILE4 = "https://img-blog.csdnimg.cn/2020061412003655.JPG"
const val URL_GET_DOWNLOAD_FILE5 = "https://img-blog.csdnimg.cn/20200614115943345.JPG"
}
}
- 界面布局文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_start_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始下载"/>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="准备开始下载"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:max="100" />
</LinearLayout>
- 单线程下载逻辑类。
package com.example.appvideo
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Message
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import kotlinx.android.synthetic.main.activity_download.*
import okhttp3.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
class DownloadActivity : AppCompatActivity() {
companion object {
private const val REQUEST_EXTERNAL_STORAGE = 101
private val PERMISSIONS_STORAGE = arrayOf(
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"
)
private const val DO_DOWNLOADING = 0
private const val DOWNLOAD_SUCCESS = 1
private const val DOWNLOAD_FAILED = -1
}
private var okHttpClient: OkHttpClient? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_download)
okHttpClient = OkHttpClient()
btn_start_download.setOnClickListener {
startDownLoad()
}
}
var handler: Handler = object : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
DOWNLOAD_FAILED, DOWNLOAD_SUCCESS -> {
val result = msg.obj as String
tv_result?.text = result
}
DO_DOWNLOADING -> {
progressBar.progress = msg.obj as Int
val progress = "已下载" + msg.obj + "%"
tv_result?.text = progress
}
}
}
}
private fun checkPermission() {
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Toast.makeText(this, "请开通相关权限,否则无法正常使用本应用!", Toast.LENGTH_SHORT).show()
}
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE)
} else {
Toast.makeText(this, "已授权!", Toast.LENGTH_SHORT).show()
getResourceFromInternet(Config.API.URL_GET_DOWNLOAD_FILE1)
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_EXTERNAL_STORAGE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "授权成功!", Toast.LENGTH_SHORT).show()
getResourceFromInternet(Config.API.URL_GET_DOWNLOAD_FILE3)
} else {
Toast.makeText(this, "授权被拒绝!", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun startDownLoad() {
checkPermission()
}
private fun getResourceFromInternet(path: String) {
val request = Request.Builder()
.url(path)
.build()
okHttpClient?.newCall(request)?.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
val msg = handler.obtainMessage(DOWNLOAD_FAILED, "下载失败")
handler.sendMessage(msg)
}
override fun onResponse(call: Call, response: Response) {
writeToSDCard(response)
}
})
}
private fun writeToSDCard(response: Response) {
val savePath = Environment.getExternalStorageDirectory().absolutePath + "/Download/"
val dir = File(savePath)
if (!dir.exists()) {
dir.mkdirs()
}
val sb = StringBuilder()
sb.append(System.currentTimeMillis()).append(".jpg")
val fileName = sb.toString()
val file = File(dir, fileName)
var inputStream: InputStream? = null
var fileOutputStream: FileOutputStream? = null
val fileReader = ByteArray(4096)
val fileSize = response.body()!!.contentLength()
var sum: Long = 0
inputStream = response.body()?.byteStream()
fileOutputStream = FileOutputStream(file)
var read: Int
while (inputStream?.read(fileReader).also { read = it!! } != -1) {
fileOutputStream.write(fileReader, 0, read)
sum += read.toLong()
Log.e("msg", "downloaded $sum of $fileSize")
val progress = (sum * 1.0 / fileSize * 100).toInt()
val msg = handler.obtainMessage(DO_DOWNLOADING, progress)
handler.sendMessage(msg)
}
fileOutputStream.flush()
val msg = handler.obtainMessage(DOWNLOAD_SUCCESS, "下载成功")
handler.sendMessage(msg)
inputStream?.close()
fileOutputStream?.close()
}
}
实现多线程下载功能
package com.example.appvideo.download
import android.content.Context
import android.os.Environment
import android.os.Looper
import android.util.Log
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicInteger
class DownloadByManyThread {
private var url: String? = null
private var connectionTimeout: Int
private var method: String?
private var range: Int
private var context: Context
private val cachePath = "imgs"
private var connection: HttpURLConnection? = null
private var suffix: String? = null
constructor(context: Context, suffix: String?) {
connectionTimeout = 500
method = "GET"
range = 0
this.context = context
this.suffix = suffix
}
fun url(url: String?): DownloadByManyThread {
this.url = url
return this
}
constructor(builder: Builder) {
url = builder.url
connectionTimeout = builder.connectionTimeout
method = builder.method
range = builder.range
context = builder.context
}
class Builder(val context: Context) {
var url: String? = null
var connectionTimeout = 0
var method: String? = null
var range = 0
fun url(url: String?): Builder {
this.url = url
return this
}
fun timeout(ms: Int): Builder {
connectionTimeout = ms
return this
}
fun method(method: String): Builder {
if (!(method.toUpperCase() == "GET" || method.toUpperCase() == "POST")) {
throw AssertionError("Assertion failed")
}
this.method = method
return this
}
fun start(range: Int): Builder {
this.range = range
return this
}
fun build(): DownloadByManyThread {
return DownloadByManyThread(this)
}
}
private interface DownloadListener {
fun onSuccess(file: File?)
fun onError(msg: String?)
}
private class DownloadThread(private val url: String?, private val startPos: Long, private val endPos: Long, private val maxFileSize: Long, private val file: File) : Thread() {
private var randomAccessFile: RandomAccessFile? = null
private val connectionTimeout = 5 * 1000
private val method = "GET"
private var listener: DownloadListener? = null
fun setDownloadListener(listener: DownloadListener?) {
this.listener = listener
}
override fun run() {
Log.e(TAG, "=========> " + currentThread().name)
var connection: HttpURLConnection? = null
var url_c: URL? = null
try {
randomAccessFile = RandomAccessFile(file, "rwd")
randomAccessFile!!.seek(startPos)
url_c = URL(url)
connection = url_c.openConnection() as HttpURLConnection
connection.connectTimeout = connectionTimeout
connection.requestMethod = method
connection.setRequestProperty("Charset", "UTF-8")
connection.setRequestProperty("accept", "**")
connection!!.connect()
val contentLength = connection!!.contentLength
if (contentLength < 0) {
Log.e(TAG, "Download fail!")
return
}
val step = contentLength / maximumPoolSize
Log.e(TAG, "maximumPoolSize: $maximumPoolSize , step:$step")
Log.e(TAG, "contentLength: $contentLength")
val sb = StringBuilder()
sb.append(System.currentTimeMillis()).append(suffix)
val file = File(path, sb.toString())
if (contentLength.toLong() == file.length()) {
Log.e(TAG, "Nothing changed!")
return
}
for (i in 0 until maximumPoolSize) {
if (i != maximumPoolSize - 1) {
val downloadThread = DownloadThread(url, (i * step).toLong(), ((i + 1) * step - 1).toLong(), contentLength.toLong(), file)
executor!!.execute(downloadThread)
} else {
val downloadThread = DownloadThread(url, (i * step).toLong(), contentLength.toLong(), contentLength.toLong(), file)
downloadThread.setDownloadListener(object : DownloadListener {
override fun onSuccess(file: File?) {
Log.e(TAG, "onSuccess: ")
}
override fun onError(msg: String?) {
Log.e(TAG, "onError: ")
}
})
executor!!.execute(downloadThread)
}
}
} catch (e: IOException) {
Log.e(TAG, "Download bitmap failed.", e)
e.printStackTrace()
} finally {
if (connection != null) connection!!.disconnect()
}
}
private fun buildPath(filePath: String): File {
val flag = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
val cachePath: String
cachePath = if (flag) context.externalCacheDir!!.path else context.cacheDir.path
val directory = File(cachePath + File.separator + filePath)
if (!directory.exists()) directory.mkdirs()
return directory
}
companion object {
private const val TAG = "Downloader"
private val corePoolSize = Runtime.getRuntime().availableProcessors() + 1
private val maximumPoolSize = Runtime.getRuntime().availableProcessors() * 2 + 1
private val mThreadFactory: ThreadFactory = object : ThreadFactory {
private val mCount = AtomicInteger(1)
override fun newThread(r: Runnable): Thread {
return Thread(r, "Thread#" + mCount.getAndIncrement())
}
}
}
init {
executor = ThreadPoolExecutor(corePoolSize, maximumPoolSize, 10L,
TimeUnit.SECONDS,
LinkedBlockingDeque(),
mThreadFactory)
}
}
val url2 = "https://img-blog.csdnimg.cn/20200614120050920.JPG"
Thread(Runnable {
val downloader = DownloadByManyThread(this@DownloadActivity, ".jpg")
downloader.url(url2).download()
}).start()
|