一、联网获取所需下载文件的长度
/**
* 获取服务器文件长度
* @param url 需要下载的文件地址
*/
private Observable<Long> getRemoteFileLength(final String url) {
return Observable.create(new Observable.OnSubscribe<Long>() {
@Override
public void call(Subscriber<? super Long> subscriber) {
Request.Builder builder = new Request.Builder();
builder.addHeader("Accept-Encoding", "identity");
Request request = builder.url(url).build();
try {
Response response = mOkHttpClient.newCall(request).execute();
long l = response.body().contentLength();
updateState.fileLength = l;
subscriber.onNext(l);
} catch (Exception e) {
subscriber.onError(e);
}
}
});
}
二、构建缓存文件(遍历缓存目录里的文件判断文件是否可以断点续传)
/**
* 构建本地文件
* @param context
* @param rootDir 放置下载的文件的根目录
* @param fileLength 所需下载文件的总长度
* @return
*/
private Observable<File> buildTargetFile(final Context context, final File rootDir, final long fileLength) {
return Observable.create(new Observable.OnSubscribe<File>() {
@Override
public void call(Subscriber<? super File> subscriber) {
ActivityManager activityManager = (ActivityManager) context
.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager
.getRunningAppProcesses();
File downsDir = new File(rootDir, "downs");
if (!downsDir.exists() || !downsDir.isDirectory()) {
downsDir.mkdirs();
}
File[] files = downsDir.listFiles();
File lastFile = null;
if (files.length > 0) {
for (File itemFile : files) {
boolean willContinueDownload = pickFile2ContinueDownload(itemFile, processInfos, fileLength);
if (willContinueDownload) {
if (lastFile != null && lastFile.length() < itemFile.length()) {
lastFile = itemFile;
} else {
lastFile = itemFile;
}
}
}
}
//把断点续传的文件改名挂载到自己的进程下
String downloadFileName = "remote_" + getProcessName(Process.myPid(), context) + "_" + Process.myPid() + "_" + fileLength + "_.apk";
File downloadFile = null;
if (lastFile != null) {
if (lastFile.getName().contains(".part")) {
downloadFileName += ".part";
}
downloadFile = new File(downsDir, downloadFileName);
lastFile.renameTo(downloadFile);
} else {
try {
downloadFileName += ".part";
File file = new File(downsDir, downloadFileName);
//lastFile 为空说明没有合适的文件断点续传(正在被进程使用的文件不应该断点续传,容易发生文件复写的bug)
// 构建当前进程名、进程ID和文件长度相关的文件又存在,说明当前正在下载
//这种情况下onNext不应该传file进去下载,忽略本次下载行为
if (!file.exists()) {//目标文件不存在的情况下,构建新的文件,从零开始下载apk
downloadFile = new File(downsDir, downloadFileName);
downloadFile.createNewFile();
}
} catch (IOException e) {
subscriber.onError(e);
e.printStackTrace();
}
}
subscriber.onNext(downloadFile);
}
});
}
判该文件是否可以断点续传的方法
/**
* 判断文件是否可以断点续传
*
* @param file 需要检查文件
* @param processInfos 进程列表
* @param remoteFileLength 服务器文件长度(条件允许最好换成MD5值校验)
*/
private boolean pickFile2ContinueDownload(File file, List<ActivityManager.RunningAppProcessInfo> processInfos, long remoteFileLength) {
if (!file.exists() || !file.isFile()) {
return false;
}
String name = file.getName();
//长度为5 1.remote 2.进程名 3.pid 4.文件长度 5.后缀
String[] items = name.split("_");
//下载以remote_为前缀
if (items.length != 5 || !items[0].equals("remote")) {
return false;
}
//比较文件长度,如果文件长度不一致,那么说明文件不是同一个,不应该断点续传
String lengthStr = items[3];
if (Long.valueOf(lengthStr) != remoteFileLength) {
return false;
}
String processName = items[1];
String pid = items[2];
for (ActivityManager.RunningAppProcessInfo item : processInfos) {
//如果进程名和pid都一样,那么说明该文件有可能正在被使用,不应该作为断点续传文件(容易发生文件重复写入)
if (item.processName.equals(processName) && Integer.valueOf(pid) == item.pid) {
return false;
}
}
return true;
}
三、下载服务器端文件到上文创建的目标文件中
/**
*
* @param url 需要下载的文件地址
* @param targetFile 本地存放文件
* @return
*/
private Observable<File> downloadFile(final String url, final File targetFile) {
return Observable.create(new Observable.OnSubscribe<File>() {
@Override
public void call(Subscriber<? super File> subscriber) {
if (targetFile == null) {
subscriber.onError(new Exception("正在下载"));
return;
}
updateState.currentLength = targetFile.length();
updateState.rangeLength = targetFile.length();
try {
byte[] readBytes = new byte[4096 * 2];
Request.Builder builder = new Request.Builder();
if (targetFile.length() > 0) {
builder.addHeader("Range", "bytes=" + targetFile.length() + "-");
}
builder.addHeader("Accept-Encoding", "identity");
Request request = builder.url(url).build();
Response response = mOkHttpClient.newCall(request).execute();
if (response == null) {
return;
}
InputStream inputStream = response.body().byteStream();
FileOutputStream fos = new FileOutputStream(targetFile.getAbsoluteFile(), true);
while (true) {
int readLength = inputStream.read(readBytes);
if (readLength == -1) {
break;
}
updateState.currentLength += readLength;
fos.write(readBytes, 0, readLength);
}
fos.close();
inputStream.close();
String name = targetFile.getName();
name = name.replace(".part", "");
File file = new File(targetFile.getParentFile(), name);
boolean flag = targetFile.renameTo(file);
if (flag) {
subscriber.onNext(file);
} else {
subscriber.onError(new Exception("下载失败"));
}
} catch (IOException e) {
subscriber.onError(e);
e.printStackTrace();
}
}
});
}
整体调用过程如下
/**
* @param context 下载所用到的上下文
* @param rootDir 根目录
*/
public void download(final String url, final Context context, final File rootDir) {
updateState = new UpdateState();
getRemoteFileLength(url).
flatMap(new Func1<Long, Observable<File>>() {
@Override
public Observable<File> call(Long aLong) {
return buildTargetFile(context, rootDir, aLong);
}
}).flatMap(new Func1<File, Observable<File>>() {
@Override
public Observable<File> call(File file) {
return downloadFile(url, file);
}
}).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.newThread())
.subscribe(new Subscriber<File>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable throwable) {
if (throwable != null) {
throwable.printStackTrace();
}
}
@Override
public void onNext(File file) {
//最后进行文件长度校验,文件长度和文件名标记长度必须一直,如果不一致,则说明下载出错,文件被复写
String name = file.getName();
//长度为5 1.remote 2.进程名 3.pid 4.文件长度 5.后缀
String[] items = name.split("_");
//下载以remote_为前缀
if (items.length != 5 || !items[0].equals("remote")) {
FileUtil.delete(file);
updateState.updateFail = true;
return;
}
String lengthStr = items[3];
LOG.e("记录文件长度-->" + lengthStr + "--真实文件长度-->" + file.length());
if (Long.valueOf(lengthStr) != file.length()) {
FileUtil.delete(file);
LOG.e("文件长度不一致,文件被复写过");
updateState.updateFail = true;
return;
}
updateState.apkFile = file;
LOG.e("下载完成文件-->" + file.getName() + "--位置-->" + file.getAbsolutePath());
}
});
}
其它
获取进程名的方法
/**
* 获取进程名
*
* @param context 上下文
*/
private String getProcessName(int pID, Context context) {
String processName = null;
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo info : runningAppProcesses) {
try {
if (info.pid == pID) {
processName = info.processName;
}
} catch (Exception e) {
e.printStackTrace();
}
}
return processName;
}
保存当前文件下载进度的类
/**
* 记录文件下载的状态
*/
public static class UpdateState {
public long fileLength;
public long currentLength;
public long rangeLength;
public int progress;
public File apkFile;
public boolean updateFail = false;
public void buildProgressInt() {
progress = (int) ((double) currentLength * 100 / (double) fileLength);
}
}