前言:最近有個斷點下載的需求,搗鼓了下,然後分享下
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝(一)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之檔案下載(二)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之檔案上傳(三)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之常見問題(四)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之斷點下載(五)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之資料預處理(六)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之多Url(七)
關於檔案下載,在第2篇中已經詳細說過,這裡就不在詳細說了,先對之前的下載做個封裝
首先是ApiServer
@Streaming
@GET
/**
* 大檔案官方建議用 @Streaming 來進行註解,不然會出現IO異常,小檔案可以忽略不注入
*/
Observable<ResponseBody> downloadFile(@Url String fileUrl);
複製程式碼
定義下載回撥
public interface DownFileCallback {
void onSuccess(String path);
void onFail(String msg);
void onProgress(long totalSize, long downSize);
}
複製程式碼
DownLoadManager
public class DownLoadManager {
private static DownLoadManager loadManager;
private HashMap<String, FileObserver> hashMap;
private OkHttpClient client;
private Retrofit retrofit;
private ApiServer apiServer;
private DownFileCallback fileCallback;
public DownLoadManager() {
hashMap = new HashMap<>();
client = new OkHttpClient.Builder()
.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response.newBuilder().body(new ProgressResponseBody(response.body(),
new ProgressResponseBody.ProgressListener() {
@Override
public void onProgress(long totalSize, long downSize) {
if (fileCallback != null) {
fileCallback.onProgress(totalSize, downSize);
}
}
})).build();
}
}).build();
retrofit = new Retrofit.Builder().client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl("https://wawa-api.vchangyi.com/").build();
apiServer = retrofit.create(ApiServer.class);
}
public static DownLoadManager getInstance() {
synchronized (Object.class) {
if (loadManager == null) {
loadManager = new DownLoadManager();
}
}
return loadManager;
}
/**
* 下載單個檔案
*
* @param url
* @param fileCallback
*/
public void downFile(final String url, final DownFileCallback fileCallback) {
//如果正在下載,則暫停
if (isDownLoad(url)) {
pause(url);
return;
}
this.fileCallback = fileCallback;
//儲存的檔案路徑
final String path = getTemporaryName(url);
FileObserver observer = apiServer.downloadFile(url).map(new Function<ResponseBody, String>() {
@Override
public String apply(ResponseBody body) throws Exception {
File file = FileUtil.saveFile(path, body);
return file.getPath();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new FileObserver<String>() {
@Override
public void onSuccess(String path) {
fileCallback.onSuccess(path);
hashMap.remove(url);
}
@Override
public void onError(String msg) {
fileCallback.onFail(msg);
hashMap.remove(url);
}
});
//儲存
hashMap.put(url, observer);
}
/**
* 暫停/取消任務
*
* @param url 完整url
*/
public void pause(String url) {
if (hashMap.containsKey(url)) {
FileObserver observer = hashMap.get(url);
if (observer != null) {
observer.dispose();
hashMap.remove(url);
}
}
}
/**
* 獲取臨時檔名
*
* @param url
* @return
*/
public static String getTemporaryName(String url) {
String type = "";
if (url.contains(".")) {
type = url.substring(url.lastIndexOf("."));
}
String dirName = Environment.getExternalStorageDirectory() + "/mvp/";
File f = new File(dirName);
//不存在建立
if (!f.exists()) {
f.mkdirs();
}
return dirName + System.currentTimeMillis() + type;
}
/**
* 是否在下載
*
* @param url
* @return
*/
public boolean isDownLoad(String url) {
return hashMap.containsKey(url);
}
public abstract class FileObserver<T> extends DisposableObserver<T> {
@Override
public void onNext(T t) {
onSuccess(t);
}
@Override
public void onError(Throwable e) {
onError(e.getMessage());
}
@Override
public void onComplete() {
}
public abstract void onSuccess(T o);
public abstract void onError(String msg);
}
}
複製程式碼
斷點下載的話,有2點要注意
- 1.要新增
RANGE
請求頭
if (downSize != 0 && totalSize != 0) {
request = request.newBuilder()
.addHeader("RANGE", "bytes=" + downSize + "-" + totalSize).build();
}
複製程式碼
downSize
和totalSize
,前者是下載的開始長度,後者是整個檔案的長度,這些都需要我們暫停時,自己做儲存。
- 2.寫檔案問題,斷點下載時,就不能從頭開始寫入檔案,需要從上次結束的地方開始,這裡就用到了
RandomAccessFile
/**
* @param filePath
* @param start 起始位置
* @param body
*/
public static File saveFile(String filePath, long start, ResponseBody body) {
InputStream inputStream = null;
RandomAccessFile raf = null;
File file = null;
try {
file = new File(filePath);
raf = new RandomAccessFile(filePath, "rw");
inputStream = body.byteStream();
byte[] fileReader = new byte[4096];
//移動到該位置
raf.seek(start);
while (true) {
int read = inputStream.read(fileReader);
if (read == -1) {
break;
}
raf.write(fileReader, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return file;
}
複製程式碼
建個實體類,來儲存檔案相關屬性,get和set就不貼了
public class DownModel {
private String url;
private String path;
private String title;
private String cover;
private long totalSize;
private long currentTotalSize;
private long downSize;
private boolean isExists;
private boolean isFinish;
private boolean isPause;
}
複製程式碼
/**
* 下載單個檔案
*
* @param downModel
* @param
*/
public void downFile(final DownModel downModel, final DownFileCallback fileCallback) {
if (downModel == null) {
return;
}
//如果正在下載,則暫停
final String url = downModel.getUrl();
if (isDownLoad(url)) {
pause(url);
Log.e("cheng", "pause url=" + url);
return;
}
//當前連結
currentUrl = url;
//是否是斷點下載
if (downModel.getDownSize() != 0 && downModel.getTotalSize() != 0) {
totalSize = downModel.getTotalSize();
downSize = downModel.getDownSize();
currentPath = downModel.getPath();
} else {
totalSize = 0;
downSize = 0;
currentPath = getTemporaryName(url);
downModel.setPath(currentPath);
}
this.fileCallback = fileCallback;
Log.e("cheng", "currentUrl=" + currentUrl);
Log.e("cheng", "downSize=" + downSize + ",totalSize=" + totalSize + ",currentPath=" + currentPath);
FileObserver observer = apiServer.downloadFile(url).map(new Function<ResponseBody, String>() {
@Override
public String apply(ResponseBody body) throws Exception {
if (downModel.getDownSize() != 0 && downModel.getTotalSize() != 0) {
return FileUtil.saveFile(currentPath, downModel.getDownSize(), body).getPath();
}
File file = FileUtil.saveFile(currentPath, body);
return file.getPath();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new FileObserver<String>() {
@Override
public void onSuccess(String path) {
downModel.setFinish(true);
downModel.setPath(path);
downModel.setExists(true);
if (fileCallback != null) {
fileCallback.onSuccess(path);
}
hashMap.remove(url);
currentUrl = null;
}
@Override
public void onError(String msg) {
if (fileCallback != null) {
fileCallback.onFail(msg);
}
hashMap.remove(url);
currentUrl = null;
}
});
//儲存
hashMap.put(url, observer);
}
複製程式碼
需要注意的是,如果檔案總大小為50M
,已下載的大小為10M
,再次下載時onProgress
返回的totalSize
是檔案總長度 減去 已下載大小 10M
, 即40M
,downSize
為本次下載的已下載量
private void down2() {
String url = "http://download.sdk.mob.com/apkbus.apk";
if (downModel == null) {
downModel = new DownModel();
downModel.setUrl(url);
}
DownLoadManager.getInstance().downFile(downModel, new DownFileCallback() {
@Override
public void onSuccess(String path) {
showtoast("下載成功,path=" + path);
}
@Override
public void onFail(String msg) {
}
@Override
public void onProgress(long totalSize, long downSize) {
Log.e("cheng", "totalSize =" + totalSize + ",downSize=" + downSize);
if (downModel.getTotalSize() == 0) {
downModel.setTotalSize(totalSize);
}
downModel.setCurrentTotalSize(totalSize);
downModel.setDownSize(downSize + downModel.getTotalSize() - downModel.getCurrentTotalSize());
runOnUiThread(new Runnable() {
@Override
public void run() {
int progress = (int) (downModel.getDownSize() * 100 / downModel.getTotalSize());
tvDown.setText(progress + "%");
sbDown.setProgress(progress);
}
});
}
});
}
複製程式碼
最後,獻上原始碼 Github