断点续传/下载,在网络情况不好的时候,可以在断开连接以后,仅继续获取部分内容。假如手机在下载文件的时候下载了80%,某些原因断网了,如果不支持范围请求,那就只有被迫重头开始下载。但是如果有范围请求的加持,就只需要下载最后 5% 的资源,避免重新下载。
记录 App更新的几个主要功能模块,包含
断点续传/下载需要使用到 java.io.RandomAccessFile 类,RandomAccessFile 的实例支持读取和写入随机访问文件,它也可以 seek(long pos)
设置从此文件的开头开始测量的文件指针偏移量,在该位置进行下一次读取或写入操作。简单点说就是可以通过seek(long pos)
方法跳过pos
字节开始写入字节。
同时还需要,在HTTP/1.1中,可以声明了一个响应头部 Access-Ranges
来标记是否支持范围请求,它只有一个可选参数 bytes,
例如这里给了一个下载APK的响应头,可以看到它是有 Accept-Ranges:bytes
来标记的,有此 标记标识当前资源支持范围请求。
如果已经确定双端都支持范围请求,我们就可以在请求资源的时候使用它。
所有的文件最终都是存储在磁盘或者内存中的字节,对于待操作的文件可以将其以字节为单位分割。这样只需要 HTTP 支持请求该文件从 n 到 n+x 这个范围内的资源,就可以实现范围请求了。
HTTP/1.1 中定义了一个 Ranges
的请求头,来指定请求实体的范围。它的范围取值是在 0-Content-Length
之间,使用 -
分割。。
例如已经下载了 1000 bytes 的资源内容,想接着继续下载之后的资源内容,只要在 HTTP 请求头部,增加 Ranges:bytes=1000-
就可以了。
Range 还有几种不同的方式来限定范围,可以根据需要灵活定制:
来通过一个实例来验证一下
几个地方为重点,网络请求添加了一个.addHeader("Range", "bytes=$currentFileLength-")
,currentFileLength为当前文件字节大小。比如当前需要下载的文件大小为1000个字节,.addHeader("Range", "bytes=500-")
表示只请求服务器500-1000的字节,后续回调中的response.body().contentLength()
大小为 500
val request = Request.Builder()
.addHeader("Range", "bytes=$currentFileLength-")
.url(apkUrl)
.build()
// 创建RandomAccessFile实例,并设置跳过currentFileLength个字节
val randomAccessFile = RandomAccessFile(apkFile, "rw")
randomAccessFile.seek(currentFileLength)
// apkFile为下载文件的绝对路径
var inputStream: InputStream? = null
var bufferedInputStream: BufferedInputStream? = null
var randomAccessFile: RandomAccessFile? = null
try {
inputStream = responseBody.byteStream()
bufferedInputStream = BufferedInputStream(inputStream, size)
// 创建RandomAccessFile实例,并设置跳过currentFileLength个字节
randomAccessFile = RandomAccessFile(apkFile, "rw")
randomAccessFile.seek(currentFileLength)
var len = 0
while (bufferedInputStream.read(buffer).apply { len = this } != -1) {
randomAccessFile.write(buffer, 0, len)
currentFileLength += len.toLong()
// 通过EventBus回调下载进度
callbackProgress(currentFileLength, totalLength)
}
// 通过EventBus回调下载进度
callbackProgress(currentFileLength, totalLength)
Log.d(TAG, "下载完成: ${apkFile.absoluteFile}")
} catch (e: IOException) {
e.printStackTrace()
Log.e(TAG, e.message, e)
} finally {
bufferedInputStream?.close()
inputStream?.close()
randomAccessFile?.close()
responseBody.close()
call.cancel()
}
/**
* 通过EventBus回调下载进度
*/
private fun callbackProgress(currentLength: Long, totalLength: Long) {
val progress = (currentLength * 1.0f / totalLength * 100).toInt()
val hasSubscriberEvent = EventBus.getDefault().hasSubscriberForEvent(Progress::class.java)
if (hasSubscriberEvent) {
EventBus.getDefault().post(Progress(progress, currentLength, totalLength))
}
Log.d(TAG, "--->>> progress = $progress $currentLength / $totalLength")
}
// 在需要显示进度的地方,通过EventBus注册和反注册
EventBus.getDefault().register(this)
EventBus.getDefault().unregister(this)
// 接收回调
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventDownloadProgress(progress: Progress) {
progress_bar.progress = progress.progress
progress_text.text = "${progress.progress}%"
}
// Progress实体类
data class Progress(val progress: Int, val currentLength: Long, val totalLength: Long)
首先一些必要权限需要添加在AndroidManifest.xml
中,在Android 6.0之后一些危险权限需要动态申请,Android 8.0还有一个安装未知应用权限需要请求打开。Android 7.0后 SDCard文件访问
需要在AndroidManifest.xml
配置
,需要在res/
目录下新创建xml
目录
// 分别是 网络、SDCard读、SDCard写、API
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
// file_paths.xml,添加到 res/xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external-path"
path="." />
<external-cache-path
name="external_cache"
path="." />
<cache-path
name="cache"
path="." />
<files-path
name="files_path"
path="." />
<external-files-path
name="external_files_path"
path="." />
</paths>
interface InstallPermissionCallBack {
// 已授权
fun onGranted()
// 未授权
fun onDenied()
}
/**
* 检查权限
*/
fun checkInstallPermission(context: Context, callback: InstallPermissionCallBack) {
if (hasInstallPermission(context)) {
callback.onGranted()
} else {
openInstallPermissionSetting(context, callback)
}
}
/**
* 是否有安装未知来源应用的权限
*/
fun hasInstallPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return context.packageManager.canRequestPackageInstalls()
} else false
}
/**
* 检查 android 8.0 及以上是否有未知应用安装权限,无则打开未知应用安装权限授权界面
*/
fun openInstallPermissionSetting(context: Context, callback: InstallPermissionCallBack) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + context.packageName))
// 封装startActivityForResult和onActivityResult,用于请求后能接收到回调
ActivityLauncher.init(context as FragmentActivity).startActivityForResult(intent, object : ActivityLauncher.Callback {
override fun onActivityResult(resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
callback.onGranted()
} else {
callback.onDenied()
}
}
})
} else {
callback.onGranted()
}
}
/**
* 安装apk,兼容7.0
*/
fun installApk(context: Context, file: File) {
val intent = Intent(Intent.ACTION_VIEW)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.fileProvider", file)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
import android.support.v4.app.Fragment
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.SparseArray
import java.util.*
class RouterFragment : Fragment() {
private val mCallbacks = SparseArray<ActivityLauncher.Callback>()
private val mCodeGenerator = Random()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
fun startActivityForResult(intent: Intent, callback: ActivityLauncher.Callback) {
val requestCode = makeRequestCode()
mCallbacks.put(requestCode, callback)
startActivityForResult(intent, requestCode)
}
private fun makeRequestCode(): Int {
var requestCode: Int
var tryCount = 0
do {
requestCode = mCodeGenerator.nextInt(0x0000FFFF)
tryCount++
} while (mCallbacks.indexOfKey(requestCode) >= 0 && tryCount < 10)
return requestCode
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("zz", "requestCode = $requestCode resultCode = $resultCode")
val callback = mCallbacks.get(requestCode)
Log.d("zz", "callback = $callback")
mCallbacks.remove(requestCode)
callback?.onActivityResult(resultCode, data)
}
companion object {
fun newInstance(): RouterFragment = RouterFragment()
}
}
import android.content.Context
import android.content.Intent
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
class ActivityLauncher private constructor(private val context: Context) {
companion object {
private const val KEY_FRAGMENT_TAG = "ActivityLauncher"
fun init(activity: FragmentActivity): ActivityLauncher {
return ActivityLauncher(activity)
}
}
private fun getRouterFragment(activity: FragmentActivity): Fragment {
return activity.supportFragmentManager.findFragmentByTag(KEY_FRAGMENT_TAG) ?: let {
val routerFragment = RouterFragment.newInstance()
val fragmentManager = activity.supportFragmentManager
fragmentManager.beginTransaction()
.add(routerFragment, KEY_FRAGMENT_TAG)
.commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
return routerFragment
}
}
fun startActivityForResult(intent: Intent, callback: Callback) {
(getRouterFragment(context as FragmentActivity) as RouterFragment)
.startActivityForResult(intent, callback)
}
interface Callback {
fun onActivityResult(resultCode: Int, data: Intent?)
}
}
ActivityLauncher.init(context as FragmentActivity).startActivityForResult(intent, object : ActivityLauncher.Callback {
override fun onActivityResult(resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
} else {
}
}
})
// 用于EventBus回调下载进度
data class Progress(val progress: Int, val currentLength: Long, val totalLength: Long)
import android.content.Context
import android.content.Intent
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
class ActivityLauncher private constructor(private val context: Context) {
companion object {
private const val KEY_FRAGMENT_TAG = "ActivityLauncher"
fun init(activity: FragmentActivity): ActivityLauncher {
return ActivityLauncher(activity)
}
}
private fun getRouterFragment(activity: FragmentActivity): Fragment {
return activity.supportFragmentManager.findFragmentByTag(KEY_FRAGMENT_TAG) ?: let {
val routerFragment = RouterFragment.newInstance()
val fragmentManager = activity.supportFragmentManager
fragmentManager.beginTransaction()
.add(routerFragment, KEY_FRAGMENT_TAG)
.commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
return routerFragment
}
}
fun startActivityForResult(intent: Intent, callback: Callback) {
(getRouterFragment(context as FragmentActivity) as RouterFragment)
.startActivityForResult(intent, callback)
}
interface Callback {
fun onActivityResult(resultCode: Int, data: Intent?)
}
}
import android.support.v4.app.Fragment
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.SparseArray
import java.util.*
class RouterFragment : Fragment() {
private val mCallbacks = SparseArray<ActivityLauncher.Callback>()
private val mCodeGenerator = Random()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
fun startActivityForResult(intent: Intent, callback: ActivityLauncher.Callback) {
val requestCode = makeRequestCode()
mCallbacks.put(requestCode, callback)
startActivityForResult(intent, requestCode)
}
private fun makeRequestCode(): Int {
var requestCode: Int
var tryCount = 0
do {
requestCode = mCodeGenerator.nextInt(0x0000FFFF)
tryCount++
} while (mCallbacks.indexOfKey(requestCode) >= 0 && tryCount < 10)
return requestCode
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("zz", "requestCode = $requestCode resultCode = $resultCode")
val callback = mCallbacks.get(requestCode)
Log.d("zz", "callback = $callback")
mCallbacks.remove(requestCode)
callback?.onActivityResult(resultCode, data)
}
companion object {
fun newInstance(): RouterFragment = RouterFragment()
}
}
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import android.support.v4.app.FragmentActivity
import android.support.v4.content.FileProvider
import android.util.Log
import android.widget.Toast
import com.wazing.myapplication.utils.ActivityLauncher
import okhttp3.*
import org.greenrobot.eventbus.EventBus
import java.io.*
object UpdateManager {
// 模拟下载地址
// const val APK_URL = "http://a7.pc6.com/cx6/pc6_market.apk";
const val APK_URL = "http://a.gdown.baidu.com/data/wisegame/aaae681f1d26c241/linghunmianju_1013.apk"
private const val TAG = "UpdateManager"
private val okHttpClient: OkHttpClient by lazy { OkHttpClient().newBuilder().build() }
fun downloadFile(context: Context, apkUrl: String) {
// 根据app包名创建一个目录
val file = File(Environment.getExternalStorageDirectory(), context.packageName)
if (!file.exists()) {
file.mkdir()
}
// 根据url指定下载文件的位置,文件名取url / 最后边的名字,并拿到当前文件的大小
val apkFile = File(file, apkUrl.substring(apkUrl.lastIndexOf("/") + 1))
var currentFileLength: Long = apkFile.length()
// 给当前请求添加一个header
val request = Request.Builder()
.addHeader("Range", "bytes=$currentFileLength-")
.url(apkUrl)
.build()
// 发起网络请求
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(TAG, "onFailure: " + e.message, e)
}
override fun onResponse(call: Call, response: Response) {
val responseBody = response.body() ?: return
val size = 128 * 8 * 2
val totalLength = responseBody.contentLength() + currentFileLength
val buffer = ByteArray(size)
var inputStream: InputStream? = null
var bufferedInputStream: BufferedInputStream? = null
var randomAccessFile: RandomAccessFile? = null
try {
inputStream = responseBody.byteStream()
bufferedInputStream = BufferedInputStream(inputStream, size)
// 创建RandomAccessFile实例,并设置跳过currentFileLength个字节
randomAccessFile = RandomAccessFile(apkFile, "rw")
randomAccessFile.seek(currentFileLength)
var len = 0
while (bufferedInputStream.read(buffer).apply { len = this } != -1) {
randomAccessFile.write(buffer, 0, len)
currentFileLength += len.toLong()
callbackProgress(currentFileLength, totalLength)
}
callbackProgress(currentFileLength, totalLength)
Log.d(TAG, "下载完成: ${apkFile.absoluteFile}")
(context as Activity).runOnUiThread {
checkInstallPermission(context, object : InstallPermissionCallBack {
override fun onGranted() {
installApk(context, apkFile)
}
override fun onDenied() {
Toast.makeText(context, "未授权", Toast.LENGTH_SHORT).show()
}
})
}
} catch (e: IOException) {
e.printStackTrace()
Log.e(TAG, e.message, e)
} finally {
bufferedInputStream?.close()
inputStream?.close()
randomAccessFile?.close()
responseBody.close()
call.cancel()
}
}
})
}
/**
* 通过EventBus回调下载进度
*/
private fun callbackProgress(currentLength: Long, totalLength: Long) {
val progress = (currentLength * 1.0f / totalLength * 100).toInt()
val hasSubscriberEvent = EventBus.getDefault().hasSubscriberForEvent(Progress::class.java)
if (hasSubscriberEvent) {
EventBus.getDefault().post(Progress(progress, currentLength, totalLength))
}
Log.d(TAG, "--->>> progress = $progress $currentLength / $totalLength")
}
fun checkInstallPermission(context: Context, callback: InstallPermissionCallBack) {
if (hasInstallPermission(context)) {
callback.onGranted()
} else {
openInstallPermissionSetting(context, callback)
}
}
/**
* 是否有安装未知来源应用的权限
*/
fun hasInstallPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return context.packageManager.canRequestPackageInstalls()
} else false
}
/**
* 检查 android 8.0 及以上是否有未知应用安装权限,无则打开未知应用安装权限授权界面
*/
fun openInstallPermissionSetting(context: Context, callback: InstallPermissionCallBack) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + context.packageName))
ActivityLauncher.init(context as FragmentActivity).startActivityForResult(intent, object : ActivityLauncher.Callback {
override fun onActivityResult(resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
callback.onGranted()
} else {
callback.onDenied()
}
}
})
} else {
callback.onGranted()
}
}
/**
* 安装apk,兼容7.0
*/
fun installApk(context: Context, file: File) {
val intent = Intent(Intent.ACTION_VIEW)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.fileProvider", file)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
interface InstallPermissionCallBack {
fun onGranted()
fun onDenied()
}
}
import android.Manifest
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.content.pm.PackageManager
import android.support.v4.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), 1)
}
download_file.setOnClickListener {
UpdateManager.downloadFile(this, UpdateManager.APK_URL)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventDownloadProgress(progress: Progress) {
progress_bar.progress = progress.progress
progress_text.text = "${progress.progress}%"
}
override fun onResume() {
super.onResume()
EventBus.getDefault().register(this)
}
override fun onPause() {
super.onPause()
EventBus.getDefault().unregister(this)
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.wazing.myapplication">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
provider>
application>
manifest>
参考:
https://github.com/square/okhttp
https://segmentfault.com/a/1190000015732010