OKHttp實現大檔案的斷點續傳

本文的亮點:

(1)網路請求用OKHttp進行下載大檔案

(2)實現了大檔案的斷點續傳

(3)取消下載時,刪除已經下載的檔案。

實現效果圖:

             

直接給出工程:

(1)定義一個介面:DownloadListener.java

package com.example.servicebestpractice;

/**
 * Created by Administrator on 2017/2/23.
 */
public interface DownloadListener {


    /**
     * 通知當前的下載進度
     * @param progress
     */
    void onProgress(int progress);

    /**
     * 通知下載成功
     */
    void onSuccess();

    /**
     * 通知下載失敗
     */
    void onFailed();

    /**
     * 通知下載暫停
     */
    void onPaused();

    /**
     * 通知下載取消事件
     */
    void onCanceled();

}

(2)開啟一個非同步下載任務:DownloadTask.java

package com.example.servicebestpractice;
import android.os.AsyncTask;
import android.os.Environment;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Created by Administrator on 2017/2/23.
*/
/**
* String 在執行AsyncTask時需要傳入的引數,可用於在後臺任務中使用。
* Integer 後臺任務執行時,如果需要在介面上顯示當前的進度,則使用這裡指定的泛型作為進度單位。
* Integer 當任務執行完畢後,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值型別。
*/
public class DownloadTask extends AsyncTask<String,Integer,Integer> {
public static final int TYPE_SUCCESS=0;
public static final int TYPE_FAILED=1;
public static final int TYPE_PAUSED=2;
public static final int TYPE_CANCELED=3;
private DownloadListener listener;
private boolean isCanceled=false;
private boolean isPaused=false;
private int lastProgress;
public DownloadTask(DownloadListener listener) {
this.listener = listener;
}
/**
* 這個方法中的所有程式碼都會在子執行緒中執行,我們應該在這裡處理所有的耗時任務。
* @param params
* @return
*/
@Override
protected Integer doInBackground(String... params) {
InputStream is=null;
RandomAccessFile savedFile=null;
File file=null;
long downloadLength=0;   //記錄已經下載的檔案長度
//檔案下載地址
String downloadUrl=params[0];
//下載檔案的名稱
String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//下載檔案存放的目錄
String directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
//建立一個檔案
file=new File(directory fileName);
if(file.exists()){
//如果檔案存在的話,得到檔案的大小
downloadLength=file.length();
}
//得到下載內容的大小
long contentLength=getContentLength(downloadUrl);
if(contentLength==0){
return TYPE_FAILED;
}else if(contentLength==downloadLength){
//已下載位元組和檔案總位元組相等,說明已經下載完成了
return TYPE_SUCCESS;
}
OkHttpClient client=new OkHttpClient();
/**
* HTTP請求是有一個Header的,裡面有個Range屬性是定義下載區域的,它接收的值是一個區間範圍,
* 比如:Range:bytes=0-10000。這樣我們就可以按照一定的規則,將一個大檔案拆分為若干很小的部分,
* 然後分批次的下載,每個小塊下載完成之後,再合併到檔案中;這樣即使下載中斷了,重新下載時,
* 也可以通過檔案的位元組長度來判斷下載的起始點,然後重啟斷點續傳的過程,直到最後完成下載過程。
*/
Request request=new Request.Builder()
.addHeader("RANGE","bytes=" downloadLength "-")  //斷點續傳要用到的,指示下載的區間
.url(downloadUrl)
.build();
try {
Response response=client.newCall(request).execute();
if(response!=null){
is=response.body().byteStream();
savedFile=new RandomAccessFile(file,"rw");
savedFile.seek(downloadLength);//跳過已經下載的位元組
byte[] b=new byte[1024];
int total=0;
int len;
while((len=is.read(b))!=-1){
if(isCanceled){
return TYPE_CANCELED;
}else if(isPaused){
return TYPE_PAUSED;
}else {
total =len;
savedFile.write(b,0,len);
//計算已經下載的百分比
int progress=(int)((total downloadLength)*100/contentLength);
//注意:在doInBackground()中是不可以進行UI操作的,如果需要更新UI,比如說反饋當前任務的執行進度,
//可以呼叫publishProgress()方法完成。
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 (Exception e){
e.printStackTrace();
}
}
return TYPE_FAILED;
}
/**
* 當在後臺任務中呼叫了publishProgress(Progress...)方法之後,onProgressUpdate()方法
* 就會很快被呼叫,該方法中攜帶的引數就是在後臺任務中傳遞過來的。在這個方法中可以對UI進行操作,利用引數中的數值就可以
* 對介面進行相應的更新。
* @param values
*/
protected void onProgressUpdate(Integer...values){
int progress=values[0];
if(progress>lastProgress){
listener.onProgress(progress);
lastProgress=progress;
}
}
/**
* 當後臺任務執行完畢並通過Return語句進行返回時,這個方法就很快被呼叫。返回的資料會作為引數
* 傳遞到此方法中,可以利用返回的資料來進行一些UI操作。
* @param status
*/
@Override
protected void onPostExecute(Integer status) {
switch (status){
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
break;
default:
break;
}
}
public void  pauseDownload(){
isPaused=true;
}
public void cancelDownload(){
isCanceled=true;
}
/**
* 得到下載內容的大小
* @param downloadUrl
* @return
*/
private long getContentLength(String downloadUrl){
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder().url(downloadUrl).build();
try {
Response response=client.newCall(request).execute();
if(response!=null&&response.isSuccessful()){
long contentLength=response.body().contentLength();
response.body().close();
return contentLength;
}
} catch (IOException e) {
e.printStackTrace();
}
return  0;
}
}

(3)開啟一個服務:DownloadService.java

package com.example.servicebestpractice;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.widget.Toast;
import java.io.File;
/**
* 為了保證DownloadTask可以一直在後臺執行,我們還需要建立一個下載的服務。
*/
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
private DownloadListener listener=new DownloadListener() {
/**
* 構建了一個用於顯示下載進度的通知
* @param progress
*/
@Override
public void onProgress(int progress) {
//NotificationManager的notify()可以讓通知顯示出來。
//notify(),接收兩個引數,第一個引數是id:每個通知所指定的id都是不同的。第二個引數是Notification物件。
getNotificationManager().notify(1,getNotification("Downloading...",progress));
}
/**
* 建立了一個新的通知用於告訴使用者下載成功啦
*/
@Override
public void onSuccess() {
downloadTask=null;
//下載成功時將前臺服務通知關閉,並建立一個下載成功的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Success",-1));
Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show();
}
/**
*使用者下載失敗
*/
@Override
public void onFailed() {
downloadTask=null;
//下載失敗時,將前臺服務通知關閉,並建立一個下載失敗的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show();
}
/**
* 使用者暫停
*/
@Override
public void onPaused() {
downloadTask=null;
Toast.makeText(DownloadService.this,"Download Paused",Toast.LENGTH_SHORT).show();
}
/**
* 使用者取消
*/
@Override
public void onCanceled() {
downloadTask=null;
//取消下載,將前臺服務通知關閉,並建立一個下載失敗的通知
stopForeground(true);
Toast.makeText(DownloadService.this,"Download Canceled",Toast.LENGTH_SHORT).show();
}
};
private DownloadBinder mBinder=new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
/**
* 為了要讓DownloadService可以和活動進行通訊,我們建立了一個DownloadBinder物件
*/
class DownloadBinder extends Binder{
/**
* 開始下載
* @param url
*/
public void  startDownload(String url){
if(downloadTask==null){
downloadUrl=url;
downloadTask=new DownloadTask(listener);
//啟動下載任務
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
}
}
/**
* 暫停下載
*/
public void pauseDownload(){
if(downloadTask!=null){
downloadTask.pauseDownload();
}
}
/**
* 取消下載
*/
public void cancelDownload(){
if(downloadTask!=null){
downloadTask.cancelDownload();
}else {
if(downloadUrl!=null){
//取消下載時需要將檔案刪除,並將通知關閉
String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file=new File(directory fileName);
if(file.exists()){
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
}
}
}
/**
* 獲取NotificationManager的例項,對通知進行管理
* @return
*/
private NotificationManager getNotificationManager(){
return (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
*
* @param title
* @param progress
* @return
*/
private Notification getNotification(String title,int progress){
Intent intent=new Intent(this,MainActivity.class);
//PendingIntent是等待的Intent,這是跳轉到一個Activity元件。當使用者點選通知時,會跳轉到MainActivity
PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
/**
* 幾乎Android系統的每一個版本都會對通知這部分功能進行獲多或少的修改,API不穩定行問題在通知上面凸顯的尤其嚴重。
* 解決方案是:用support庫中提供的相容API。support-v4庫中提供了一個NotificationCompat類,使用它可以保證我們的
* 程式在所有的Android系統版本中都能正常工作。
*/
NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
//設定通知的小圖示
builder.setSmallIcon(R.mipmap.ic_launcher);
//設定通知的大圖示,當下拉系統狀態列時,就可以看到設定的大圖示
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));
//當通知被點選的時候,跳轉到MainActivity中
builder.setContentIntent(pi);
//設定通知的標題
builder.setContentTitle(title);
if(progress>0){
//當progress大於或等於0時,才需要顯示下載進度
builder.setContentText(progress "%");
builder.setProgress(100,progress,false);
}
return builder.build();
}
}

(4)MainActivity.java

package com.example.servicebestpractice;
import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder=(DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button startDownload=(Button) findViewById(R.id.start_download);
startDownload.setOnClickListener(this);
Button pauseDownload=(Button) findViewById(R.id.pause_download);
pauseDownload.setOnClickListener(this);
Button cancelDownload=(Button)findViewById(R.id.cancel_download);
cancelDownload.setOnClickListener(this);
Intent intent=new Intent(this,DownloadService.class);
//這一點至關重要,因為啟動服務可以保證DownloadService一直在後臺執行,繫結服務則可以讓MaiinActivity和DownloadService
//進行通訊,因此兩個方法的呼叫都必不可少。
startService(intent);  //啟動服務
bindService(intent,connection,BIND_AUTO_CREATE);//繫結服務
/**
*執行時許可權處理:我們需要再用到許可權的地方,每次都要檢查是否APP已經擁有許可權
* 下載功能,需要些SD卡的許可權,我們在寫入之前檢查是否有WRITE_EXTERNAL_STORAGE許可權,沒有則申請許可權
*/
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}
}
public void onClick(View v){
if(downloadBinder==null){
return;
}
switch (v.getId()){
case R.id.start_download:
//String url="http://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
String url="http://10.0.2.2:8080/ChromeSetup.exe";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();
break;
default:
break;
}
}
/**
* 使用者選擇允許或拒絕後,會回撥onRequestPermissionsResult
* @param requestCode  請求碼
* @param permissions
* @param grantResults  授權結果
*/
@Override
public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0&&grantResults[0]!= PackageManager.PERMISSION_GRANTED){
Toast.makeText(this,"拒絕許可權將無法使用程式",Toast.LENGTH_SHORT).show();
finish();
}
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//解除繫結服務
unbindService(connection);
}
}

(5)簡單的佈局檔案:activity_main.xml

<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.servicebestpractice.MainActivity">
<Button
android:id="@ id/start_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Download"/>
<Button
android:id="@ id/pause_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause Download"/>
<Button
android:id="@ id/cancel_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Cancel Download"/>
</LinearLayout>

(6)AndroidMainfest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicebestpractice">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DownloadService"
android:enabled="true"
android:exported="true"></service>
</application>
</manifest>

(7)build.gradle

 compile 'com.squareup.okhttp3:okhttp:3.4.1'

(8)最後給出專案下載地址:

https://github.com/Microstrong0305/OKHttp_DownloadFile