多线程下载大文件时序图
FileDownloader.java
1 package com.wangjialin.internet.service.downloader; 2 3 import java.io.File; 4 import java.io.RandomAccessFile; 5 import java.net.HttpURLConnection; 6 import java.net.URL; 7 import java.util.LinkedHashMap; 8 import java.util.Map; 9 import java.util.UUID; 10 import java.util.concurrent.ConcurrentHashMap; 11 import java.util.regex.Matcher; 12 import java.util.regex.Pattern; 13 14 import android.content.Context; 15 import android.util.Log; 16 17 import com.wangjialin.internet.service.FileService; 18 19 public class FileDownloader { 20 21 private static final String TAG = "FileDownloader"; //设置标签,方便Logcat日志记录 22 private static final int RESPONSEOK = 200; //响应码为200,即访问成功 23 private Context context; //应用程序的上下文对象 24 private FileService fileService; //获取本地数据库的业务Bean 25 private boolean exited; //停止下载标志 26 private int downloadedSize = 0; //已下载文件长度 27 private int fileSize = 0; //原始文件长度 28 private DownloadThread[] threads; //根据线程数设置下载线程池 29 private File saveFile; //数据保存到的本地文件 30 private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>(); //缓存各线程下载的长度 31 private int block; //每条线程下载的长度 32 private String downloadUrl; //下载路径 33 34 /** 35 * 获取线程数 36 */ 37 public int getThreadSize() { 38 return threads.length; //根据数组长度返回线程数 39 } 40 41 /** 42 * 退出下载 43 */ 44 public void exit(){ 45 this.exited = true; //设置退出标志为true 46 } 47 public boolean getExited(){ 48 return this.exited; 49 } 50 /** 51 * 获取文件大小 52 * @return 53 */ 54 public int getFileSize() { 55 return fileSize; //从类成员变量中获取下载文件的大小 56 } 57 58 /** 59 * 累计已下载大小 60 * @param size 61 */ 62 protected synchronized void append(int size) { //使用同步关键字解决并发访问问题 63 downloadedSize += size; //把实时下载的长度加入到总下载长度中 64 } 65 66 /** 67 * 更新指定线程最后下载的位置 68 * @param threadId 线程id 69 * @param pos 最后下载的位置 70 */ 71 protected synchronized void update(int threadId, int pos) { 72 this.data.put(threadId, pos); //把制定线程ID的线程赋予最新的下载长度,以前的值会被覆盖掉 73 this.fileService.update(this.downloadUrl, threadId, pos); //更新数据库中指定线程的下载长度 74 } 75 /** 76 * 构建文件下载器 77 * @param downloadUrl 下载路径 78 * @param fileSaveDir 文件保存目录 79 * @param threadNum 下载线程数 80 */ 81 public FileDownloader(Context context, String downloadUrl, File fileSaveDir, int threadNum) { 82 83 try { 84 this.context = context; //对上下文对象赋值 85 this.downloadUrl = downloadUrl; //对下载的路径赋值 86 fileService = new FileService(this.context); //实例化数据操作业务Bean,此处需要使用Context,因为此处的数据库是应用程序私有 87 URL url = new URL(this.downloadUrl); //根据下载路径实例化URL 88 if(!fileSaveDir.exists()) fileSaveDir.mkdirs(); //如果指定的文件不存在,则创建目录,此处可以创建多层目录 89 this.threads = new DownloadThread[threadNum]; //根据下载的线程数创建下载线程池 90 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //建立一个远程连接句柄,此时尚未真正连接 91 conn.setConnectTimeout(5*1000); //设置连接超时时间为5秒 92 conn.setRequestMethod("GET"); //设置请求方式为GET 93 conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); //设置客户端可以接受的媒体类型 94 conn.setRequestProperty("Accept-Language", "zh-CN"); //设置客户端语言 95 conn.setRequestProperty("Referer", downloadUrl); //设置请求的来源页面,便于服务端进行来源统计 96 conn.setRequestProperty("Charset", "UTF-8"); //设置客户端编码 97 conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"); //设置用户代理 98 conn.setRequestProperty("Connection", "Keep-Alive"); //设置Connection的方式 99 conn.connect(); //和远程资源建立真正的连接,但尚无返回的数据流 100 printResponseHeader(conn); //答应返回的HTTP头字段集合 101 102 if (conn.getResponseCode()==RESPONSEOK) { //此处的请求会打开返回流并获取返回的状态码,用于检查是否请求成功,当返回码为200时执行下面的代码 103 this.fileSize = conn.getContentLength();//根据响应获取文件大小 104 if (this.fileSize <= 0) throw new RuntimeException("Unkown file size "); //当文件大小为小于等于零时抛出运行时异常 105 106 String filename = getFileName(conn);//获取文件名称 107 this.saveFile = new File(fileSaveDir, filename);//根据文件保存目录和文件名构建保存文件 108 Map<Integer, Integer> logdata = fileService.getData(downloadUrl);//获取下载记录 109 110 if(logdata.size()>0){//如果存在下载记录 111 for(Map.Entry<Integer, Integer> entry : logdata.entrySet()) //遍历集合中的数据 112 data.put(entry.getKey(), entry.getValue());//把各条线程已经下载的数据长度放入data中 113 } 114 115 if(this.data.size()==this.threads.length){//如果已经下载的数据的线程数和现在设置的线程数相同时则计算所有线程已经下载的数据总长度 116 for (int i = 0; i < this.threads.length; i++) { //遍历每条线程已经下载的数据 117 this.downloadedSize += this.data.get(i+1); //计算已经下载的数据之和 118 } 119 print("已经下载的长度"+ this.downloadedSize + "个字节"); //打印出已经下载的数据总和 120 } 121 122 this.block = (this.fileSize % this.threads.length)==0? this.fileSize / this.threads.length : this.fileSize / this.threads.length + 1; //计算每条线程下载的数据长度 123 }else{ 124 print("服务器响应错误:" + conn.getResponseCode() + conn.getResponseMessage()); //打印错误 125 throw new RuntimeException("server response error "); //抛出运行时服务器返回异常 126 } 127 } catch (Exception e) { 128 print(e.toString()); //打印错误 129 throw new RuntimeException("Can't connection this url"); //抛出运行时无法连接的异常 130 } 131 } 132 /** 133 * 获取文件名 134 */ 135 private String getFileName(HttpURLConnection conn) { 136 String filename = this.downloadUrl.substring(this.downloadUrl.lastIndexOf('/') + 1); //从下载路径的字符串中获取文件名称 137 138 if(filename==null || "".equals(filename.trim())){//如果获取不到文件名称 139 for (int i = 0;; i++) { //无限循环遍历 140 String mine = conn.getHeaderField(i); //从返回的流中获取特定索引的头字段值 141 if (mine == null) break; //如果遍历到了返回头末尾这退出循环 142 if("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())){ //获取content-disposition返回头字段,里面可能会包含文件名 143 Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase()); //使用正则表达式查询文件名 144 if(m.find()) return m.group(1); //如果有符合正则表达规则的字符串 145 } 146 } 147 filename = UUID.randomUUID()+ ".tmp";//由网卡上的标识数字(每个网卡都有唯一的标识号)以及 CPU 时钟的唯一数字生成的的一个 16 字节的二进制作为文件名 148 } 149 return filename; 150 } 151 152 /** 153 * 开始下载文件 154 * @param listener 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null 155 * @return 已下载文件大小 156 * @throws Exception 157 */ 158 public int download(DownloadProgressListener listener) throws Exception{ //进行下载,并抛出异常给调用者,如果有异常的话 159 160 try { 161 RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rwd"); //The file is opened for reading and writing. Every change of the file's content must be written synchronously to the target device. 162 if(this.fileSize>0) randOut.setLength(this.fileSize); //设置文件的大小 163 randOut.close(); //关闭该文件,使设置生效 164 URL url = new URL(this.downloadUrl); //A URL instance specifies the location of a resource on the internet as specified by RFC 1738 165 if(this.data.size() != this.threads.length){ //如果原先未曾下载或者原先的下载线程数与现在的线程数不一致 166 this.data.clear(); //Removes all elements from this Map, leaving it empty. 167 for (int i = 0; i < this.threads.length; i++) { //遍历线程池 168 this.data.put(i+1, 0);//初始化每条线程已经下载的数据长度为0 169 } 170 this.downloadedSize = 0; //设置已经下载的长度为0 171 } 172 for (int i = 0; i < this.threads.length; i++) {//开启线程进行下载 173 int downloadedLength = this.data.get(i+1); //通过特定的线程ID获取该线程已经下载的数据长度 174 if(downloadedLength < this.block && this.downloadedSize < this.fileSize){//判断线程是否已经完成下载,否则继续下载 175 this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1); //初始化特定id的线程 176 this.threads[i].setPriority(7); //设置线程的优先级,Thread.NORM_PRIORITY = 5 Thread.MIN_PRIORITY = 1 Thread.MAX_PRIORITY = 10 177 this.threads[i].start(); //启动线程 178 }else{ 179 this.threads[i] = null; //表明在线程已经完成下载任务 180 } 181 } 182 fileService.delete(this.downloadUrl); //如果存在下载记录,删除它们,然后重新添加 183 fileService.save(this.downloadUrl, this.data); //把已经下载的实时数据写入数据库 184 boolean notFinished = true;//下载未完成 185 while (notFinished) {// 循环判断所有线程是否完成下载 186 Thread.sleep(900); 187 notFinished = false;//假定全部线程下载完成 188 for (int i = 0; i < this.threads.length; i++){ 189 if (this.threads[i] != null && !this.threads[i].isFinished()) {//如果发现线程未完成下载 190 notFinished = true;//设置标志为下载没有完成 191 if(this.threads[i].getDownloadedLength() == -1){//如果下载失败,再重新在已经下载的数据长度的基础上下载 192 this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1); //重新开辟下载线程 193 this.threads[i].setPriority(7); //设置下载的优先级 194 this.threads[i].start(); //开始下载线程 195 } 196 } 197 } 198 if(listener!=null) listener.onDownloadSize(this.downloadedSize);//通知目前已经下载完成的数据长度 199 } 200 if(downloadedSize == this.fileSize) fileService.delete(this.downloadUrl);//下载完成删除记录 201 } catch (Exception e) { 202 print(e.toString()); //打印错误 203 throw new Exception("File downloads error"); //抛出文件下载异常 204 } 205 return this.downloadedSize; 206 } 207 /** 208 * 获取Http响应头字段 209 * @param http HttpURLConnection对象 210 * @return 返回头字段的LinkedHashMap 211 */ 212 public static Map<String, String> getHttpResponseHeader(HttpURLConnection http) { 213 Map<String, String> header = new LinkedHashMap<String, String>(); //使用LinkedHashMap保证写入和遍历的时候的顺序相同,而且允许空值存在 214 for (int i = 0;; i++) { //此处为无限循环,因为不知道头字段的数量 215 String fieldValue = http.getHeaderField(i); //getHeaderField(int n)用于返回 第n个头字段的值。 216 217 if (fieldValue == null) break; //如果第i个字段没有值了,则表明头字段部分已经循环完毕,此处使用Break退出循环 218 header.put(http.getHeaderFieldKey(i), fieldValue); //getHeaderFieldKey(int n)用于返回 第n个头字段的键。 219 } 220 return header; 221 } 222 /** 223 * 打印 Http 头字段 224 * @param http HttpURLConnection对象 225 */ 226 public static void printResponseHeader(HttpURLConnection http){ 227 Map<String, String> header = getHttpResponseHeader(http); //获取Http响应头字段 228 for(Map.Entry<String, String> entry : header.entrySet()){ //使用For-Each循环的方式遍历获取的头字段的值,此时遍历的循序和输入的顺序相同 229 String key = entry.getKey()!=null ? entry.getKey()+ ":" : ""; //当有键的时候这获取键,如果没有则为空字符串 230 print(key+ entry.getValue()); //答应键和值的组合 231 } 232 } 233 234 /** 235 * 打印信息 236 * @param msg 信息字符串 237 */ 238 private static void print(String msg){ 239 Log.i(TAG, msg); //使用LogCat的Information方式打印信息 240 } 241 }
DownloadThread.java
1 package com.wangjialin.internet.service.downloader; 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 9 import android.util.Log; 10 11 /** 12 * 下载线程,根据具体下载地址、保持到的文件、下载块的大小、已经下载的数据大小等信息进行下载 13 * @author Wang Jialin 14 * 15 */ 16 public class DownloadThread extends Thread { 17 18 private static final String TAG = "DownloadThread"; //定义TAG,方便日子的打印输出 19 private File saveFile; //下载的数据保存到的文件 20 private URL downUrl; //下载的URL 21 private int block; //每条线程下载的大小 22 private int threadId = -1; //初始化线程id设置 23 private int downloadedLength; //该线程已经下载的数据长度 24 private boolean finished = false; //该线程是否完成下载的标志 25 private FileDownloader downloader; //文件下载器 26 27 public DownloadThread(FileDownloader downloader, URL downUrl, File saveFile, int block, int downloadedLength, int threadId) { 28 this.downUrl = downUrl; 29 this.saveFile = saveFile; 30 this.block = block; 31 this.downloader = downloader; 32 this.threadId = threadId; 33 this.downloadedLength = downloadedLength; 34 } 35 36 @Override 37 public void run() { 38 39 if(downloadedLength < block){//未下载完成 40 try { 41 HttpURLConnection http = (HttpURLConnection) downUrl.openConnection(); //开启HttpURLConnection连接 42 http.setConnectTimeout(5 * 1000); //设置连接超时时间为5秒钟 43 http.setRequestMethod("GET"); //设置请求的方法为GET 44 http.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); //设置客户端可以接受的返回数据类型 45 http.setRequestProperty("Accept-Language", "zh-CN"); //设置客户端使用的语言问中文 46 http.setRequestProperty("Referer", downUrl.toString()); //设置请求的来源,便于对访问来源进行统计 47 http.setRequestProperty("Charset", "UTF-8"); //设置通信编码为UTF-8 48 int startPos = block * (threadId - 1) + downloadedLength;//开始位置 49 int endPos = block * threadId -1;//结束位置 50 http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//设置获取实体数据的范围,如果超过了实体数据的大小会自动返回实际的数据大小 51 http.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"); //客户端用户代理 52 http.setRequestProperty("Connection", "Keep-Alive"); //使用长连接 53 54 InputStream inStream = http.getInputStream(); //获取远程连接的输入流 55 byte[] buffer = new byte[1024]; //设置本地数据缓存的大小为1M 56 int offset = 0; //设置每次读取的数据量 57 print("Thread " + this.threadId + " starts to download from position "+ startPos); //打印该线程开始下载的位置 58 RandomAccessFile threadFile = new RandomAccessFile(this.saveFile, "rwd"); //If the file does not already exist then an attempt will be made to create it and it require that every update to the file's content be written synchronously to the underlying storage device. 59 threadFile.seek(startPos); //文件指针指向开始下载的位置 60 61 while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) { //但用户没有要求停止下载,同时没有到达请求数据的末尾时候会一直循环读取数据 62 threadFile.write(buffer, 0, offset); //直接把数据写到文件中 63 downloadedLength += offset; //把新下载的已经写到文件中的数据加入到下载长度中 64 downloader.update(this.threadId, downloadedLength); //把该线程已经下载的数据长度更新到数据库和内存哈希表中 65 downloader.append(offset); //把新下载的数据长度加入到已经下载的数据总长度中 66 }//该线程下载数据完毕或者下载被用户停止 67 68 threadFile.close(); //Closes this random access file stream and releases any system resources associated with the stream. 69 inStream.close(); //Concrete implementations of this class should free any resources during close 70 if(downloader.getExited()) 71 { 72 print("Thread " + this.threadId + " has been paused"); 73 } 74 else 75 { 76 print("Thread " + this.threadId + " download finish"); 77 } 78 79 this.finished = true; //设置完成标志为true,无论是下载完成还是用户主动中断下载 80 } catch (Exception e) { //出现异常 81 this.downloadedLength = -1; //设置该线程已经下载的长度为-1 82 print("Thread "+ this.threadId+ ":"+ e); //打印出异常信息 83 } 84 } 85 } 86 /** 87 * 打印信息 88 * @param msg 信息 89 */ 90 private static void print(String msg){ 91 Log.i(TAG, msg); // 使用 Logcat 的 Information 方式打印信息 92 } 93 94 /** 95 * 下载是否完成 96 * @return 97 */ 98 public boolean isFinished() { 99 return finished; 100 } 101 102 /** 103 * 已经下载的内容大小 104 * @return 如果返回值为-1,代表下载失败 105 */ 106 public long getDownloadedLength() { 107 return downloadedLength; 108 } 109 }
FileService.java
1 package com.wangjialin.internet.service; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 import android.content.Context; 7 import android.database.Cursor; 8 import android.database.sqlite.SQLiteDatabase; 9 10 11 /** 12 * 业务Bean,实现对数据的操作 13 * @author Wang Jialin 14 * 15 */ 16 public class FileService { 17 private DBOpenHelper openHelper; // 声明数据库管理器 18 19 public FileService(Context context) { 20 openHelper = new DBOpenHelper(context); //根据上下文对象实例化数据库管理器 21 } 22 /** 23 * 获取特定URI的每条线程已经下载的文件长度 24 * @param path 25 * @return 26 */ 27 public Map<Integer, Integer> getData(String path){ 28 29 SQLiteDatabase db = openHelper.getReadableDatabase(); //获取可读的数据库句柄,一般情况下在该操作的内部实现中其返回的其实是可写的数据库句柄 30 Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?", new String[]{path}); //根据下载路径查询所有线程下载数据,返回的Cursor指向第一条记录之前 31 Map<Integer, Integer> data = new HashMap<Integer, Integer>(); //建立一个哈希表用于存放每条线程的已经下载的文件长度 32 while(cursor.moveToNext()){ //从第一条记录开始开始遍历Cursor对象 33 data.put(cursor.getInt(0), cursor.getInt(1)); //把线程id和该线程已下载的长度设置进data哈希表中 34 data.put(cursor.getInt(cursor.getColumnIndexOrThrow("threadid")), cursor.getInt(cursor.getColumnIndexOrThrow("downlength"))); 35 } 36 cursor.close(); //关闭cursor,释放资源 37 db.close(); //关闭数据库 38 return data; //返回获得的每条线程和每条线程的下载长度 39 } 40 /** 41 * 保存每条线程已经下载的文件长度 42 * @param path 下载的路径 43 * @param map 现在的id和已经下载的长度的集合 44 */ 45 public void save(String path, Map<Integer, Integer> map){ 46 47 SQLiteDatabase db = openHelper.getWritableDatabase(); //获取可写的数据库句柄 48 db.beginTransaction(); //开始事务,因为此处要插入多批数据 49 try{ 50 for(Map.Entry<Integer, Integer> entry : map.entrySet()){ //采用For-Each的方式遍历数据集合 51 db.execSQL("insert into filedownlog(downpath, threadid, downlength) values(?,?,?)", 52 new Object[]{path, entry.getKey(), entry.getValue()}); //插入特定下载路径特定线程ID已经下载的数据 53 } 54 db.setTransactionSuccessful(); //设置事务执行的标志为成功 55 }finally{ //此部分的代码肯定是被执行的,如果不杀死虚拟机的话 56 db.endTransaction(); //结束一个事务,如果事务设立了成功标志,则提交事务,否则会滚事务 57 } 58 db.close(); //关闭数据库,释放相关资源 59 } 60 61 /** 62 * 实时更新每条线程已经下载的文件长度 63 * @param path 64 * @param map 65 */ 66 public void update(String path, int threadId, int pos){ 67 SQLiteDatabase db = openHelper.getWritableDatabase(); //获取可写的数据库句柄 68 db.execSQL("update filedownlog set downlength=? where downpath=? and threadid=?", 69 new Object[]{pos, path, threadId}); //更新特定下载路径下特定线程已经下载的文件长度 70 db.close(); //关闭数据库,释放相关的资源 71 } 72 73 /** 74 * 当文件下载完成后,删除对应的下载记录 75 * @param path 76 */ 77 public void delete(String path){ 78 SQLiteDatabase db = openHelper.getWritableDatabase(); //获取可写的数据库句柄 79 db.execSQL("delete from filedownlog where downpath=?", new Object[]{path}); //删除特定下载路径的所有线程记录 80 db.close(); //关闭数据库,释放资源 81 } 82 83 }
DBOpenHelper.java
1 package com.wangjialin.internet.service; 2 3 import android.content.Context; 4 import android.database.sqlite.SQLiteDatabase; 5 import android.database.sqlite.SQLiteOpenHelper; 6 7 /** 8 * SQLite管理器,实现创建数据库和表,但版本变化时实现对表的数据库表的操作 9 * @author think 10 * 11 */ 12 public class DBOpenHelper extends SQLiteOpenHelper { 13 14 private static final String DBNAME = "eric.db"; //设置数据库的名称 15 private static final int VERSION = 1; //设置数据库的版本 16 17 /** 18 * 通过构造方法 19 * @param context 应用程序的上下文对象 20 */ 21 public DBOpenHelper(Context context) { 22 super(context, DBNAME, null, VERSION); 23 } 24 25 @Override 26 public void onCreate(SQLiteDatabase db) { //建立数据表 27 db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)"); 28 } 29 30 @Override 31 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //当版本变化时系统会调用该回调方法 32 33 db.execSQL("DROP TABLE IF EXISTS filedownlog"); //此处是删除数据表,在实际的业务中一般是需要数据备份的 34 onCreate(db); //调用onCreate方法重新创建数据表,也可以自己根据业务需要创建新的的数据表 35 } 36 37 }
DownloadProgressListener
1 package com.wangjialin.internet.service.downloader; 2 3 /** 4 * 下载进度监听器 5 * @author Wang Jialin 6 * 7 */ 8 public interface DownloadProgressListener { 9 /** 10 * 下载进度监听方法 获取和处理下载点数据的大小 11 * @param size 数据大小 12 */ 13 public void onDownloadSize(int size); 14 }
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 > 7 <!-- 下载路径提示文字 --> 8 <TextView 9 android:layout_width="fill_parent" 10 android:layout_height="wrap_content" 11 android:text="@string/path" 12 /> 13 <!-- 下载路径输入框,此处为了方便测试,我们设置了默认的路径,可以根据需要在用户界面处修改 --> 14 <EditText 15 android:layout_width="fill_parent" 16 android:layout_height="wrap_content" 17 android:text="http://192.168.1.103:8080/ServerForMultipleThreadDownloader/CNNRecordingFromWangjialin.mp3" 18 android:id="@+id/path" 19 /> 20 21 <!-- 水平LinearLayout布局,包裹下载按钮和暂停按钮 --> 22 <LinearLayout 23 android:orientation="horizontal" 24 android:layout_width="fill_parent" 25 android:layout_height="wrap_content" 26 > 27 <!-- 下载按钮,用于触发下载事件 --> 28 <Button 29 android:layout_width="wrap_content" 30 android:layout_height="wrap_content" 31 android:text="@string/button" 32 android:id="@+id/downloadbutton" 33 /> 34 <!-- 暂停按钮,在初始状态下为不可用 --> 35 <Button 36 android:layout_width="wrap_content" 37 android:layout_height="wrap_content" 38 android:text="@string/stopbutton" 39 android:enabled="false" 40 android:id="@+id/stopbutton" 41 /> 42 </LinearLayout> 43 44 45 <!-- 水平进度条,用图形化的方式实时显示进步信息 --> 46 <ProgressBar 47 android:layout_width="fill_parent" 48 android:layout_height="18dp" 49 style="?android:attr/progressBarStyleHorizontal" 50 android:id="@+id/progressBar" 51 /> 52 53 <!-- 文本框,用于显示实时下载的百分比 --> 54 <TextView 55 android:layout_width="fill_parent" 56 android:layout_height="wrap_content" 57 android:gravity="center" 58 android:id="@+id/resultView" 59 /> 60 61 </LinearLayout>
DownloaderActivity.java
1 package com.wangjialin.internet.Downloader; 2 3 import java.io.File; 4 5 import com.wangjialin.internet.Downloader.R; 6 import com.wangjialin.internet.service.downloader.DownloadProgressListener; 7 import com.wangjialin.internet.service.downloader.FileDownloader; 8 9 import android.app.Activity; 10 import android.os.Bundle; 11 import android.os.Environment; 12 import android.os.Handler; 13 import android.os.Message; 14 import android.view.View; 15 import android.widget.Button; 16 import android.widget.EditText; 17 import android.widget.ProgressBar; 18 import android.widget.TextView; 19 import android.widget.Toast; 20 21 public class DownloaderActivity extends Activity { 22 23 private static final int PROCESSING = 1; 24 private static final int FAILURE = -1; 25 26 private EditText pathText; 27 private TextView resultView; 28 private Button downloadButton; 29 private Button stopbutton; 30 private ProgressBar progressBar; 31 32 private Handler handler = new UIHander(); 33 34 private final class UIHander extends Handler{ 35 /** 36 * 系统会自动调用的回调方法,用于处理消息事件 37 * Message 一般会包含消息的标志和消息的内容以及消息的处理器 Handler 38 */ 39 public void handleMessage(Message msg){ 40 switch(msg.what){ 41 case PROCESSING: // 下载时 42 // 从消息中获取已经下载的数据长度 43 int size = msg.getData().getInt("size"); 44 // 设置进度条的进度 45 progressBar.setProgress(size); 46 // 计算已经下载的百分比,浮点数计算 47 float num = (float)progressBar.getProgress()/(float)progressBar.getMax(); 48 // 把获得的浮点数计算结果转换为整数 49 int result = (int)(num * 100); 50 // 把下载的百分比显示在界面控件上 51 resultView.setText(result + "%"); 52 if(progressBar.getProgress() == progressBar.getMax()){ 53 Toast.makeText(getApplicationContext(), R.string.success, Toast.LENGTH_LONG).show(); 54 } 55 break; 56 case -1: // 下载失败 57 Toast.makeText(getApplicationContext(), R.string.error, Toast.LENGTH_LONG).show(); 58 break; 59 } 60 } 61 } 62 63 /* (non-Javadoc) 64 * @see android.app.Activity#onCreate(android.os.Bundle) 65 */ 66 @Override 67 protected void onCreate(Bundle savedInstanceState) { 68 // TODO Auto-generated method stub 69 super.onCreate(savedInstanceState); 70 71 setContentView(R.layout.main); 72 pathText = (EditText)this.findViewById(R.id.path); 73 resultView = (TextView)this.findViewById(R.id.resultView); 74 downloadButton = (Button)this.findViewById(R.id.downloadbutton); 75 stopbutton = (Button)this.findViewById(R.id.stopbutton); 76 progressBar = (ProgressBar)this.findViewById(R.id.progressBar); 77 ButtonClickListener listener = new ButtonClickListener(); 78 downloadButton.setOnClickListener(listener); 79 stopbutton.setOnClickListener(listener); 80 } 81 82 /** 83 * 按钮监听器实现类 84 * 85 */ 86 private final class ButtonClickListener implements View.OnClickListener{ 87 88 @Override 89 public void onClick(View v) { 90 // TODO Auto-generated method stub 91 switch(v.getId()){ 92 case R.id.downloadbutton: 93 String path = pathText.getText().toString(); 94 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ 95 // 获取 SDCard 根目录 96 Environment.getExternalStorageDirectory(); 97 File saveDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); 98 getExternalFilesDir(Environment.DIRECTORY_MOVIES); 99 // 下载文件 100 download(path, saveDir); 101 }else{ 102 // 当 SDCard 不存在时 103 Toast.makeText(getApplicationContext(), R.string.sdcarderror, Toast.LENGTH_LONG).show(); 104 } 105 downloadButton.setEnabled(false); 106 stopbutton.setEnabled(true); 107 break; 108 109 case R.id.stopbutton: 110 exit(); 111 downloadButton.setEnabled(true); 112 stopbutton.setEnabled(false); 113 break; 114 } 115 } 116 117 private DownloadTask task; // 声明下载执行者 118 /** 119 * 退出下载 120 */ 121 122 public void exit(){ 123 // 如果有下载对象,退出下载 124 if(task != null) task.exit(); 125 } 126 127 /** 128 * 下载资源,声明下载执行者并开辟线程开始下载 129 * 此方法运行在主线程 130 */ 131 private void download(String path, File saveDir){ 132 // 实例化下载任务 133 task = new DownloadTask(path, saveDir); 134 // 开始下载 135 new Thread(task).start(); 136 } 137 138 /** 139 * 一定要在主线程更新 UI 控件的值,这样才能在界面上显示出来 140 * 不能再子线程更新 UI 控件的值 141 */ 142 private final class DownloadTask implements Runnable{ 143 private String path; 144 private File saveDir; 145 // 文件下载器(下载线程的容器) 146 private FileDownloader loader; 147 /** 148 * 构造方法,实现变量初始化 149 */ 150 public DownloadTask(String path, File saveDir){ 151 this.path = path; 152 this.saveDir = saveDir; 153 } 154 155 /** 156 * 退出下载 157 */ 158 public void exit(){ 159 // 如果下载器存在的话则退出下载 160 if(loader != null) loader.exit(); 161 } 162 163 // 开始下载,并设置下载的监听器 164 DownloadProgressListener downloadProgressListener = new DownloadProgressListener(){ 165 /** 166 * 下载的文件长度会不断地被传入该回调方法 167 */ 168 public void onDownloadSize(int size){ 169 Message msg = new Message(); 170 msg.what = PROCESSING; // 设置 id 为 1 171 msg.getData().putInt("size", size); 172 handler.sendMessage(msg); 173 } 174 }; 175 176 /** 177 * 下载线程的执行方法 178 */ 179 @Override 180 public void run() { 181 // TODO Auto-generated method stub 182 try{ 183 // 初始化下载 184 loader = new FileDownloader(getApplicationContext(),path,saveDir,3); 185 progressBar.setMax(loader.getFileSize()); 186 loader.download(downloadProgressListener); 187 }catch(Exception e){ 188 // 下载失败时向消息队列发送消息 189 e.printStackTrace(); 190 handler.sendMessage(handler.obtainMessage(FAILURE)); 191 192 } 193 194 } 195 196 } 197 } 198 199 }