Android 多线程断点续传下载

前面我们一步一步实现了多线程下载,本章节我们一步一步实现多线程断点续传下载

先来看看最后的效果图


  1. 创建一个 空的 Android 项目 cn.twle.android.KeepOnDownload

  2. 修改 AndroidManifest.xml 添加相关权限

    <!-- 访问 internet 权限 -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <!-- 往 SDCard 写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
  3. 多线程断点续传肯定要保存每个分块的下载进度,所以需要使用 SQLite 来保存数据,于是我们创建一个 SQLite 帮助类 DBOpenHelper.java

    表结构如下

    字段 说明
    id id
    downpath 当前下载的资源
    threadid 下载的线程 id
    downlength 线程下载的最后位置

    DBOpenHelper.java

    package cn.twle.android.keepondownload;
    
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    
    public class DBOpenHelper extends SQLiteOpenHelper {
      public DBOpenHelper(Context context) {
        super(context, "ms_download.db", null, 1);
      }
      @Override
      public void onCreate(SQLiteDatabase db) {
    
        db.execSQL("CREATE TABLE IF NOT EXISTS download_log " +
            "(id integer primary key autoincrement," +
            " downpath varchar(100)," +
            " threadid INTEGER, downlength INTEGER)");
      }
    
      @Override
      public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    
    }
    
  4. 为了方便查询数据库,我们再创建一个数据库操作类 DownloadBean.java

    1. 根据 URL 获得每条线程当前下载长度的方法

    2. 线程新开辟后,需要往数据库中插入与该线程相关参数的方法

    3. 定义一个可以实时更新下载文件长度的方法

    4. 线程下载完,还需要根据线程 id,删除对应记录的方法

    DownloadBean.java

    package cn.twle.android.keepondownload;
    
    import java.util.HashMap;
    import java.util.Map;
    import android.content.Context;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    
    public class DownloadBean {
    
      //声明数据库管理器
      private DBOpenHelper openHelper;
    
      //在构造方法中根据上下文对象实例化数据库管理器
      public DownloadBean(Context context) {
        openHelper = new DBOpenHelper(context);
      }
    
      /**
       * 获得指定 URI 的每条线程已经下载的文件长度
       * @param path
       * @return 
       * */
      public Map<Integer, Integer> getData(String path)
      {
        //获得可读数据库句柄,通常内部实现返回的其实都是可写的数据库句柄
        SQLiteDatabase db = openHelper.getReadableDatabase();
        //根据下载的路径查询所有现场的下载数据,返回的Cursor指向第一条记录之前
        Cursor cursor = db.rawQuery("select threadid, downlength from download_log where downpath=?",
            new String[]{path});
        //建立一个哈希表用于存放每条线程已下载的文件长度
        Map<Integer,Integer> data = new HashMap<Integer, Integer>();
        //从第一条记录开始遍历Cursor对象
        cursor.moveToFirst();
        while(cursor.moveToNext())
        {
          //把线程id与该线程已下载的长度存放到data哈希表中
          data.put(cursor.getInt(0), cursor.getInt(1));
          data.put(cursor.getInt(cursor.getColumnIndexOrThrow("threadid")),
              cursor.getInt(cursor.getColumnIndexOrThrow("downlength")));
        }
        cursor.close();//关闭cursor,释放资源;
        db.close();
        return data;
      }
    
      /**
       * 保存每条线程已经下载的文件长度
       * @param path 下载的路径
       * @param map 现在的di和已经下载的长度的集合
      */
      public void save(String path,Map<Integer,Integer> map)
      {
        SQLiteDatabase db = openHelper.getWritableDatabase();
        //开启事务,因为此处需要插入多条数据
        db.beginTransaction();
        try{
          //使用增强for循环遍历数据集合
          for(Map.Entry<Integer, Integer> entry : map.entrySet())
          {
            //插入特定下载路径特定线程ID已经下载的数据
            db.execSQL("insert into download_log(downpath, threadid, downlength) values(?,?,?)",
                new Object[]{path, entry.getKey(), entry.getValue()});
          }
          //设置一个事务成功的标志,如果成功就提交事务,如果没调用该方法的话那么事务回滚
          //就是上面的数据库操作撤销
          db.setTransactionSuccessful();
        }finally{
          //结束一个事务
          db.endTransaction();
        }
        db.close();
      }
    
      /**
       * 实时更新每条线程已经下载的文件长度
       * @param path
       */
      public void update(String path,int threadId,int pos)
      {
        SQLiteDatabase db = openHelper.getWritableDatabase();
        //更新特定下载路径下特定线程已下载的文件长度
        db.execSQL("update download_log set downlength=? where downpath=? and threadid=?",
            new Object[]{pos, path, threadId});
        db.close();
      }
    
      /**
       *当文件下载完成后,删除对应的下载记录
       *@param path 
       */
      public void delete(String path)
      {
        SQLiteDatabase db = openHelper.getWritableDatabase();
        db.execSQL("delete from download_log where downpath=?", new Object[]{path});
        db.close();
      }
    
    }
    
  5. 自定义一个下载线程类 DownloadThread

    1. 继承 Thread 类重写 run() 方法

    2. run() 方法中先判断是否下载完成,如果没有则打开 URLConnection 链接,接着使用 RandomAccessFile 进行数据读写, 完成时设置完成标记为 true,发生异常则设置长度为 -1 ,打印异常信息

    DownloadThread.java

    package cn.twle.android.keepondownload;
    
    import java.io.File;
    import java.io.InputStream;
    import java.io.RandomAccessFile;
    import java.net.HttpURLConnection;
    import java.net.URL;
    
    import android.util.Log;
    
    public class DownloadThread extends Thread {
    
        //定义TAG,在打印log时进行标记
        private static final String TAG = "下载线程类";    
        private File saveFile;              //下载的数据保存到的文件
        private URL downUrl;              //下载的URL
        private int block;                //每条线程下载的大小
        private int threadId = -1;            //初始化线程id设置
        private int downLength;             //该线程已下载的数据长度
        private boolean finish = false;         //该线程是否完成下载的标志
        private DownloadHelper downloader;      //文件下载器
    
        public DownloadThread(DownloadHelper downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) {
        this.downUrl = downUrl;
        this.saveFile = saveFile;
        this.block = block;
        this.downloader = downloader;
        this.threadId = threadId;
        this.downLength = downLength;
        }
    
        @Override
        public void run() {
    
        //未下载完成
        if( downLength < block )
        {
    
          try {
                HttpURLConnection http = (HttpURLConnection) downUrl.openConnection();
    
            http.setConnectTimeout(5 * 1000);
            http.setRequestMethod("GET");
            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, */*");
    
            http.setRequestProperty("Accept-Language", "zh-CN");
            http.setRequestProperty("Referer", downUrl.toString()); 
            http.setRequestProperty("Charset", "UTF-8");
    
            int startPos = block * (threadId - 1) + downLength;//开始位置
            int endPos = block * threadId -1;//结束位置
    
            http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//设置获取实体数据的范围
    
            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)");
    
            http.setRequestProperty("Connection", "Keep-Alive");
    
            InputStream inStream = http.getInputStream();     //获得远程连接的输入流
            byte[] buffer = new byte[1024];           //设置本地数据的缓存大小为1MB
            int offset = 0;                   //每次读取的数据量
            print("Thread " + this.threadId + " start download from position "+ startPos);  //打印该线程开始下载的位置
    
            RandomAccessFile threadfile = new RandomAccessFile(this.saveFile, "rwd");
    
            threadfile.seek(startPos);
            //用户没有要求停止下载,同时没有达到请求数据的末尾时会一直循环读取数据
            while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) {
              threadfile.write(buffer, 0, offset);          //直接把数据写入到文件中
              downLength += offset;             //把新线程已经写到文件中的数据加入到下载长度中
              downloader.update(this.threadId, downLength); //把该线程已经下载的数据长度更新到数据库和内存哈希表中
              downloader.append(offset);            //把新下载的数据长度加入到已经下载的数据总长度中
            }
            threadfile.close();
            inStream.close();
            print("Thread " + this.threadId + " download finish");
            this.finish = true;                               //设置完成标记为true,无论下载完成还是用户主动中断下载
    
          } catch (Exception e) {
    
                this.downLength = -1;               //设置该线程已经下载的长度为-1
                print("Thread "+ this.threadId+ ":"+ e);
                }
            }
        }
    
        private static void print(String msg){
            Log.i(TAG, msg);
        }
    
        /**
        * 下载是否完成
        * @return
        */
        public boolean isFinish() {
            return finish;
        }
    
        /**
        * 已经下载的内容大小
        * @return 如果返回值为-1,代表下载失败
        */
        public long getDownLength() {
            return downLength;
        }
    }
    
  6. 创建一个文件下载帮助类,主要包括以下内容

    1. 定义一堆变量,核心是线程池 threads 和同步集合 ConcurrentHashMap 用于缓存线程下载长度的

    2. 定义一个获取线程池中线程数的方法

    3. 定义一个退出下载的方法

    4. 获取当前文件大小的方法

    5. 累计当前已下载长度的方法,这里需要添加一个 synchronized 关键字,用来解决并发访问的问题

    6. 更新指定线程最后的下载位置,同样也需要用同步

    7. 在构造方法中完成文件下载,线程开辟等操作

    8. 获取文件名的方法:先截取提供的url最后的'/'后面的字符串,如果获取不到,再从头字段查找,还是找不到的话,就使用网卡标识数字+cpu的唯一数字生成一个16个字节的二进制作为文件名

    9. 开始下载文件的方法

    10. 获取 http 响应头字段的方法

    11. 打印 HTTP 头字段的方法

    12. 打印日志信息的方法

    DownloadHelper.java

    package cn.twle.android.keepondownload;
    
    import java.io.File;
    import java.io.RandomAccessFile;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.UUID;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    import android.content.Context;
    import android.util.Log;
    
    public class DownloadHelper {
    
      private static final String TAG = "文件下载类";  //设置一个查log时的一个标志
      private static final int RESPONSEOK = 200;    //设置响应码为200,代表访问成功
      private DownloadBean downloadBean;        //获取本地数据库的业务Bean
      private boolean exited;             //停止下载的标志
      private Context context;            //程序的上下文对象
      private int downloadedSize = 0;               //已下载的文件长度
      private int fileSize = 0;           //开始的文件长度
      private DownloadThread[] threads;        //根据线程数设置下载的线程池
      private File saveFile;              //数据保存到本地的文件中
      private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();  //缓存个条线程的下载的长度
      private int block;                            //每条线程下载的长度
      private String downloadUrl;                   //下载的路径
    
      /**
       * 获取线程数
       */
      public int getThreadSize()
      {
        //return threads.length;
        return 0;
      }
    
      /**
       * 退出下载
       * */
      public void exit()
      {
        this.exited = true;    //将退出的标志设置为true;
      }
      public boolean getExited()
      {
        return this.exited;
      }
    
      /**
       * 获取文件的大小
       * */
      public int getFileSize()
      {
        return fileSize;
      }
    
      /**
       * 累计已下载的大小
       * 使用同步锁来解决并发的访问问题
       * */
      protected synchronized void append(int size)
      {
        //把实时下载的长度加入到总的下载长度中
        downloadedSize += size;
      }
    
      /**
       * 更新指定线程最后下载的位置
       * @param threadId 线程id
       * @param pos 最后下载的位置
       * */
      protected synchronized void update(int threadId,int pos)
      {
        //把指定线程id的线程赋予最新的下载长度,以前的值会被覆盖掉
        this.data.put(threadId, pos);
        //更新数据库中制定线程的下载长度
        this.downloadBean.update(this.downloadUrl, threadId, pos);
      }
    
      /**
       * 构建文件下载器
       * @param downloadUrl 下载路径
       * @param fileSaveDir 文件的保存目录
       * @param threadNum  下载线程数
       * @return 
       */
      public DownloadHelper(Context context,String downloadUrl,File fileSaveDir,int threadNum)
      {
        try {
          this.context = context;     //获取上下文对象,赋值
          this.downloadUrl = downloadUrl;  //为下载路径赋值
    
          //实例化数据库操作的业务Bean类,需要传一个context值
          downloadBean = new DownloadBean(this.context);
    
          //根据下载路径实例化URL 
          URL url = new URL(this.downloadUrl);
    
          //如果文件不存在的话指定目录,这里可创建多层目录   
          if(!fileSaveDir.exists()) fileSaveDir.mkdir();
    
          //根据下载的线程数量创建下载的线程池  
          this.threads = new DownloadThread[threadNum];
    
          HttpURLConnection conn = (HttpURLConnection) url.openConnection();   //创建远程连接句柄,这里并未真正连接
          conn.setConnectTimeout(5000);      //设置连接超时事件为5
          conn.setRequestMethod("GET");      //设置请求方式为GET
          //设置用户端可以接收的媒体类型
          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, */*");
    
          conn.setRequestProperty("Accept-Language", "zh-CN");  //设置用户语言
          conn.setRequestProperty("Referer", downloadUrl);    //设置请求的来源页面,便于服务端进行来源统计
          conn.setRequestProperty("Charset", "UTF-8");    //设置客户端编码
          //设置用户代理
          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)");
    
          conn.setRequestProperty("Connection", "Keep-Alive");  //设置connection的方式
          conn.connect();      //和远程资源建立正在的链接,但尚无返回的数据流
          printResponseHeader(conn);   //打印返回的Http的头字段集合
          //对返回的状态码进行判断,用于检查是否请求成功,返回200时执行下面的代码
          if(conn.getResponseCode() == RESPONSEOK)
          {
            this.fileSize = conn.getContentLength();  //根据响应获得文件大小
            if(this.fileSize <= 0)throw new RuntimeException("不知道文件大小");  //文件长度小于等于0时抛出运行时异常
            String filename = getFileName(conn);      //获取文件名称
            this.saveFile = new File(fileSaveDir,filename);  //根据文件保存目录和文件名保存文件
            Map<Integer,Integer> logdata = downloadBean.getData(downloadUrl);    //获取下载记录
            //如果存在下载记录
            if(logdata.size() > 0)
            {
              //遍历集合中的数据,把每条线程已下载的数据长度放入data中
              for(Map.Entry<Integer, Integer> entry : logdata.entrySet())
              {
                data.put(entry.getKey(), entry.getValue());
              }
            }
            //如果已下载的数据的线程数和现在设置的线程数相同时则计算所有现场已经下载的数据总长度
            if(this.data.size() == this.threads.length)
            {
              //遍历每条线程已下载的数据
              for(int i = 0;i < this.threads.length;i++)
              {
                this.downloadedSize += this.data.get(i+1);
              }
              print("已下载的长度" + this.downloadedSize + "个字节");
            }
            //使用条件运算符求出每个线程需要下载的数据长度
            this.block = (this.fileSize % this.threads.length) == 0?
                this.fileSize / this.threads.length:
                  this.fileSize / this.threads.length + 1;
          }else{
            //打印错误信息
            print("服务器响应错误:" + conn.getResponseCode() + conn.getResponseMessage());
            throw new RuntimeException("服务器反馈出错");
          }
    
        }catch (Exception e) 
        {
          print(e.toString());   //打印错误
          throw new RuntimeException("无法连接URL");
        }
      }
    
      /**
       * 获取文件名
       * */
      private String getFileName(HttpURLConnection conn)
      {
        //从下载的路径的字符串中获取文件的名称
        String filename = this.downloadUrl.substring(this.downloadUrl.lastIndexOf('/') + 1);
        if(filename == null || "".equals(filename.trim())){     //如果获取不到文件名称
        for(int i = 0;;i++)  //使用无限循环遍历
        {
          String mine = conn.getHeaderField(i);     //从返回的流中获取特定索引的头字段的值
          if (mine == null) break;          //如果遍历到了返回头末尾则退出循环
          //获取content-disposition返回字段,里面可能包含文件名
          if("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())){
            //使用正则表达式查询文件名
            Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());
            if(m.find()) return m.group(1);    //如果有符合正则表达式规则的字符串,返回
          }
        }
        filename = UUID.randomUUID()+ ".tmp";//如果都没找到的话,默认取一个文件名
        //由网卡标识数字(每个网卡都有唯一的标识号)以及CPU时间的唯一数字生成的一个16字节的二进制作为文件名
        }
          return filename;
      }
    
      /**
       *  开始下载文件
       * @param listener 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null
       * @return 已下载文件大小
       * @throws Exception
       */
      //进行下载,如果有异常的话,抛出异常给调用者
      public int download(DownloadProgressListener listener) throws Exception{
        try {
          RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rwd");
          //设置文件大小
          if(this.fileSize>0) randOut.setLength(this.fileSize);
          randOut.close();    //关闭该文件,使设置生效
          URL url = new URL(this.downloadUrl);
          if(this.data.size() != this.threads.length){
          //如果原先未曾下载或者原先的下载线程数与现在的线程数不一致
            this.data.clear();
            //遍历线程池
            for (int i = 0; i < this.threads.length; i++) {
              this.data.put(i+1, 0);//初始化每条线程已经下载的数据长度为0
            }
            this.downloadedSize = 0;   //设置已经下载的长度为0
          }
    
          for (int i = 0; i < this.threads.length; i++) {//开启线程进行下载
            int downLength = this.data.get(i+1);   
            //通过特定的线程id获取该线程已经下载的数据长度
            //判断线程是否已经完成下载,否则继续下载 
            if(downLength < this.block && this.downloadedSize<this.fileSize){
              //初始化特定id的线程
              this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);
              //设置线程优先级,Thread.NORM_PRIORITY = 5;
              //Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,数值越大优先级越高
              this.threads[i].setPriority(7);   
              this.threads[i].start();    //启动线程
            }else{
              this.threads[i] = null;   //表明线程已完成下载任务
            }
          }
    
          downloadBean.delete(this.downloadUrl);           
          //如果存在下载记录,删除它们,然后重新添加
          downloadBean.save(this.downloadUrl, this.data);  
          //把下载的实时数据写入数据库中
          boolean notFinish = true;             
          //下载未完成
          while (notFinish) {               
          // 循环判断所有线程是否完成下载
            Thread.sleep(900);
            notFinish = false;                
            //假定全部线程下载完成
            for (int i = 0; i < this.threads.length; i++){
              if (this.threads[i] != null && !this.threads[i].isFinish()) {
              //如果发现线程未完成下载
                notFinish = true;                   
                //设置标志为下载没有完成
                if(this.threads[i].getDownLength() == -1){        
                //如果下载失败,再重新在已下载的数据长度的基础上下载
                //重新开辟下载线程,设置线程的优先级
                  this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);
                  this.threads[i].setPriority(7);
                  this.threads[i].start();
                }
              }
            }       
            if(listener!=null) listener.onDownloadSize(this.downloadedSize);
            //通知目前已经下载完成的数据长度
          }
          if(downloadedSize == this.fileSize) downloadBean.delete(this.downloadUrl);
          //下载完成删除记录
        } catch (Exception e) {
          print(e.toString());
          throw new Exception("文件下载异常");
        }
        return this.downloadedSize;
      }
    
      /**
       * 获取Http响应头字段
       * @param http
       * @return
       */
      public static Map<String, String> getHttpResponseHeader(HttpURLConnection http) {
        //使用LinkedHashMap保证写入和便利的时候的顺序相同,而且允许空值
        Map<String, String> header = new LinkedHashMap<String, String>();
        //此处使用无线循环,因为不知道头字段的数量
        for (int i = 0;; i++) {
          String mine = http.getHeaderField(i);  //获取第i个头字段的值
          if (mine == null) break;      //没值说明头字段已经循环完毕了,使用break跳出循环
          header.put(http.getHeaderFieldKey(i), mine); //获得第i个头字段的键
        }
        return header;
      }
      /**
       * 打印Http头字段
       * @param http
       */
      public static void printResponseHeader(HttpURLConnection http){
        //获取http响应的头字段
        Map<String, String> header = getHttpResponseHeader(http);
        //使用增强for循环遍历取得头字段的值,此时遍历的循环顺序与输入树勋相同
        for(Map.Entry<String, String> entry : header.entrySet()){
          //当有键的时候则获取值,如果没有则为空字符串
          String key = entry.getKey()!=null ? entry.getKey()+ ":" : "";
          print(key+ entry.getValue());      //打印键和值得组合
        }
      }
    
      /**
       * 打印信息
       * @param msg 信息字符串
       * */
      private static void print(String msg) {
        Log.i(TAG, msg);
      }
    }
    
  7. MainActivity.java 同一个目录下创建一个 DownloadProgressListener 接口监听下载进度

    因为 FileDownloader 中使用了 DownloadProgressListener 进行进度监听,所以这里需要创建一个接口,同时定义一个方法的空实现

    package cn.twle.android.keepondownload;
    public interface DownloadProgressListener {
      public void onDownloadSize(int downloadedSize);
    }
    
  8. 修改 activity_main.xml 创建布局

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="请输入要下载的文件地址" />
    
        <EditText 
            android:id="@+id/editpath"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="https://www.twle.cn/static/download/python.chm"    
        />
    
        <Button 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btndown"
            android:text="下载" />
    
        <Button 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnstop"
            android:text="停止"
            android:enabled="false"    
        />
    
        <ProgressBar
            android:layout_width="fill_parent" 
            android:layout_height="18dp" 
            style="?android:attr/progressBarStyleHorizontal"
            android:id="@+id/progressBar"
        />
    
       <TextView  
            android:layout_width="fill_parent" 
            android:layout_height="wrap_content" 
            android:gravity="center"
            android:id="@+id/textresult"
            android:text="显示实时下载的百分比" />
    
    </LinearLayout>
    
  9. 修改 MainActivity.java

    完成组件以及相关变量的初始化,使用 handler 来完成界面的更新操作, 另外耗时操作不能够在主线程中进行, 所以这里需要开辟新的线程,这里用 Runnable 实现

    package cn.twle.android.keepondownload;
    
    import java.io.File;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.os.Environment;
    import android.os.Handler;
    import android.os.Message;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.ProgressBar;
    import android.widget.TextView;
    import android.widget.Toast;
    
    public class MainActivity extends Activity {
    
      private EditText editpath;
      private Button btndown;
      private Button btnstop;
      private TextView textresult;
      private ProgressBar progressbar;
      private static final int PROCESSING = 1;   //正在下载实时数据传输Message标志
      private static final int FAILURE = -1;     //下载失败时的Message标志
    
      private Handler handler = new UIHander();
    
        private final class UIHander extends Handler{
        public void handleMessage(Message msg) {
          switch (msg.what) {
          //下载时
          case PROCESSING:
            int size = msg.getData().getInt("size");     //从消息中获取已经下载的数据长度
            progressbar.setProgress(size);         //设置进度条的进度
            //计算已经下载的百分比,此处需要转换为浮点数计算
            float num = (float)progressbar.getProgress() / (float)progressbar.getMax();
            int result = (int)(num * 100);     //把获取的浮点数计算结果转换为整数
            textresult.setText(result+ "%");   //把下载的百分比显示到界面控件上
            if(progressbar.getProgress() == progressbar.getMax()){ //下载完成时提示
              Toast.makeText(getApplicationContext(), "文件下载成功", Toast.LENGTH_LONG).show();
            }
            break;
    
          case FAILURE:    //下载失败时提示
            Toast.makeText(getApplicationContext(), "文件下载失败", Toast.LENGTH_LONG).show();
            break;
          }
        }
        }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        editpath = (EditText) findViewById(R.id.editpath);
        btndown = (Button) findViewById(R.id.btndown);
        btnstop = (Button) findViewById(R.id.btnstop);
        textresult = (TextView) findViewById(R.id.textresult);
        progressbar = (ProgressBar) findViewById(R.id.progressBar);
        ButtonClickListener listener = new ButtonClickListener();
        btndown.setOnClickListener(listener);
        btnstop.setOnClickListener(listener);
    
      }
    
      private final class ButtonClickListener implements View.OnClickListener{
        public void onClick(View v) {
          switch (v.getId()) {
          case R.id.btndown:
            String path = editpath.getText().toString();
            if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
              File saveDir = Environment.getExternalStorageDirectory();
              download(path, saveDir);
            }else{
              Toast.makeText(getApplicationContext(), "sd卡读取失败", Toast.LENGTH_LONG).show();
            }
            btndown.setEnabled(false);
            btnstop.setEnabled(true);
            break;
    
          case R.id.btnstop:
            exit();
            btndown.setEnabled(true);
            btnstop.setEnabled(false);
            break;
          }
        }
        /*
        由于用户的输入事件(点击button, 触摸屏幕....)是由主线程负责处理的,如果主线程处于工作状态,
        此时用户产生的输入事件如果没能在5秒内得到处理,系统就会报“应用无响应”错误。
        所以在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件,
        导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。
        */
        private DownloadTask task;
        /**
         * 退出下载
         */
        public void exit(){
          if(task!=null) task.exit();
        }
        private void download(String path, File saveDir) {//运行在主线程
          task = new DownloadTask(path, saveDir);
          new Thread(task).start();
        }
    
        /*
         * UI控件画面的重绘(更新)是由主线程负责处理的,如果在子线程中更新UI控件的值,更新后的值不会重绘到屏幕上
         * 一定要在主线程里更新UI控件的值,这样才能在屏幕上显示出来,不能在子线程中更新UI控件的值
         */
        private final class DownloadTask implements Runnable{
          private String path;
          private File saveDir;
          private DownloadHelper loader;
          public DownloadTask(String path, File saveDir) {
            this.path = path;
            this.saveDir = saveDir;
          }
          /**
           * 退出下载
           */
          public void exit(){
            if(loader!=null) loader.exit();
          }
    
          public void run() {
            try {
              loader = new DownloadHelper(getApplicationContext(), path, saveDir, 3);
              progressbar.setMax(loader.getFileSize());//设置进度条的最大刻度
              loader.download(new DownloadProgressListener() {
                public void onDownloadSize(int size) {
                  Message msg = new Message();
                  msg.what = PROCESSING;
                  msg.getData().putInt("size", size);
                  handler.sendMessage(msg);
                }
              });
            } catch (Exception e) {
              e.printStackTrace();
              handler.sendMessage(handler.obtainMessage(FAILURE));
            }
          }     
        }
        }
    }
    

Android 基础教程

关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.