App Network Request Actual Warfare III: Download Files and Breakpoint Renewals
Melon skin is online, hey hey.Want to fight back, there is no!
Boss, salvage.
Or the original formula, no pictures diao
This article records how download functions are integrated into a network framework.
Solving Steps
1. Big aspect to understand the relationship among okhttp, retrofit, rxjava
A. First okhttp is a bottom request library for Http, and square development has been adopted by Google Android.Corresponding to okhttp are httpurlconnection and httpClient.The httpClient has been deprecated.
b. Then retrofit is an Http high-level request library, which should reasonably not care what the underlying HTTP request library is.Don't believe you can notice this sentence every time we write retrofit
new Retrofit.Builder()
.client(client);
According to the extensibility principle, this client can be either httpurlconnection or okhttp or httpClient.But retrofit 2.0 seems to have located okhttp directly after that, which means retrofit can only configure okhttp.You can also see from the retrofit source:
/**
* The HTTP client used for requests.
* <p>
* This is a convenience method for calling {@link #callFactory}.
*/
public Builder client(OkHttpClient client) {
return callFactory(checkNotNull(client, "client == null"));
}
/**
* Specify a custom call factory for creating {@link Call} instances.
* <p>
* Note: Calling {@link #client} automatically sets this value.
*/
public Builder callFactory(okhttp3.Call.Factory factory) {
this.callFactory = checkNotNull(factory, "factory == null");
return this;
}
The frameworks corresponding to retrofit are volley, async-http-client.
c. Finally, rxjava has nothing to do with the network. In this case, rxjava's role is to use the responsive and chain programming paradigms, and to handle asynchronous problems more easily.In a project, any operation involving asynchronization can be handled using rxjava, not necessarily a network request.
2.retrofit download file
Having understood the above relationship, we can reasonably know if okhttp is the final download, just that there is now a layer of retrofit.So, lower back, we first have to understand how retrofit downloads files.The two ways are as follows:
//Mode 1
//Download Files
//If the file is very large, you must use the Streaming annotation.Otherwise retrofit will read the entire file into memory by default
//Cause OOM
@Streaming
@GET
Observable<ResponseBody> downLoadFile(@Url String fileUrl);
//Mode 2
@Streaming
@GET("tools/test.apk")
Observable<ResponseBody> downLoadFile();
From the top down, note that the download file must be annotated with Steaming.Because if you don't add this annotation, retrofit will read the entire file into memory by default, causing OOM if the file is very large.The difference between the two methods is that the downloaded url can be changed more flexibly in one way and only files under baseUrl can be downloaded in the other.Understand no, a flexible, a rigid, so choose the flexible one decisively.The next expected return is the ReponseBody object of okhttp, which has a method:
public final InputStream byteStream() {
return source().inputStream();
}
Brothers, understand, get the input stream, so we can do whatever we want.The latter operation is the same as httpurlConnection, httpClient or even okhttp download ~~Try it out now, and it won't hit my face, wipe it, for fear.
String url = "http://imtt.dd.qq.com/16891/8EE1D586937A31F6E0B14DA48F8D362E.apk?fsname=com.dewmobile.kuaiya_5.4.2(CN)_216.apk&csr=1bbd";
Observable<ResponseBody> observable = apiService.downLoadFile(url)
.compose(RxSchedulers.<ResponseBody>io_main());
observable.subscribe(new DownLoadObserver()
{
@Override
public void onNext(ResponseBody responseBody)
{
//Once we get the input stream, we can do exactly what we want!
InputStream inputStream = responseBody.byteStream();
}
@Override
public void onError(Throwable e)
{
Toast.makeText(MainActivity.this, "Download failed", Toast.LENGTH_SHORT).show();
}
});
See how kind I am to find a link for everyone to download an apk.Note that there is no need to use map to convert the return type. Just add a thread switch switch switch.Here again, when to use the map, cough, pay attention to the big brothers:
First
@Streaming
@GET
Observable<ResponseBody> downLoadFile(@Url String fileUrl);
Represents that apiService calls this downLoadFile method. We expect a ResponseBody object to be returned, right?Yes, we wanted this object to generate the input stream, so we just fucked.The map conversion type was previously required because:
//Get http for token expiration
@GET("tools/mockapi/440/token_expired")
Observable<BaseResponse<ResEntity1.DataBean>> getExpiredHttp();
Here we expect the BaseResponse object, and what we actually want is the ResEntity1.DataBean object, so we need to convert it.Wipe, tired, understand, don't know, look down.
Actually, there are only java files io read and write, but it is important to note that io operation needs to open sub-threads, so it can be said that the download function has been completed.But there is a big brother who is reluctant to say: So you can neither show the download progress here, nor control the start of download, pause download, cancel download.I jumped up and hit your knee. Oh, old fellow, can you stop bb?
3. Display download progress, control start, pause, cancel Download
In fact, the download progress is a good implementation, just use the length of the downloaded file to compare with the length of the total file.
int progress = (int) (downloadedLength * 100 / contentLength);
But let's just assume that there is an old fellow outside using a 4g network, accidentally clicking on the download.When the calf is finished, you can't pause or cancel the download. You abuse the program developed by **.
We are afraid of being chased across a province for thou sa nds of miles. We just have to think about how to suspend the download and think about a little bit of grievance.First, analyze the above requirements: Suppose you download the file 10M, can I download 1M first, then click Pause, then go out of the high s, then continue writing streams from 1M.There is a class in java that can control the file pointer length offset, that is, a class that can jump to the location of this file 1M.Yes, it is the RandomAccessFile class.This class is not detailed, but it has the ability to fulfill our needs.Second, after downloading half of the 5M files, if you find that you erased the wrong files, you should cancel the download. Note that canceling the download is accompanied by a hidden operation that is to delete the 5M files that have already been downloaded.Keep in mind, otherwise it will be cross-province (you can actually use FileOutputStream streams here as well).
However, it is important to note that the seemingly rxjava-retrofit combination cannot return synchronously as follows:
Response<BaseResponse<ResEntity1.DataBean>> tokenRes = call.execute();
(I really don't know how to do this with rxjava, the woody guys know it.)
Asynchronous callbacks do not provide both progress and pause or cancel downloads.So you have to think of another way.Provides a code downloaded by okhttpi (quoted from the first line of code by Guo Lin):
try
{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if (response != null)
{
Log.e("DownLoadTask", "(DownLoadTask.java:78)" + response.body().contentLength());
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadedLength);
byte[] bytes = new byte[1024];
int total = 0;
int len;
while ((len = is.read(bytes)) != -1)
{
if (isCanceled)
{
return TYPE_CANCELED;
} else if (isPaused)
{
return TYPE_PAUSE;
} else
{
total += len;
savedFile.write(bytes, 0, len);
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
publishProgress(progress);
}
}
}
response.body().close();
return TYPE_SUCCESS;
} catch (IOException e)
{
e.printStackTrace();
} finally
{
try
{
if (is != null)
{
is.close();
}
if (savedFile != null)
{
savedFile.close();
}
if (isCanceled && file != null)
{
file.delete();
}
} catch (IOException e)
{
e.printStackTrace();
}
}
In fact, there are two issues involved here:
- Download Progress
- Breakpoint Continuation
4. Download Progress
The key is to customize the ResponseBody, notice the source method inside, where we can face the source in advance and feel like the idea of the okhttp interceptor.
First, define a download interface:
/**
* <pre>
* Author: Xiao Kun
* Time: 2018/04/20
* Description:
* Version: 1.0
* </pre>
*/
public interface DownLoadListener
{
void onProgress(int progress, boolean downSuc, boolean downFailed);
}
The three parameters are the download progress, whether the download is completed, whether the download failed.
Next, look at the custom ReponseBody code as follows:
@Override
public BufferedSource source()
{
if (bufferedSource == null)
{
bufferedSource = Okio.buffer(source(mResponseBody.source()));
}
return bufferedSource;
}
private Source source(Source source)
{
return new ForwardingSource(source)
{
@Override
public long read(Buffer sink, long byteCount) throws IOException
{
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
if (bytesRead == -1)
{
//Download complete
sHandler.post(new Runnable()
{
@Override
public void run()
{
mListener.onProgress(100, true, false);
}
});
} else
{
//Downloading, update progress
sHandler.post(new Runnable()
{
@Override
public void run()
{
int progress = (int) (totalBytesRead * 100 / contentLength);
mListener.onProgress(progress, false, false);
}
});
}
saveToFile(sink);
return bytesRead;
}
};
}
//write file
private void saveToFile(Buffer buffer)
{
InputStream inputStream = buffer.inputStream();
RandomAccessFile saveFile = null;
try
{
saveFile = new RandomAccessFile(file, "rw");
saveFile.seek(getDownloadedLength());
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1)
{
saveFile.write(bytes, 0, len);
}
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
} finally
{
try
{
if (inputStream != null)
{
inputStream.close();
}
if (saveFile != null)
{
saveFile.close();
}
} catch (IOException e)
{
e.printStackTrace();
}
}
}
It's that ForwardingSource class, and it really feels like an interceptor in okhttp, that's to intercept that source and then do something undescribable, hey hey.RandomAccessFile is used here for subsequent breakpoint renewal functionality.It is also a constructor that can use two parameters of FileOutputStream, as follows
//if <code>true</code>, then bytes will be written to the end of the file rather than the //beginning
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}
The second argument, true, is also writable.Overall, download progress is relatively simple.
5. Breakpoint Renewal
In fact, the key is that Made words are not clear.Look at the picture:
There is one attribute in the Http request header as follows:
request = request.newBuilder()
.header("RANGE", "bytes=" + downloadedLength + "-")
.build();
The purpose of this property is to change the request domain, for example, if I want to start downloading at the 500th byte.So how do you dynamically modify it, yes, or use the Interceptor interceptor:
//Download File Interceptor
static Interceptor downloadInterceptor = new Interceptor()
{
@Override
public Response intercept(Chain chain) throws IOException
{
Request request = chain.request();
//Record the length of the written file
long downloadedLength = DownloadManager.dSp.getLong(downloadEntity.getFileName(), 0);
Response proceed = chain.proceed(request);
//Total length of saved source file (critical)
DownloadManager.dSp.edit().putLong(downloadEntity.getFileName() + "content_length", proceed.body().contentLength()).commit();
request = request.newBuilder()
.header("RANGE", "bytes=" + downloadedLength + "-")
.build();
Response response = chain.proceed(request);
response = response.newBuilder()
.body(new ProgressResponseBody(response.body(), downloadEntity))
.build();
return response;
}
};
All progress = (length of written file + length of downloading) / total length of source file, I will not paste the code in the project.Another important thing to note is that I suspend and cancel downloads, using Disposable in rxjava, and I wrote a download management class specifically, as follows:
/**
* Created by Xiao Kun on 2018/4/22.
*
* @author Xiao Kun
* @date 2018/4/22
*/
public class DownloadManager
{
public static SharedPreferences dSp;
/**
* Initialize DownloadManager
*
* @param context
*/
public static void initDownManager(Context context)
{
dSp = context.getSharedPreferences("download_file", Context.MODE_PRIVATE);
}
/**
* Pause Download
*
* @param disposable Controlling the switch on rxjava
* @param fileName Downloaded file name, must contain suffix
*/
public static void pauseDownload(Disposable disposable, String fileName)
{
if (disposable == null || TextUtils.isEmpty(fileName))
{
return;
}
if (!disposable.isDisposed())
{
disposable.dispose();
}
if (dSp == null)
{
throw new NullPointerException("Must be initialized first DownloadManager");
}
File file = initFile(fileName);
if (file.exists() && dSp != null)
{
dSp.edit().putLong(file.getName(), file.length()).commit();
}
}
/**
* Cancel Download
*
* @param disposable Controlling the switch on rxjava
* @param fileName Downloaded file name, must contain suffix
*/
public static void cancelDownload(Disposable disposable, String fileName)
{
if (disposable == null || TextUtils.isEmpty(fileName))
{
return;
}
if (!disposable.isDisposed())
{
disposable.dispose();
}
File file = initFile(fileName);
if (file.exists() && dSp != null)
{
dSp.edit().putLong(file.getName(), 0).commit();
}
//Cancel download, last step remember to delete downloaded files
if (file.exists())
{
file.delete();
}
}
public static File initFile(String fileName)
{
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + File.separator + fileName);
return file;
}
}
Note that I deleted the written file when I canceled the download. No problem, old fellow.Whole, clear and white, Deyun Society has a brand!
Finally, it's fairly easy for us to download in the project, as the code says:
//Test downloaded files
private void downloadFile()
{
fileName = "httpTest.apk";
downloadEntity = new DownloadEntity(loadListener, fileName);
ApiService apiService = RetrofitHelper.createService(ApiService.class,
RetrofitHelper.getDownloadRetrofit(downloadEntity));
Observable<ResponseBody> observable = apiService.downLoadFile(url)
.subscribeOn(Schedulers.io());
observable.subscribe(new DownLoadObserver()
{
@Override
public void onSubscribe(Disposable d)
{
disposable = d;
}
});
}
Then pause:
//Pause Download
DownloadManager.pauseDownload(disposable, fileName);
Then cancel the download:
//Cancel Download
DownloadManager.cancelDownload(disposable, fileName);
github link: Demo
Above.