[转载]Android之断点续传下载

[转载]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 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:orientation="vertical"
 4     android:layout_width="fill_parent"
 5     android:layout_height="fill_parent"
 6     android:id="@+id/llRoot">
 7     <ListView android:id="@android:id/list"
 8         android:layout_width="fill_parent"
 9         android:layout_height="fill_parent">
10     </ListView>
11 </LinearLayout>

list_item.xml:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3            android:orientation="vertical"
 4            android:layout_width="fill_parent"
 5            android:layout_height="wrap_content">
 6     <LinearLayout
 7            android:orientation="horizontal"
 8            android:layout_width="fill_parent"
 9            android:layout_height="wrap_content"
10            android:layout_marginBottom="5dip">
11         <TextView 
12             android:layout_width="fill_parent"
13             android:layout_height="wrap_content"
14             android:layout_weight="1"
15             android:id="@+id/tv_resouce_name"/>
16         <Button
17             android:layout_width="fill_parent"
18             android:layout_height="wrap_content"
19             android:layout_weight="1"
20             android:text="下载"
21             android:id="@+id/btn_start"
22             android:onClick="startDownload"/>
23         <Button
24             android:layout_width="fill_parent"
25             android:layout_height="wrap_content"
26             android:layout_weight="1"
27             android:text="暂停"
28             android:id="@+id/btn_pause"
29             android:onClick="pauseDownload"/>
30       </LinearLayout>
31 </LinearLayout>

主界面运行效果如下:

下面我们来看具体实现下载的方法。首先,我们要定义一个记录在下载时各个时期的数据的类,这里我创建了一个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<DownloadInfo> 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<DownloadInfo>();
 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<DownloadInfo> 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<DownloadInfo> getInfos(String urlstr) {
52         List<DownloadInfo> list = new ArrayList<DownloadInfo>();
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 = "http://192.168.1.105:8080/struts2_net/";
 24     // 固定存放下载的音乐的路径:SD卡目录下
 25     private static final String SD_PATH = "/mnt/sdcard/";
 26     // 存放各个下载器
 27     private Map<String, Downloader> downloaders = new HashMap<String, Downloader>();
 28     // 存放与下载器对应的进度条
 29     private Map<String, ProgressBar> ProgressBars = new HashMap<String, ProgressBar>();
 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<String, String>> data = new ArrayList<Map<String, String>>();
 66         Map<String, String> map = new HashMap<String, String>();
 67         map.put("name", "mm.mp3");
 68         data.add(map);
 69         map = new HashMap<String, String>();
 70         map.put("name", "pp.mp3");
 71         data.add(map);
 72         map = new HashMap<String, String>();
 73         map.put("name", "tt.mp3");
 74         data.add(map);
 75         map = new HashMap<String, String>();
 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卡写数据的权限。代码如下:

 <uses-permission android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

这样我们就实现了文件的断点续传下载功能。具体效果图如下:

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏