1. 断点续传原理
在本地下载过程中要使用数据库实时存储到底存储到文件的哪个位置了,这样点击开始继续传递时,才能通过HTTP的GET请求中的setRequestProperty()方法可以告诉服务器,数据从哪里开始,到哪里结束。同时在本地的文件写入时,RandomAccessFile的seek()方法也支持在文件中的任意位置进行写入操作。同时通过广播将子线程的进度告诉Activity的ProcessBar。
2. Activity的按钮响应
当点击开始按钮时,将url写在了FileInfo类的对象info中并通过Intent从Activity传递到了Service中。这里使用setAction()来区分是开始按钮还是暂停按钮。
首先是相关的权限 在清单文件里面 添加
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
- compile 'io.reactivex:rxjava:1.0.14'
- compile 'io.reactivex:rxandroid:1.0.1'
- compile 'com.squareup.retrofit2:retrofit:2.1.0'
- compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
- compile 'io.reactivex:rxandroid:1.1.0'
- compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
- compile 'com.squareup.retrofit2:converter-gson:2.1.0'
- compile 'com.squareup.okio:okio:1.5.0'
- compile 'com.squareup.okhttp3:okhttp:3.2.0'
- compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
- compile 'com.jakewharton:butterknife:7.0.1'
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/activity_main"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingBottom="@dimen/activity_vertical_margin"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
- android:orientation="vertical"
- tools:context="com.eightgroup.jinchengxiancheng.MainActivity">
- <ProgressBar
- android:id="@+id/progressBar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="10dp"
- android:layout_marginRight="10dp"
- android:max="100"
- style="?android:attr/progressBarStyleHorizontal" />
- <Button
- android:id="@+id/downloadButton"
- android:onClick="downloadButtons"
- android:layout_marginTop="10dp"
- android:layout_marginLeft="10dp"
- android:layout_marginRight="10dp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="下载"/>
- <Button
- android:onClick="cancel_buttons"
- android:id="@+id/cancel_button"
- android:layout_marginTop="10dp"
- android:layout_marginLeft="10dp"
- android:layout_marginRight="10dp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="暂停"/>
- <Button
- android:onClick="continue_buttons"
- android:id="@+id/continue_button"
- android:layout_marginTop="10dp"
- android:layout_marginLeft="10dp"
- android:layout_marginRight="10dp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="继续"/>
- </LinearLayout>
//然后创建两个相关的类 来实现
第一个
ProgressDownloader//在下载、暂停后的继续下载中可复用同一个client对象 //每次下载需要新建新的Call对象
- public class ProgressDownloader {
- public static final String TAG = "ProgressDownloader";
- private ProgressResponseBody.ProgressListener progressListener;
- private String url;
- private OkHttpClient client;
- private File destination;
- private Call call;
- public ProgressDownloader(String url, File destination, ProgressResponseBody.ProgressListener progressListener) {
- this.url = url;
- this.destination = destination;
- this.progressListener = progressListener;
- //在下载、暂停后的继续下载中可复用同一个client对象
- client = getProgressClient();
- }
- //每次下载需要新建新的Call对象
- private Call newCall(long startPoints) {
- Request request = new Request.Builder()
- .url(url)
- .header("RANGE", "bytes=" + startPoints + "-")//断点续传要用到的,指示下载的区间
- .build();
- return client.newCall(request);
- }
- public OkHttpClient getProgressClient() {
- // 拦截器,用上ProgressResponseBody
- Interceptor interceptor = new Interceptor() {
- @Override
- public Response intercept(Chain chain) throws IOException {
- Response originalResponse = chain.proceed(chain.request());
- return originalResponse.newBuilder()
- .body(new ProgressResponseBody(originalResponse.body(), progressListener))
- .build();
- }
- };
- return new OkHttpClient.Builder()
- .addNetworkInterceptor(interceptor)
- .build();
- }
- // startsPoint指定开始下载的点
- public void download(final long startsPoint) {
- call = newCall(startsPoint);
- call.enqueue(new Callback() {
- @Override
- public void onFailure(Call call, IOException e) {
- }
- @Override
- public void onResponse(Call call, Response response) throws IOException {
- save(response, startsPoint);
- }
- });
- }
- public void pause() {
- if(call!=null){
- call.cancel();
- }
- }
- private void save(Response response, long startsPoint) {
- ResponseBody body = response.body();
- InputStream in = body.byteStream();
- FileChannel channelOut = null;
- // 随机访问文件,可以指定断点续传的起始位置
- RandomAccessFile randomAccessFile = null;
- try {
- randomAccessFile = new RandomAccessFile(destination, "rwd");
- //Chanel NIO中的用法,由于RandomAccessFile没有使用缓存策略,直接使用会使得下载速度变慢,亲测缓存下载3.3秒的文件,用普通的RandomAccessFile需要20多秒。
- channelOut = randomAccessFile.getChannel();
- // 内存映射,直接使用RandomAccessFile,是用其seek方法指定下载的起始位置,使用缓存下载,在这里指定下载位置。
- MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE, startsPoint, body.contentLength());
- byte[] buffer = new byte[1024];
- int len;
- while ((len = in.read(buffer)) != -1) {
- mappedBuffer.put(buffer, 0, len);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- try {
- in.close();
- if (channelOut != null) {
- channelOut.close();
- }
- if (randomAccessFile != null) {
- randomAccessFile.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
接下来是第二个类 需要继承 ResponBoay
- public class ProgressResponseBody extends ResponseBody {
- //设置对外访问的进度监听
- public interface ProgressListener {
- void onPreExecute(long contentLength);
- void update(long totalBytes, boolean done);
- }
- private final ResponseBody responseBody;
- private final ProgressListener progressListener;
- private BufferedSource bufferedSource;
- public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
- this.responseBody = responseBody;
- this.progressListener = progressListener;
- if (progressListener != null) {
- progressListener.onPreExecute(contentLength());
- }
- }
- @Override
- public MediaType contentType() {
- return responseBody.contentType();
- }
- @Override
- public long contentLength() {
- return responseBody.contentLength();
- }
- @Override
- public BufferedSource source() {
- if (bufferedSource == null) {
- bufferedSource = Okio.buffer(source(responseBody.source()));
- }
- return bufferedSource;
- }
- private Source source(Source source) {
- return new ForwardingSource(source) {
- long totalBytes = 0L;
- @Override
- public long read(Buffer sink, long byteCount) throws IOException {
- long bytesRead = super.read(sink, byteCount);
- // read() returns the number of bytes read, or -1 if this source is exhausted.
- totalBytes += bytesRead != -1 ? bytesRead : 0;
- if (null != progressListener) {
- progressListener.update(totalBytes, bytesRead == -1);
- }
- return bytesRead;
- }
- };
- }
- }
- public class MainActivity extends AppCompatActivity implements ProgressResponseBody.ProgressListener {
- public static final String TAG = "MainActivity";
- public static final String PACKAGE_URL = "http://gdown.baidu.com/data/wisegame/df65a597122796a4/weixin_821.apk";
- private ProgressBar progressBar;
- private long breakPoints;
- private ProgressDownloader downloader;
- private File file;
- private long totalBytes;
- private long contentLength;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- progressBar = (ProgressBar) findViewById(R.id.progressBar);
- }
- public void downloadButtons(View view){
- // 新下载前清空断点信息
- breakPoints = 0L;
- // 下载的位置
- file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "sample.apk");
- downloader = new ProgressDownloader(PACKAGE_URL, file, this);
- downloader.download(0L);
- }
- public void cancel_buttons(View view){
- downloader.pause();
- Toast.makeText(this, "下载暂停", Toast.LENGTH_SHORT).show();
- // 存储此时的totalBytes,即断点位置。
- breakPoints = totalBytes;
- }
- public void continue_buttons(View view){
- downloader.download(breakPoints);
- }
- @Override
- public void onPreExecute(long contentLength) {
- // 文件总长只需记录一次,要注意断点续传后的contentLength只是剩余部分的长度
- if (this.contentLength == 0L) {
- this.contentLength = contentLength;
- progressBar.setMax((int) (contentLength / 1024));
- }
- }
- @Override
- public void update(long totalBytes, boolean done) {
- // 注意加上断点的长度
- this.totalBytes = totalBytes + breakPoints;
- progressBar.setProgress((int) (totalBytes + breakPoints) / 1024);
- if (done) {
- // 切换到主线程
- Observable
- .empty()
- .observeOn(AndroidSchedulers.mainThread())
- .doOnCompleted(new Action0() {
- @Override
- public void call() {
- Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
- }
- })
- .subscribe();
- }
- }
- }