Android之斷點續傳下載

今天學習瞭Android開發中比較難的一個環節,就是斷點續傳下載,很多人看到這個標題就感覺頭大,的確,如果沒有良好的邏輯思維,這塊的確很難搞明白。下面我就將自己學到的知識和一些見解寫下供那些在這個環節還煩惱的人參考。這裡我以下載mp3文件為例。

斷點續傳下載,顧名思義,那就是我們在一次下載未結束時,退出下載,第二次下載時會接著第一次下載的進度繼續下載。那麼怎麼記錄第一次下載的數據呢,這裡肯定就要用到數據庫瞭。下面就是我創建數據庫的一個SQLiteOpenHelper類。用來首次運行時創建數據庫。

DBHelper類:

復制代碼

 1 package cn.yj3g.DBHelper;
 2 
 3 import android.content.Context;
 4 import android.database.sqlite.SQLiteDatabase;
 5 import android.database.sqlite.SQLiteOpenHelper;
 6 
 7     /**
 8      * 建立一個數據庫幫助類
 9      */
10 public class DBHelper extends SQLiteOpenHelper {
11     //download.db-->數據庫名
12     public DBHelper(Context context) {
13         super(context, "download.db", null, 1);
14     }
15     
16     /**
17      * 在download.db數據庫下創建一個download_info表存儲下載信息
18      */
19     @Override
20     public void onCreate(SQLiteDatabase db) {
21         db.execSQL("create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer, "
22                 + "start_pos integer, end_pos integer, compelete_size integer,url char)");
23     }
24     @Override
25     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
26 
27     }
28 
29 }

復制代碼

下面來看主界面的佈局,在這裡,我隻設計瞭一個ListView來顯示下載的音樂的名稱,和一個開始下載按鈕和一個暫停按鈕。

佈局文件如下:

main.xml:

復制代碼

 1 
 2 
 7     
10     
11 

復制代碼

list_item.xml:

復制代碼

 1 
 2 
 6     
11         
16         

復制代碼

主界面運行效果如下:

下面我們來看具體實現下載的方法。首先,我們要定義一個記錄在下載時各個時期的數據的類,這裡我創建瞭一個DownloadInfo類來記錄。代碼如下:

DownloadInfo:

復制代碼

 1 package cn.yj3g.entity;
 2 /**
 3  *創建一個下載信息的實體類
 4  */
 5 public class DownloadInfo {
 6     private int threadId;//下載器id
 7     private int startPos;//開始點
 8     private int endPos;//結束點
 9     private int compeleteSize;//完成度
10     private String url;//下載器網絡標識
11     public DownloadInfo(int threadId, int startPos, int endPos,
12             int compeleteSize,String url) {
13         this.threadId = threadId;
14         this.startPos = startPos;
15         this.endPos = endPos;
16         this.compeleteSize = compeleteSize;
17         this.url=url;
18     }
19     public DownloadInfo() {
20     }
21     public String getUrl() {
22         return url;
23     }
24     public void setUrl(String url) {
25         this.url = url;
26     }
27     public int getThreadId() {
28         return threadId;
29     }
30     public void setThreadId(int threadId) {
31         this.threadId = threadId;
32     }
33     public int getStartPos() {
34         return startPos;
35     }
36     public void setStartPos(int startPos) {
37         this.startPos = startPos;
38     }
39     public int getEndPos() {
40         return endPos;
41     }
42     public void setEndPos(int endPos) {
43         this.endPos = endPos;
44     }
45     public int getCompeleteSize() {
46         return compeleteSize;
47     }
48     public void setCompeleteSize(int compeleteSize) {
49         this.compeleteSize = compeleteSize;
50     }
51 
52     @Override
53     public String toString() {
54         return "DownloadInfo [threadId=" + threadId
55                 + ", startPos=" + startPos + ", endPos=" + endPos
56                 + ", compeleteSize=" + compeleteSize +"]";
57     }
58 }

復制代碼

在下載時,我們有進度條來顯示進度,怎麼確定進度條的進度,大小和起始位置呢?這裡我定義瞭一個LoadInfo類來記錄下載器詳細信息。代碼如下:

LoadInfo:

復制代碼

 1 package cn.yj3g.entity;
 2 /**
 3  *自定義的一個記載下載器詳細信息的類 
 4  */
 5 public class LoadInfo {
 6     public int fileSize;//文件大小
 7     private int complete;//完成度
 8     private String urlstring;//下載器標識
 9     public LoadInfo(int fileSize, int complete, String urlstring) {
10         this.fileSize = fileSize;
11         this.complete = complete;
12         this.urlstring = urlstring;
13     }
14     public LoadInfo() {
15     }
16     public int getFileSize() {
17         return fileSize;
18     }
19     public void setFileSize(int fileSize) {
20         this.fileSize = fileSize;
21     }
22     public int getComplete() {
23         return complete;
24     }
25     public void setComplete(int complete) {
26         this.complete = complete;
27     }
28     public String getUrlstring() {
29         return urlstring;
30     }
31     public void setUrlstring(String urlstring) {
32         this.urlstring = urlstring;
33     }
34     @Override
35     public String toString() {
36         return "LoadInfo [fileSize=" + fileSize + ", complete=" + complete
37                 + ", urlstring=" + urlstring + "]";
38     }
39 }

復制代碼

下面是最最重要的一步,那就是定義一個下載器來進行下載瞭,這裡我就不多說,具體解釋在代碼中都有註解,我就直接將代碼附下,供大傢研究參考

Downloader:

復制代碼

  1 package cn.yj3g.service;
  2 
  3 import java.io.File;
  4 import java.io.InputStream;
  5 import java.io.RandomAccessFile;
  6 import java.net.HttpURLConnection;
  7 import java.net.URL;
  8 import java.util.ArrayList;
  9 import java.util.List;
 10 import android.content.Context;
 11 import android.os.Handler;
 12 import android.os.Message;
 13 import android.util.Log;
 14 import cn.yj3g.Dao.Dao;
 15 import cn.yj3g.entity.DownloadInfo;
 16 import cn.yj3g.entity.LoadInfo;
 17 
 18 public class Downloader {
 19     private String urlstr;// 下載的地址
 20     private String localfile;// 保存路徑
 21     private int threadcount;// 線程數
 22     private Handler mHandler;// 消息處理器
 23     private Dao dao;// 工具類
 24     private int fileSize;// 所要下載的文件的大小
 25     private List infos;// 存放下載信息類的集合
 26     private static final int INIT = 1;//定義三種下載的狀態:初始化狀態,正在下載狀態,暫停狀態
 27     private static final int DOWNLOADING = 2;
 28     private static final int PAUSE = 3;
 29     private int state = INIT;
 30 
 31     public Downloader(String urlstr, String localfile, int threadcount,
 32             Context context, Handler mHandler) {
 33         this.urlstr = urlstr;
 34         this.localfile = localfile;
 35         this.threadcount = threadcount;
 36         this.mHandler = mHandler;
 37         dao = new Dao(context);
 38     }
 39     /**
 40      *判斷是否正在下載 
 41      */
 42     public boolean isdownloading() {
 43         return state == DOWNLOADING;
 44     }
 45     /**
 46      * 得到downloader裡的信息
 47      * 首先進行判斷是否是第一次下載,如果是第一次就要進行初始化,並將下載器的信息保存到數據庫中
 48      * 如果不是第一次下載,那就要從數據庫中讀出之前下載的信息(起始位置,結束為止,文件大小等),並將下載信息返回給下載器
 49      */
 50     public LoadInfo getDownloaderInfors() {
 51         if (isFirst(urlstr)) {
 52             Log.v("TAG", "isFirst");
 53             init();
 54             int range = fileSize / threadcount;
 55             infos = new ArrayList();
 56             for (int i = 0; i < threadcount - 1; i++) {
 57                 DownloadInfo info = new DownloadInfo(i, i * range, (i + 1)* range - 1, 0, urlstr);
 58                 infos.add(info);
 59             }
 60             DownloadInfo info = new DownloadInfo(threadcount - 1,(threadcount - 1) * range, fileSize - 1, 0, urlstr);
 61             infos.add(info);
 62             //保存infos中的數據到數據庫
 63             dao.saveInfos(infos);
 64             //創建一個LoadInfo對象記載下載器的具體信息
 65             LoadInfo loadInfo = new LoadInfo(fileSize, 0, urlstr);
 66             return loadInfo;
 67         } else {
 68             //得到數據庫中已有的urlstr的下載器的具體信息
 69             infos = dao.getInfos(urlstr);
 70             Log.v("TAG", "not isFirst size=" + infos.size());
 71             int size = 0;
 72             int compeleteSize = 0;
 73             for (DownloadInfo info : infos) {
 74                 compeleteSize += info.getCompeleteSize();
 75                 size += info.getEndPos() - info.getStartPos() + 1;
 76             }
 77             return new LoadInfo(size, compeleteSize, urlstr);
 78         }
 79     }
 80 
 81     /**
 82      * 初始化
 83      */
 84     private void init() {
 85         try {
 86             URL url = new URL(urlstr);
 87             HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 88             connection.setConnectTimeout(5000);
 89             connection.setRequestMethod("GET");
 90             fileSize = connection.getContentLength();
 91 
 92             File file = new File(localfile);
 93             if (!file.exists()) {
 94                 file.createNewFile();
 95             }
 96             // 本地訪問文件
 97             RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
 98             accessFile.setLength(fileSize);
 99             accessFile.close();
100             connection.disconnect();
101         } catch (Exception e) {
102             e.printStackTrace();
103         }
104     }
105 
106     /**
107      * 判斷是否是第一次 下載
108      */
109     private boolean isFirst(String urlstr) {
110         return dao.isHasInfors(urlstr);
111     }
112 
113     /**
114      * 利用線程開始下載數據
115      */
116     public void download() {
117         if (infos != null) {
118             if (state == DOWNLOADING)
119                 return;
120             state = DOWNLOADING;
121             for (DownloadInfo info : infos) {
122                 new MyThread(info.getThreadId(), info.getStartPos(),
123                         info.getEndPos(), info.getCompeleteSize(),
124                         info.getUrl()).start();
125             }
126         }
127     }
128 
129     public class MyThread extends Thread {
130         private int threadId;
131         private int startPos;
132         private int endPos;
133         private int compeleteSize;
134         private String urlstr;
135 
136         public MyThread(int threadId, int startPos, int endPos,
137                 int compeleteSize, String urlstr) {
138             this.threadId = threadId;
139             this.startPos = startPos;
140             this.endPos = endPos;
141             this.compeleteSize = compeleteSize;
142             this.urlstr = urlstr;
143         }
144         @Override
145         public void run() {
146             HttpURLConnection connection = null;
147             RandomAccessFile randomAccessFile = null;
148             InputStream is = null;
149             try {
150                 URL url = new URL(urlstr);
151                 connection = (HttpURLConnection) url.openConnection();
152                 connection.setConnectTimeout(5000);
153                 connection.setRequestMethod("GET");
154                 // 設置范圍,格式為Range:bytes x-y;
155                 connection.setRequestProperty("Range", "bytes="+(startPos + compeleteSize) + "-" + endPos);
156 
157                 randomAccessFile = new RandomAccessFile(localfile, "rwd");
158                 randomAccessFile.seek(startPos + compeleteSize);
159                 // 將要下載的文件寫到保存在保存路徑下的文件中
160                 is = connection.getInputStream();
161                 byte[] buffer = new byte[4096];
162                 int length = -1;
163                 while ((length = is.read(buffer)) != -1) {
164                     randomAccessFile.write(buffer, 0, length);
165                     compeleteSize += length;
166                     // 更新數據庫中的下載信息
167                     dao.updataInfos(threadId, compeleteSize, urlstr);
168                     // 用消息將下載信息傳給進度條,對進度條進行更新
169                     Message message = Message.obtain();
170                     message.what = 1;
171                     message.obj = urlstr;
172                     message.arg1 = length;
173                     mHandler.sendMessage(message);
174                     if (state == PAUSE) {
175                         return;
176                     }
177                 }
178             } catch (Exception e) {
179                 e.printStackTrace();
180             } finally {
181                 try {
182                     is.close();
183                     randomAccessFile.close();
184                     connection.disconnect();
185                     dao.closeDb();
186                 } catch (Exception e) {
187                     e.printStackTrace();
188                 }
189             }
190 
191         }
192     }
193     //刪除數據庫中urlstr對應的下載器信息
194     public void delete(String urlstr) {
195         dao.delete(urlstr);
196     }
197     //設置暫停
198     public void pause() {
199         state = PAUSE;
200     }
201     //重置下載狀態
202     public void reset() {
203         state = INIT;
204     }
205 }

復制代碼

在這邊下載器類的定義中,我們用到瞭許多關於進行數據庫操作的方法,這裡我定義瞭一個數據庫工具類,來提供這些方法,代碼如下:

Dao:

復制代碼

 1 package cn.yj3g.Dao;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import android.content.Context;
 6 import android.database.Cursor;
 7 import android.database.sqlite.SQLiteDatabase;
 8 import cn.yj3g.DBHelper.DBHelper;
 9 import cn.yj3g.entity.DownloadInfo;
10 
11 /**
12  * 
13  * 一個業務類
14  */
15 public class Dao {
16     private DBHelper dbHelper;
17 
18     public Dao(Context context) {
19         dbHelper = new DBHelper(context);
20     }
21 
22     /**
23      * 查看數據庫中是否有數據
24      */
25     public boolean isHasInfors(String urlstr) {
26         SQLiteDatabase database = dbHelper.getReadableDatabase();
27         String sql = "select count(*)  from download_info where url=?";
28         Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
29         cursor.moveToFirst();
30         int count = cursor.getInt(0);
31         cursor.close();
32         return count == 0;
33     }
34 
35     /**
36      * 保存 下載的具體信息
37      */
38     public void saveInfos(List infos) {
39         SQLiteDatabase database = dbHelper.getWritableDatabase();
40         for (DownloadInfo info : infos) {
41             String sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,url) values (?,?,?,?,?)";
42             Object[] bindArgs = { info.getThreadId(), info.getStartPos(),
43                     info.getEndPos(), info.getCompeleteSize(), info.getUrl() };
44             database.execSQL(sql, bindArgs);
45         }
46     }
47 
48     /**
49      * 得到下載具體信息
50      */
51     public List getInfos(String urlstr) {
52         List list = new ArrayList();
53         SQLiteDatabase database = dbHelper.getReadableDatabase();
54         String sql = "select thread_id, start_pos, end_pos,compelete_size,url from download_info where url=?";
55         Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
56         while (cursor.moveToNext()) {
57             DownloadInfo info = new DownloadInfo(cursor.getInt(0),
58                     cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),
59                     cursor.getString(4));
60             list.add(info);
61         }
62         cursor.close();
63         return list;
64     }
65 
66     /**
67      * 更新數據庫中的下載信息
68      */
69     public void updataInfos(int threadId, int compeleteSize, String urlstr) {
70         SQLiteDatabase database = dbHelper.getReadableDatabase();
71         String sql = "update download_info set compelete_size=? where thread_id=? and url=?";
72         Object[] bindArgs = { compeleteSize, threadId, urlstr };
73         database.execSQL(sql, bindArgs);
74     }
75     /**
76      * 關閉數據庫
77      */
78     public void closeDb() {
79         dbHelper.close();
80     }
81 
82     /**
83      * 下載完成後刪除數據庫中的數據
84      */
85     public void delete(String url) {
86         SQLiteDatabase database = dbHelper.getReadableDatabase();
87         database.delete("download_info", "url=?", new String[] { url });
88         database.close();
89     }
90 }

復制代碼

下來就是要進行下載和暫停按鈕的響應事件瞭。具體代碼和解釋如下。

MainActivity:

復制代碼

  1 package cn.yj3g.AndroidDownload;
  2 
  3 import java.util.ArrayList;
  4 import java.util.HashMap;
  5 import java.util.List;
  6 import java.util.Map;
  7 import android.app.ListActivity;
  8 import android.os.Bundle;
  9 import android.os.Handler;
 10 import android.os.Message;
 11 import android.view.View;
 12 import android.widget.LinearLayout;
 13 import android.widget.LinearLayout.LayoutParams;
 14 import android.widget.ProgressBar;
 15 import android.widget.SimpleAdapter;
 16 import android.widget.TextView;
 17 import android.widget.Toast;
 18 import cn.yj3g.entity.LoadInfo;
 19 import cn.yj3g.service.Downloader;
 20 
 21 public class MainActivity extends ListActivity {
 22     // 固定下載的資源路徑,這裡可以設置網絡上的地址
 23     private static final String URL = "https://192.168.1.105:8080/struts2_net/";
 24     // 固定存放下載的音樂的路徑:SD卡目錄下
 25     private static final String SD_PATH = "/mnt/sdcard/";
 26     // 存放各個下載器
 27     private Map downloaders = new HashMap();
 28     // 存放與下載器對應的進度條
 29     private Map ProgressBars = new HashMap();
 30     /**
 31      * 利用消息處理機制適時更新進度條
 32      */
 33     private Handler mHandler = new Handler() {
 34         public void handleMessage(Message msg) {
 35             if (msg.what == 1) {
 36                 String url = (String) msg.obj;
 37                 int length = msg.arg1;
 38                 ProgressBar bar = ProgressBars.get(url);
 39                 if (bar != null) {
 40                     // 設置進度條按讀取的length長度更新
 41                     bar.incrementProgressBy(length);
 42                     if (bar.getProgress() == bar.getMax()) {
 43                         Toast.makeText(MainActivity.this, "下載完成!", 0).show();
 44                         // 下載完成後清除進度條並將map中的數據清空
 45                         LinearLayout layout = (LinearLayout) bar.getParent();
 46                         layout.removeView(bar);
 47                         ProgressBars.remove(url);
 48                         downloaders.get(url).delete(url);
 49                         downloaders.get(url).reset();
 50                         downloaders.remove(url);
 51 
 52                     }
 53                 }
 54             }
 55         }
 56     };
 57     @Override
 58     public void onCreate(Bundle savedInstanceState) {
 59         super.onCreate(savedInstanceState);
 60         setContentView(R.layout.main);
 61         showListView();
 62     }
 63     // 顯示listView,這裡可以隨便添加音樂
 64     private void showListView() {
 65         List<Map> data = new ArrayList<Map>();
 66         Map map = new HashMap();
 67         map.put("name", "mm.mp3");
 68         data.add(map);
 69         map = new HashMap();
 70         map.put("name", "pp.mp3");
 71         data.add(map);
 72         map = new HashMap();
 73         map.put("name", "tt.mp3");
 74         data.add(map);
 75         map = new HashMap();
 76         map.put("name", "You.mp3");
 77         data.add(map);
 78         SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.list_item, new String[] { "name" },
 79                 new int[] { R.id.tv_resouce_name });
 80         setListAdapter(adapter);
 81     }
 82     /**
 83      * 響應開始下載按鈕的點擊事件
 84      */
 85     public void startDownload(View v) {
 86         // 得到textView的內容
 87         LinearLayout layout = (LinearLayout) v.getParent();
 88         String musicName = ((TextView) layout.findViewById(R.id.tv_resouce_name)).getText().toString();
 89         String urlstr = URL + musicName;
 90         String localfile = SD_PATH + musicName;
 91         //設置下載線程數為4,這裡是我為瞭方便隨便固定的
 92         int threadcount = 4;
 93         // 初始化一個downloader下載器
 94         Downloader downloader = downloaders.get(urlstr);
 95         if (downloader == null) {
 96             downloader = new Downloader(urlstr, localfile, threadcount, this, mHandler);
 97             downloaders.put(urlstr, downloader);
 98         }
 99         if (downloader.isdownloading())
100             return;
101         // 得到下載信息類的個數組成集合
102         LoadInfo loadInfo = downloader.getDownloaderInfors();
103         // 顯示進度條
104         showProgress(loadInfo, urlstr, v);
105         // 調用方法開始下載
106         downloader.download();
107     }
108 
109     /**
110      * 顯示進度條
111      */
112     private void showProgress(LoadInfo loadInfo, String url, View v) {
113         ProgressBar bar = ProgressBars.get(url);
114         if (bar == null) {
115             bar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
116             bar.setMax(loadInfo.getFileSize());
117             bar.setProgress(loadInfo.getComplete());
118             ProgressBars.put(url, bar);
119             LinearLayout.LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, 5);
120             ((LinearLayout) ((LinearLayout) v.getParent()).getParent()).addView(bar, params);
121         }
122     }
123     /**
124      * 響應暫停下載按鈕的點擊事件
125      */
126     public void pauseDownload(View v) {
127         LinearLayout layout = (LinearLayout) v.getParent();
128         String musicName = ((TextView) layout.findViewById(R.id.tv_resouce_name)).getText().toString();
129         String urlstr = URL + musicName;
130         downloaders.get(urlstr).pause();
131     }
132 }

復制代碼

最後我們要在清單文件中添加權限,一個是訪問網絡的權限,一個是往SD卡寫數據的權限。代碼如下:

 
 

這樣我們就實現瞭文件的斷點續傳下載功能。具體效果圖如下:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。