不积跬步,无以至千里;不积小流,无以成江海。要沉下心来,诗和远方的路费真的很贵!
对网络上的资源进行下载,下载的文件保存在本地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"
}
}
<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() {
// 检查权限 是否被授权
// PackageManager.PERMISSION_GRANTED表示已授权过
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()
}
//申请权限 参数1.context 2.具体权限 3.权限码
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)
}
})
}
/**
* 将文件写入SD卡来保存
*/
private fun writeToSDCard(response: Response) {
//决定存放路径
// 1.随着app的消失而消失,外部存储 在mnt/sdcard/Android中
/* val savePath = getExternalFilesDir(null).toString() + File.separator
val fileName = "wj.jpg"
val file = File(save_Path,fileName)*/
// 2.SD卡 不会随着app消失而消失,内部存储
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
/**
* @author lvjunkai
* @DATE 2022/9/5
* @TIME 9:51
* @Description 多线程下载类
*/
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
/**
* 构造函数
* @param context
* @param suffix
*/
constructor(context: Context, suffix: String?) {
connectionTimeout = 500 // 500毫秒
method = "GET"
range = 0
this.context = context
this.suffix = suffix
}
/**
* 获取网址
* @param url
* @return
*/
fun url(url: String?): DownloadByManyThread {
this.url = url
return this
}
/**
* 建造者设计模式
* @param builder
*/
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
}
/**
* 只能GET或者POST方法
* @param method
* @return
*/
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
}
/**
* 初始化build方法
* @return
*/
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 // 5秒钟
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.setRequestProperty("Range", "bytes=$startPos-$endPos")
Log.e(TAG, "Range: bytes=$startPos-$endPos")
val inputStream = connection.inputStream
Log.e(TAG, "connection.getContentLength() == " + connection.contentLength)
val contentLength = connection.contentLength
if (contentLength < 0) {
Log.e(TAG, "Download fail!")
return
}
try {
if (connection.responseCode == 206) {
val buffer = ByteArray(2048)
var len = -1
while (inputStream!!.read(buffer).also { len = it } != -1) {
randomAccessFile!!.write(buffer, 0, len)
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
inputStream?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
} catch (e: IOException) {
Log.e(TAG, "Download bitmap failed.", e)
if (listener != null) listener!!.onError(e.localizedMessage)
e.printStackTrace()
} finally {
connection?.disconnect()
// todo 通知下载完毕
if (endPos == maxFileSize) {
Log.e(TAG, "Download bitmap success.")
if (listener != null) listener!!.onSuccess(file)
}
}
}
}
private var executor: Executor? = null
fun download() {
val path = buildPath(cachePath)
if (Looper.myLooper() == Looper.getMainLooper()) {
throw RuntimeException("Can't visit network from UI thread.")
}
try {
val url1 = URL(url)
connection = url1.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
}
// TODO 分为多个线程,进行执行
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!") // already downlaod.
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 {
// 是否有SD卡
val flag = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
// 如果有SD卡就存在外存,否则就位于这个应用的data/package name/cache目录下
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()