之前有用過retrofit來做下載的功能,雖然retrofit基于okhttp,但是這還是有點不同。
我是在做更新功能的時候用到這個,具體的操作可能不會說太多,因為網(wǎng)上能找到很多基本的操作,我就說下一些流程和BUG,不管是okhttp還是retrofit都適用。
一.下載文件
1.下載操作
下載文件其實我感覺并不像上傳那么復雜,就按照拉取文本文件一樣弄就行。
這是我普通的用okhttp的get請求
Request request = new Request.Builder()
.url(murl)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(callback);
下載文件的操作其實差不多
public Call download(String url, final DownloadListener downloadListener, final long startsPoint, Callback callback){
Request request = new Request.Builder()
.url(url)
.header("RANGE", "bytes=" + startsPoint + "-")//斷點續(xù)傳
.build();
// 重寫ResponseBody監(jiān)聽請求
Interceptor interceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new DownloadResponseBody(originalResponse, startsPoint, downloadListener))
.build();
}
};
OkHttpClient.Builder dlOkhttp = new OkHttpClient.Builder()
.addNetworkInterceptor(interceptor);
// 繞開證書
try {
setSSL(dlOkhttp);
} catch (Exception e) {
e.printStackTrace();
}
// 發(fā)起請求
Call call = dlOkhttp.build().newCall(request);
call.enqueue(callback);
return call;
}
注釋講得也比較請求,要重寫ResponseBody是因為要監(jiān)聽下載進度,網(wǎng)上有很多人的寫法是在onResponse的回調中讀寫字節(jié)到本地時監(jiān)聽進度,我建議是重寫ResponseBody來監(jiān)聽下載進度,因為好像寫在onResponse會有什么問題我忘記了,就算沒問題,自定義ResponseBody也會顯得更靈活。
2.自定義的ResponseBody
public class DownloadResponseBody extends ResponseBody{
private Response originalResponse;
private DownloadListener downloadListener;
private long oldPoint = 0;
public DownloadResponseBody(Response originalResponse, long startsPoint, DownloadListener downloadListener){
this.originalResponse = originalResponse;
this.downloadListener = downloadListener;
this.oldPoint = startsPoint;
}
@Override
public MediaType contentType() {
return originalResponse.body().contentType();
}
@Override
public long contentLength() {
return originalResponse.body().contentLength();
}
@Override
public BufferedSource source() {
return Okio.buffer(new ForwardingSource(originalResponse.body().source()) {
private long bytesReaded = 0;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
bytesReaded += bytesRead == -1 ? 0 : bytesRead;
if (downloadListener != null) {
downloadListener.loading((int) ((bytesReaded+oldPoint)/(1024)));
}
return bytesRead;
}
});
}
}
主要就是要重寫這個source方法來實現(xiàn)監(jiān)聽,代碼也不難,其實不用多說什么。
3.定義請求中的回調
最好是用一個接口來定義在下載過程中的行為,接口的好處不用多說
public interface DownloadListener {
/**
* 開始下載
*/
void start(long max);
/**
* 正在下載
*/
void loading(int progress);
/**
* 下載完成
*/
void complete(String path);
/**
* 請求失敗
*/
void fail(int code, String message);
/**
* 下載過程中失敗
*/
void loadfail(String message);
}
我這里定義了兩種失敗,主要是這邊要根據(jù)請求網(wǎng)絡的失敗和讀寫過程的失敗寫不同的邏輯,如果沒有特定的需求,這里只定義一個失敗的回調也是可以的。
二.下載的文件保存到本地
okhttp中是寫在onResponse方法中進行io操作,retrofit可以寫在onNext中
@Override
public void onResponse(Call call, Response response) throws IOException {
long length = response.body().contentLength();
if (length == 0){
// 說明文件已經(jīng)下載完,直接跳轉安裝就好
downloadListener.complete(String.valueOf(getFile().getAbsoluteFile()));
return;
}
downloadListener.start(length+startsPoint);
// 保存文件到本地
InputStream is = null;
RandomAccessFile randomAccessFile = null;
BufferedInputStream bis = null;
byte[] buff = new byte[2048];
int len = 0;
try {
is = response.body().byteStream();
bis =new BufferedInputStream(is);
File file = getFile();
// 隨機訪問文件,可以指定斷點續(xù)傳的起始位置
randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.seek (startsPoint);
while ((len = bis.read(buff)) != -1) {
randomAccessFile.write(buff, 0, len);
}
// 下載完成
downloadListener.complete(String.valueOf(file.getAbsoluteFile()));
} catch (Exception e) {
e.printStackTrace();
downloadListener.loadfail(e.getMessage());
} finally {
try {
if (is != null) {
is.close();
}
if (bis != null){
bis.close();
}
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
private File getFile() {
String root = Environment.getExternalStorageDirectory().getPath();
File file = new File(root,"updateDemo.apk");
return file;
}
private long getFileStart(){
String root = Environment.getExternalStorageDirectory().getPath();
File file = new File(root,"updateDemo.apk");
return file.length();
}
因為我們之前在請求時寫了
這里先獲取報文的長度,如果長度為0,說明在我門本地已經(jīng)下載好文件了,這里就不用下載了,直接跳轉到安裝。這個主要是對斷點續(xù)傳的一個判斷,你想想,如果我都已經(jīng)下載完文件了,那我有什么必要再去開啟io流。
這里還要注意一下,獲取文件長度用file.length()而不用fis.available()是因為網(wǎng)上有個朋友測試過用fis.available()如果數(shù)據(jù)過大的話會出問題。
downloadListener.start(length+startsPoint);是我在開始讀寫前要先給ProgressBar設最大值。
讀寫時用到RandomAccessFile,這個主要是能隨時讀寫,做的就是斷點續(xù)傳的操作。其實斷點續(xù)傳主要就三句代碼
.header("RANGE", "bytes=" + startsPoint + "-")
randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.seek (startsPoint);
后面就沒有什么了,就是普通的io操作。
三.安裝應用
下載完成后跳轉到安裝頁面,需要做一個7.0的判斷
private void installApk(String path){
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
File file = new File(path);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
String authority = "com.example.kylin.mindabs" + ".fileProvider";
Uri fileUri = FileProvider.getUriForFile(getActivity().getApplicationContext(), authority, file);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
this.startActivityForResult(intent, 0);
}catch (Exception e){
// todo 安裝失敗的操作
}
}
配置在清單中
<provider
tools:replace="android:authorities"
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.kylin.mindabs.fileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
tools:replace="android:resource"
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
定義一個xml文件
<paths>
<external-path path="Android/data/com.example.kylin.mindabs/" name="files_root" />
<external-path path="." name="external_storage_root" />
</paths>
還有就是很多人網(wǎng)上寫的跳轉是用startActivity,我建議用startActivityForResult,這樣可以拿到安裝頁面解析時的回到,方便之后做解析失敗之類的。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 0) {
......
}
}
我這里之前調試的時候打印成功和失敗的回調
(1)成功
看到resultCode為0,data為空。注意,就算你不安裝應用點取消,只要是能識別這個包出來,都是算解析成功,那么什么時候是失敗的呢,比如安裝包損壞這些才會回調失敗的情況。
(2)失敗
我自己模擬了一個失敗的情況,這種情況它會直接提示你安裝包解析失敗。
四.開發(fā)中遇到的問題
1.斷點續(xù)傳的細節(jié)
在使用
.header("RANGE", "bytes=" + startsPoint + "-")
之后,其實就是從文件的startsPoint 字節(jié)開始去下載,startsPoint 是我獲取的當前本地文件的大小,這本來是沒啥問題的,但是在apk已經(jīng)下載完成的情況下startsPoint 就是整個文件的長度,按理說這里這樣操作應該會讓response.body().contentLength()等于0,而實際上我在調試的時候沒有崩潰,但是response.body().contentLength()莫名其妙的等于229,我認真看日記才發(fā)現(xiàn)請求時報416錯誤,就是越界了。為了解決這個文件,我在拿到斷點的時候會做一個-1的操作
final long startsPoint = getFileStart() > 0 ? getFileStart()-1 : getFileStart();
2.流程問題
其實更新的知識點就那兩三個,但是對于流程來說需要嚴謹些,說得直白些,盡量有條能走通的路,所以我的代碼里大量加了try-catch
3.請求放在service
請求為什么要放在service中呢,其實是一個生命周期的問題,如果請求是在activity中發(fā)起的,關閉activity之后其實activity還不會結束,他會被請求影響生命周期,所以需要再service中請求。而如果你不是在activity中請求的話,比如你在彈框中請求,只要在彈框消失的時候取消請求就行,這種情況放不放在service里面做請求我覺得就無所謂了。