org.xutils.http.loader.FileLoader
import android.text.TextUtils;
import org.xutils.cache.DiskCacheEntity;
import org.xutils.cache.DiskCacheFile;
import org.xutils.cache.LruDiskCache;
import org.xutils.common.Callback;
import org.xutils.common.util.IOUtil;
import org.xutils.common.util.LogUtil;
import org.xutils.common.util.ProcessLock;
import org.xutils.ex.FileLockedException;
import org.xutils.ex.HttpException;
import org.xutils.http.RequestParams;
import org.xutils.http.request.UriRequest;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Date;
/**
* @author 注释者:王教成
* @version 注释版:1.0.0
* 文件加载器
* 下载参数策略:
* 1.RequestParams#saveFilePath不为空时,目标文件保存在saveFilePath;否则由Cache策略分配文件下载路径
* 2.下载时临时目标文件路径为tempSaveFilePath,下载完后进行a:CacheFile#commit;b:重命名等操作
* 断点下载策略:
* 1.要下载的目标文件不存在或小于CHECK_SIZE时删除目标文件,重新下载
* 2.若文件存在且大于CHECK_SIZE,range=fileLen-CHECK_SIZE,校验check_buffer,相同:继续下载;不相同:删掉目标文件,并抛出RuntimeException(HttpRetryHandler会使下载重新开始)
*/
public class FileLoader extends Loader<File> {
private static final int CHECK_SIZE = 512;//创建检查尺寸512
private String tempSaveFilePath;//声明临时保存文件路径
private String saveFilePath;//声明保存文件路径
private boolean isAutoResume;//是否自动恢复
private boolean isAutoRename;//是否自动重命名
private long contentLength;//声明内容长度
private String responseFileName;//声明响应文件名称
private DiskCacheFile diskCacheFile;//声明磁盘缓存文件
/**
* 创建新实例
* @return 返回文件加载器
*/
@Override
public Loader<File> newInstance() {
return new FileLoader();
}
/**
* 设置请求参数
* @param params 请求参数
*/
@Override
public void setParams(final RequestParams params) {
if (params != null) {
this.params = params;//如果请求参数非空则赋值
isAutoResume = params.isAutoResume();//获取请求参数是否自动恢复
isAutoRename = params.isAutoRename();//获取请求参数是否自动重命名
}
}
/**
* 加载输入流
* @param in 输入流
* @return 返回文件
* @throws Throwable 抛出异常
*/
@Override
public File load(final InputStream in) throws Throwable {
File targetFile = null;//声明目标文件
BufferedInputStream bis = null;//声明缓冲输入流
BufferedOutputStream bos = null;//声明缓冲输出流
try {
targetFile = new File(tempSaveFilePath);//以临时保存文件路径创建目标文件
if (targetFile.isDirectory()) {
IOUtil.deleteFileOrDir(targetFile);//如果目标文件是目录,删除目标文件目录
}//防止文件正在写入时,父文件夹被删除,继续写入时造成偶现文件节点异常问题
if (!targetFile.exists()) {
File dir = targetFile.getParentFile();//如果目标文件不存在,获取目标文件父目录
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("can not create dir: " + dir.getAbsolutePath());//如果父目录不存在且未创建,抛出输入输出异常
}
}
//处理【断点逻辑2】(见文件头doc)代码块
long targetFileLen = targetFile.length();//获取目标文件长度
if (isAutoResume && targetFileLen > 0) {
FileInputStream fis = null;//如果自动恢复且目标文件长度大于0,声明文件输入流
try {
long filePos = targetFileLen - CHECK_SIZE;//目标文件长度减512检查尺寸,作为文件位置
if (filePos > 0) {
fis = new FileInputStream(targetFile);//如果文件位置位于0,以目标文件创建文件输入流
byte[] fileCheckBuffer = IOUtil.readBytes(fis, filePos, CHECK_SIZE);//用文件输入流创建文件检查缓冲字节数组
byte[] checkBuffer = IOUtil.readBytes(in, 0, CHECK_SIZE);//用输入流创建检查缓冲字节数组
if (!Arrays.equals(checkBuffer, fileCheckBuffer)) {//先关闭文件流,否则文件删除会失败
IOUtil.closeQuietly(fis);//如果字节数组比较不相同,关闭文件输入流
IOUtil.deleteFileOrDir(targetFile);//删除目标文件
throw new RuntimeException("need retry");//抛出运行时异常
} else {
contentLength -= CHECK_SIZE;//如果字节数组比较相同,内容长度叠减检查尺寸512
}
} else {
IOUtil.deleteFileOrDir(targetFile);//如果文件位置不大于0,删除目标文件
throw new RuntimeException("need retry");//抛出运行时异常
}
} finally {
IOUtil.closeQuietly(fis);//关闭文件输入流
}
}
//开始下载代码块
long current = 0;//创建当前初始值0
FileOutputStream fileOutputStream = null;//声明文件输出流
if (isAutoResume) {
current = targetFileLen;//如果自动恢复,目标文件长度赋值当前值
fileOutputStream = new FileOutputStream(targetFile, true);//用目标文件创建文件输出流(true表示附加)
} else {
fileOutputStream = new FileOutputStream(targetFile);//如果不自动回复,用目标文件创建文件输出流
}
long total = contentLength + current;//内容长度加上当前值作为总计
bis = new BufferedInputStream(in);//用输入流创建缓冲输入流
bos = new BufferedOutputStream(fileOutputStream);//用文件输出流创建缓冲输出流
if (progressHandler != null && !progressHandler.updateProgress(total, current, true)) {
throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新进度失败,抛出已取消异常
}
byte[] tmp = new byte[4096];//创建4k缓存字节数组
int len;//声明长度
while ((len = bis.read(tmp)) != -1) {
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();//如果从缓冲输入流读取缓存长度不等于-1则迭代,如果目标文件父目录不存在,创建父目录
throw new IOException("parent be deleted!");//抛出输入输出异常
}//防止父文件夹被其他进程删除,继续写入时造成父文件夹变为0字节文件的问题
bos.write(tmp, 0, len);//写入缓存输出流
current += len;//叠加已读写长度
if (progressHandler != null) {
if (!progressHandler.updateProgress(total, current, false)) {
bos.flush();//如果进度操作器非空且更新进度失败,刷新且强制输出缓存输入流
throw new Callback.CancelledException("download stopped!");//抛出已取消异常
}
}
}
bos.flush();//刷新且强制输出缓存输入流
//处理【下载逻辑2.a】(见文件头doc)代码块
if (diskCacheFile != null) {
targetFile = diskCacheFile.commit();//如果磁盘缓存文件非空,提交作为目标文件
}
if (progressHandler != null) {
progressHandler.updateProgress(total, current, true);//如果进度操作器非空,更新进度
}
} finally {
IOUtil.closeQuietly(bis);//关闭缓冲输入流
IOUtil.closeQuietly(bos);//关闭缓冲输出流
}
return autoRename(targetFile);//返回自动重命名目标文件
}
/**
* 加载Uri请求参数
* @param request Uri请求参数
* @return 返回文件
* @throws Throwable 抛出异常
*/
@Override
public File load(final UriRequest request) throws Throwable {
ProcessLock processLock = null;//声明进度锁
File result = null;//声明结果文件
try {
//处理【下载逻辑1】(见文件头doc)代码块
saveFilePath = params.getSaveFilePath();//请求参数获取保存文件路径
diskCacheFile = null;//磁盘缓存文件初始为null
if (TextUtils.isEmpty(saveFilePath)) {
if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {
throw new Callback.CancelledException("download stopped!");//如果保存文件路径非空,并且进度操作器非空且更新进度失败,抛出已取消异常
}
initDiskCacheFile(request);//以Uri请求参数初始化磁盘缓存文件;保存路径为空,存入磁盘缓存
} else {
tempSaveFilePath = saveFilePath + ".tmp";//如果保存文件路径为空,加后缀成为缓存保存文件路径
}
if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {
throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新失败,抛出已取消异常
}
processLock = ProcessLock.tryLock(saveFilePath + "_lock", true);//获取进度锁
if (processLock == null || !processLock.isValid()) {
throw new FileLockedException("download exists: " + saveFilePath);//如果进度锁为空或无效,抛出文件锁异常
}//等待若不能下载则取消此次下载
params = request.getParams();//获取请求参数
{
long range = 0;//初始范围值0
if (isAutoResume) {
File tempFile = new File(tempSaveFilePath);//如果允许自动恢复,以缓存保存文件路径创建缓存文件
long fileLen = tempFile.length();//获取缓存文件长度
if (fileLen <= CHECK_SIZE) {
IOUtil.deleteFileOrDir(tempFile);//如果文件长度小于等于检查尺寸512,删除缓存文件
range = 0;//范围值设为0
} else {
range = fileLen - CHECK_SIZE;//如果文件长度大于检查尺寸512,设置范围值为文件长度减检查尺寸
}
}
params.setHeader("RANGE", "bytes=" + range + "-");//请求参数设置头;retry时需要覆盖RANGE参数
}//处理【断点逻辑1】(见文件头doc)代码块
if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {
throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新进度失败,抛出已取消异常
}
request.sendRequest();//发送请求;可能抛出网络异常
contentLength = request.getContentLength();//Uri请求参数获取内容长度
if (isAutoRename) {
responseFileName = getResponseFileName(request);//如果允许自动重命名,以Uri请求参数获取响应文件名
}
if (isAutoResume) {
isAutoResume = isSupportRange(request);//如果允许自动恢复,判断是否支持范围赋值是否自动恢复
}
if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {
throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新进度失败,抛出已取消异常
}
if (diskCacheFile != null) {
DiskCacheEntity entity = diskCacheFile.getCacheEntity();//如果磁盘缓存文件非空,获取磁盘缓存实体
entity.setLastAccess(System.currentTimeMillis());//设置最近访问实际为当前时间
entity.setEtag(request.getETag());//设置电子标签
entity.setExpires(request.getExpiration());//设置到期
entity.setLastModify(new Date(request.getLastModified()));//设置最后修改日期
}
result = this.load(request.getInputStream());//调用本类加载输入流方法获取文件
} catch (HttpException httpException) {
if (httpException.getCode() == 416) {
if (diskCacheFile != null) {
result = diskCacheFile.commit();//捕获网络异常,如果获取代码为416,并且磁盘缓存文件非空,提交磁盘缓存文件
} else {
result = new File(tempSaveFilePath);//如果磁盘缓存文件为空,创建缓存保存文件路径
}
//从缓存获取文件,不rename和断点,直接退出
if (result != null && result.exists()) {
if (isAutoRename) {
responseFileName = getResponseFileName(request);//如果结果文件非空且存在,并且允许自动重命名,获取响应文件名
}
result = autoRename(result);//调用本类中自动重命名方法
} else {
IOUtil.deleteFileOrDir(result);//否则删除结果文件
throw new IllegalStateException("cache file not found" + request.getCacheKey());//抛出非法状态异常
}
} else {
throw httpException;//抛出网络异常
}
} finally {
IOUtil.closeQuietly(processLock);//关闭进度锁
IOUtil.closeQuietly(diskCacheFile);//关闭磁盘缓存文件
}
return result;
}
/**
* 初始化磁盘缓存文件
* @param request Uri请求参数
* @throws Throwable 抛出异常
*/
private void initDiskCacheFile(final UriRequest request) throws Throwable {
DiskCacheEntity entity = new DiskCacheEntity();//创建磁盘缓存实体
entity.setKey(request.getCacheKey());//从Uri请求参数获取缓存键,为磁盘缓存实体设置键
diskCacheFile = LruDiskCache.getDiskCache(params.getCacheDirName()).createDiskCacheFile(entity);//以请求参数获取缓存目录名,再以磁盘缓存实体为参数创建磁盘缓存文件,作为参数获取磁盘缓存
if (diskCacheFile != null) {
saveFilePath = diskCacheFile.getAbsolutePath();//如果磁盘缓存文件非空,获取绝对路径作为保存文件路径
//磁盘缓存文件是缓存路径,磁盘缓存文件的commit方法返回目标文件
tempSaveFilePath = saveFilePath;//保存文件路径作为缓存保存文件路径
isAutoRename = false;//设置不自动重命名
} else {
throw new IOException("create cache file error:" + request.getCacheKey());//如果磁盘缓存文件为空,抛出输入输出异常
}
}//保存路径为空,存入磁盘缓存
/**
* 自动重命名
* @param loadedFile 已加载文件
* @return 返回文件
*/
private File autoRename(File loadedFile) {
if (isAutoRename && loadedFile.exists() && !TextUtils.isEmpty(responseFileName)) {
File newFile = new File(loadedFile.getParent(), responseFileName);//如果允许自动重命名,且已加载文件存在,且响应文件名非空,以响应文件名新建父路径
while (newFile.exists()) {
newFile = new File(loadedFile.getParent(), System.currentTimeMillis() + responseFileName);//如果新文件存在,以当前实际加响应文件名创建父路径
}
return loadedFile.renameTo(newFile) ? newFile : loadedFile;//返回已加载文件重命名为新文件成功,则返回新文件名,否则返回已加载文件名
} else if (!saveFilePath.equals(tempSaveFilePath)) {
File newFile = new File(saveFilePath);//如果缓存保存文件路径与保存文件路径不同,已保存文件路径创建新文件
return loadedFile.renameTo(newFile) ? newFile : loadedFile;//返回已加载文件重命名为新文件成功,则返回新文件名,否则返回已加载文件名
} else {
return loadedFile;//不符合以上条件,直接返回已加载文件
}
}//处理【下载逻辑2.b】(见文件头doc)
/**
* 获取响应文件名
* @param request Uri请求参数
* @return 返回响应文件名或null
*/
private static String getResponseFileName(UriRequest request) {
if (request == null) return null;//如果Uri请求参数为空,返回null
String disposition = request.getResponseHeader("Content-Disposition");//获取Content-Disposition响应头作为响应字符串
if (!TextUtils.isEmpty(disposition)) {
int startIndex = disposition.indexOf("filename=");//如果配置字符串非空,获取filename=索引位置作为开始位置
if (startIndex > 0) {
startIndex += 9;//如果开始位置大于0则增加9:“filename=”.length()
int endIndex = disposition.indexOf(";", startIndex);//获取跳过开始索引位置后,分号索引位置作为结束位置
if (endIndex < 0) {
endIndex = disposition.length();//如果结束位置小于0,获取响应字符串长度
}
if (endIndex > startIndex) {
try {
String name = URLDecoder.decode(
disposition.substring(startIndex, endIndex),
request.getParams().getCharset());//如果结束位置小于开始位置,解析子字符串作为名称
if (name.startsWith("\"") && name.endsWith("\"")) {
name = name.substring(1, name.length() - 1);//如果名称以引号开始结束,从名称获取去引号子字符串
}
return name;//返回名称
} catch (UnsupportedEncodingException ex) {
LogUtil.e(ex.getMessage(), ex);//捕获不支持编码异常,记录日志
}
}
}
}
return null;
}
/**
* 判断是否支持范围
* @param request Uri请求参数
* @return 返回是否支持范围
*/
private static boolean isSupportRange(UriRequest request) {
if (request == null) return false;//如果Uri请求参数为空,返回false
String ranges = request.getResponseHeader("Accept-Ranges");//获取Accept-Ranges响应头
if (ranges != null) {
return ranges.contains("bytes");//如果响应头非空,返回是否包含“bytes”字符串
}
ranges = request.getResponseHeader("Content-Range");//获取获取Accept-Ranges响应头为空,获取Content-Range响应头
return ranges != null && ranges.contains("bytes");//返回是否响应头非空且包含“bytes”字符串
}
/**
* 加载,从磁盘缓存实体
* @param cacheEntity 磁盘缓存实体
* @return 返回文件
* @throws Throwable 抛出异常
*/
@Override
public File loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable {
return LruDiskCache.getDiskCache(params.getCacheDirName()).getDiskCacheFile(cacheEntity.getKey());//获取磁盘缓存,再获取磁盘缓存文件
}
/**
* 保存到缓存实体
* @param request Uri请求参数
*/
@Override
public void save2Cache(final UriRequest request) {
}//实现空方法,已经通过磁盘缓存文件commit方法保存
}