欢迎使用CSDN-markdown编辑器

断点续传

实现文件的断点下载功能
为了实现文件的断点续传,会用到的知识:
- 简单的文件布局
- 子线程与Handle
- 广播
- 服务
- 数据库表操作
- 网络和文件传输

简单的文件布局

如图中所示,有两个按钮和ProgresBar(横向)

<ProgressBar
       android:id="@+id/progressBar"
       style="?android:attr/progressBarStyleHorizontal"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:max="100"
       />
    <Button
        android:id="@+id/stopButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="停止"
        android:layout_below="@+id/progressBar"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"

        android:layout_marginTop="38dp"
       />

    <Button
        android:id="@+id/downButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="下载"
        android:layout_alignBottom="@+id/stopButton"
        android:layout_toLeftOf="@+id/stopButton"
        android:layout_toStartOf="@+id/stopButton" />

在Activity中加载布局文件并实例化控件,为按钮添加点击事件

使用Intent 连接服务 ,设置传递的标志位fileInfo
,值为 private FileInfo fileInfo = new FileInfo(“http://dota2.dl.wanmei.com/dota2/client/wanmeidota3.2.1.apk
,0 ,”wanmeidota3.2.1.apk”,0,0);
FileInfo 是一个Modle类,构造方法

public FileInfo(String url, int id, String fileName, int length, int finished) {
        this.url = url;
        this.id = id;
        this.fileName = fileName;
        this.length = length;
        this.finished = finished;
    }

这个类主要

  @Override
    public void onClick(View view) {
           switch (view.getId()){
            case R.id.downButton:
                {
                Intent intent = new Intent(MainActivity.this, DownLoadService.class);
                intent.setAction(DownLoadService.ACTION_START);
                intent.putExtra("fileInfo", fileInfo);
                startService(intent);
                break;
                }
                case R.id.stopButton:
                {
                Intent intent = new Intent(MainActivity.this, DownLoadService.class);
                intent.setAction(DownLoadService.ACTION_STOP);
                intent.putExtra("fileInfo", fileInfo);
                startService(intent);
                break;
                }
            default:
                break;
        }
        }

DownLoadService 是一个服务类,首先就要到AndroidManifest.xml中注册

  <service android:name="org.ninebox.services.DownLoadService"/>

这里用到的startService 来启动的服务,考虑到多次暂停和启动服务,在onStartCommand来处理逻辑最为合适,当然也可以使用BindService的方式启动服务

  @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (ACTION_START.equals(intent.getAction()))
        {
            FileInfo  fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
            Log.i("TAG",fileInfo.toString());

            //初始化线程
            new InitThread(fileInfo).start();
        }else  if (ACTION_STOP.equals(intent.getAction()))
        {
            FileInfo  fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
            Log.i("TAG",fileInfo.toString());
           if (mTask!=null){
               mTask.isPause=true;
           }
        }
        return super.onStartCommand(intent, flags, startId);
    }

DownLoadService 定义的Static变量,用于记录Intent传递状态

    public  static final  String  ACTION_START= "ACTION_START";
    public  static final  String  ACTION_STOP ="ACTION_STOP";
    public  static final  String  ACTION_UPDATE ="ACTION_UPDATE";

最后初始化线程
new InitThread(fileInfo).start();
DownLoadService 内部定义了一个线程类:
Service 是运行在主线程中的 从网络下载数据,读取数据,或者处理一些耗时操作,应该在子线程中进行
这个子线程应该做些什么呢:我们知道了下载网址,文件名,但我们不知道文件的大小,首先我们就要获取这个大小 通过大小来设置本地文件的大小——–>最后就是下载了,下载有点复杂,所以单独写一个类来处理.
设置本地文件大小结束后 ———> Handler ———-> 其他类(需要新的子线程)
线程和service.activity交互需要使用handle

run()方法里考虑设置本地文件的大小需要哪些操作
1.打开网络
2.获得文件长度
3.创建本地文件
4.传递给Handler


1.打开网络

    RandomAccessFile mRFile=null;//随机访问文件
    HttpURLConnection con=null;  
    URL url  = new URL(mFileInfo.getUrl());
                con = (HttpURLConnection) url.openConnection();//打开网络连接
                con.setRequestMethod("GET");//下载都用get
                con.setConnectTimeout(3000);//设置响应延迟

mFileInfo 是在启动时传递进进来的参数,mFileInfo.length =0

  private  FileInfo mFileInfo = null;

        public InitThread( FileInfo mFileInfo) {
            this.mFileInfo = mFileInfo;
        }

2.获得文件长度

   int length = -1;  
                if (con.getResponseCode()== HttpsURLConnection.HTTP_OK)
                {
                    length = con.getContentLength();

                }else if(length<=0){
                    return; //结束子线程,这样处理不是很好,如果不存在这个网络文件,那么就会崩溃
                }

3.创建本地文件

  //这是获得SD卡的下的路径  /downloads/...
  public  static final  String FILE_PATH =Environment.          getExternalStorageDirectory().getAbsolutePath()+"/downloads/";
 File mDir = new File(FILE_PATH);//创建路径时候,因为使用的是模拟器,就没有SD卡,就是用当前程序缓存文件来存储下载文件
  if (!mDir.exists()){
       getCacheDir().mkdir();
        mDir= new File(getCacheDir().getPath());
    }
 File mFile = new File(mDir,mFileInfo.getFileName());
 mRFile = new RandomAccessFile(mFile,"rwd");//RandomAccessFile同时将FileInputStream和FileOutputStream整合到一起,而且支持将从文件任意字节处读或写数据~File类只是将文件作为整体来处理文件的,不能读写文件
mRFile.setLength(length);  //设置长度

4.传递给Handler

  public  static final  int  MSG_INIT =1;
  mFileInfo.setLength(length);             //线程和service.activity交互是使用handle
               mHander.obtainMessage(MSG_INIT,mFileInfo).sendToTarget();

Handler
handler中获得mFileInfo这个参数已经设置了长度,接下来就是下载文件了,前面已经说过使用其他类 来处理这个下载,
想一想 ,我们做的是断点续传的功能,下载到一半的时候暂停,下一次下载的时候要从暂停的位置继续下载,那么是不是要记录这次的暂停位置,也就是结束位置,还要随时记录完成下载进度,那么我们就可以定义一个Modle类 ,它包含id,url,start,end,finished

public class ThreadInfo {
    private  int id ;
    private String url;
    private int start;
    private int end;
    private int finished;

    public ThreadInfo(int id, String url, int start, int end, int finished) {
        this.id = id;
        this.url = url;
        this.start = start;
        this.end = end;
        this.finished = finished;
    }
    public ThreadInfo() {
       super();
    }

    @Override
    public String toString() {
        return "ThreadInfo{" +
                "id=" + id +
                ", url='" + url + '\'' +
                ", start=" + start +
                ", end=" + end +
                ", finished=" + finished +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public int getStart() {
        return start;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public int getEnd() {
        return end;
    }

    public void setEnd(int end) {
        this.end = end;
    }

    public int getFinished() {
        return finished;
    }

    public void setFinished(int finished) {
        this.finished = finished;
    }
}

接着就是 数据库,要向数据库插入这条数据,需要3个类

  • 继承SQLiteOpenHelper的类,用于创建数据库,创建表

  • 一个接口 定义了 增删改查功能

  • 接口的实现类

创建数据库,创建表

public class DBHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "download.db";
    private static final int VERSION =1;
    private static final String CREATE_SQL="create table thread_info( id integer primary key autoincrement," +
            "thread_id integer,url text,start integer,end integer,finished integer);";
    private static final String DROP_SQL="drop table if exists thread_info";

    public DBHelper(Context context) {
        super(context, DB_NAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_SQL);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {
      db.execSQL(DROP_SQL);
      db.execSQL(CREATE_SQL);
    }
}

接口

public interface ThreadDAO {

    //插入线程信息
    public  void insertThread(ThreadInfo threadInfo);
    /**
     * 删除线程的方法
     * 考虑多次下载同一个url的情况,需要传递一个线程id来判断具体删除哪一个
     * */
    public  void deleteThread(String url,int thread_id);

    /**
     *
     * @param url
     * @param thread_id  更新线程进度
     * @param finished
     */
    public  void updateThread(String url,int thread_id,int finished);

    /**
     *
     * @param url       查询线程信息
     * @return
     */
    public List<ThreadInfo> getThreads(String url);


    /**
     *
     * @param url
     * @param thread_id 查询线程是否存在
     * @return
     */
    public boolean isRxists(String url,int thread_id);
}

接口实现类

public class ThreadDAOImpl implements  ThreadDAO {

    private  DBHelper mDBHelper=null;

    public ThreadDAOImpl(Context context) {
        mDBHelper = new DBHelper(context);
    }

    @Override
    public void insertThread(ThreadInfo threadInfo) {
        SQLiteDatabase db = mDBHelper.getWritableDatabase();
        db.execSQL("insert into thread_info(thread_id,url,start,end,finished) values(?,?,?,?,?)",
                new Object[]{threadInfo.getId(),threadInfo.getUrl(),threadInfo.getStart(),threadInfo.getEnd(),threadInfo.getFinished()});
        db.close();
    }

    @Override
    public void deleteThread(String url, int thread_id) {
        SQLiteDatabase db = mDBHelper.getWritableDatabase();
        db.execSQL("delete from thread_info where url = ? and thread_id= ?",
                new Object[]{url,thread_id});
        db.close();
    }

    @Override
    public void updateThread(String url, int thread_id, int finished) {
        SQLiteDatabase db = mDBHelper.getWritableDatabase();
        db.execSQL("update thread_info set finished= ? where url = ? and thread_id= ?",
                new Object[]{finished,url,thread_id});
        db.close();
    }

    @Override
    public List<ThreadInfo> getThreads(String url) {
        SQLiteDatabase db = mDBHelper.getWritableDatabase();
        List<ThreadInfo> list = new ArrayList<ThreadInfo>();
        Cursor cursor = db.rawQuery("select * from thread_info where url=?",new String[]{url});
        while (cursor.moveToNext()){
            ThreadInfo thread = new ThreadInfo();
            thread.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            thread.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            thread.setStart(cursor.getInt(cursor.getColumnIndex("start")));
            thread.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
            thread.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
            list.add(thread);
        }
        cursor.close();
        db.close();
        return list;
    }

    @Override
    public boolean isRxists(String url, int thread_id) {
        SQLiteDatabase db = mDBHelper.getWritableDatabase();
        List<ThreadInfo> list = new ArrayList<ThreadInfo>();
        Cursor cursor = db.rawQuery("select * from thread_info where url=? and thread_id = ?",new String[]{url,thread_id+""});
        boolean exists = cursor.moveToNext();
        db.close();
        cursor.close();
        return exists;
    }
}

接着我们就可以在Handler中启动下载类了
定义一个DownloadTask因为是重新编写的工具类,在发送广播,写入文件,还有数据库操作的时候,这类就可能会用到service的上下文对象,需要下载肯定就需要FileInfo的信息

 public DownloadTask(FileInfo nFileInfo, Context mContext) {
        this.nFileInfo = nFileInfo;
        this.mContext = mContext;
        mThreadDAO =new ThreadDAOImpl(mContext);

     //   new DownloadThread()
    }

定义一个download()
下载,这些数据库的操作handle中不太好

 public void download(){
        ThreadInfo threadInfo = null;
        //读取数据库线程信息,可能存在多条下载线程信息
List<ThreadInfo> threadInfos=mThreadDAO.getThreads(nFileInfo.getUrl());

 if (threadInfos.size() ==0){
    //初始化线程信息
   threadInfo  =  
   new  ThreadInfo(0,nFileInfo.getUrl(),0,nFileInfo.getLength(),0);
        }else {
            threadInfo = threadInfos.get(0);//因为这里是单线程,只取第一条就可以了,多线程的话情况会稍微复杂一点
        }
        //创建子线程进行下载
        new DownloadThread(threadInfo).start();
    }

DownloadThreadDownloadTaskd的一个内部线程类
这个线程的run()方法需要做什么呢?构造方法怎么定义呢?

构造方法
启动这个线程需要DownloadTaskd查询得到的数据,所以就有ThreadInfo的形参,还需要一个私有变量来接受这个形参

run()
当然是下载啦,下载需要几部呢

  • 向数据库里插入线程信息(如果数据库不存在这条线程信息的情况下)
  • 设置下载位置
  • 设置文件写入位置
  • 开始下载

向数据库里插入线程信息

 if (!mThreadDAO.isRxists(mThreadInfo.getUrl(),mThreadInfo.getId())){
                mThreadDAO.insertThread(mThreadInfo);
            }

设置下载位置

      HttpURLConnection conn = null;
      RandomAccessFile raf=null;
      InputStream input=null;

    URL url = new URL(mThreadInfo.getUrl());
                conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(3000);
                int start = mThreadInfo.getStart()+mThreadInfo.getFinished();//Range 范围的意思,用于部分下载,可以设置开始的字节数到结束的字节数
                conn.setRequestProperty("Range","bytes="+start+"-"+mThreadInfo.getEnd());

设置文件写入位置

      //设置文件写入位置
                File  mFile = new File(mContext.getCacheDir().getPath(),nFileInfo.getFileName());
                 raf = new RandomAccessFile(mFile,"rwd");
                raf.seek(start); //seek
                Intent  intent = new Intent(DownLoadService.ACTION_UPDATE);
                mFinished +=mThreadInfo.getFinished();
 private  int mFinished=0;

开始下载

     if (conn.getResponseCode() ==HttpURLConnection.HTTP_PARTIAL){
                    //读取数据
                    input=conn.getInputStream();
                    byte[] buffer = new byte[1024*4];
                    int len = -1;
                    long time = System.currentTimeMillis();
                    while((len=input.read(buffer))!= -1){
                        //写入文件
                        raf.write(buffer,0,len);
                        //把下载的进度广播给Activity
                        mFinished +=len;

                        if (System.currentTimeMillis()-time>500) {
                            Log.i("TAG","下载中:---------------------------------------"+mFinished+"");
                            Log.i("TAG","总大小:---------------------------------------"+ nFileInfo.getLength()+"");
                            Log.i("TAG","堕胎:---------------------------------------"+   mFinished * 100 / nFileInfo.getLength()+"");
                            time =System.currentTimeMillis();
                            intent.putExtra("finished", mFinished * 100 / nFileInfo.getLength());
                            mContext.sendBroadcast(intent);
                        }
                        //在暂停时 保存下载进度
                        if (isPause){
                            Log.i("TAG","暂停时:----------------------"+mThreadInfo.getFinished()+"---------------");
                            mThreadDAO.updateThread(mThreadInfo.getUrl(),mThreadInfo.getId(),mFinished);
                            Log.i("TAG","暂停后:----------------------"+mThreadInfo.getFinished()+"---------------");
                            return;
                        }
                    }
                    //删除线程ID
                    mThreadDAO.deleteThread(mThreadInfo.getUrl(),mThreadInfo.getId());


                }

最后就是关闭这些连接,在这里我们发送了一个广播,最后我们要在Activity中接收这个广播和注册这个广播
接收广播

    BroadcastReceiver mReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {

            switch (intent.getAction()){
                case DownLoadService.ACTION_UPDATE:
                    int finished= intent.getIntExtra("finished",0);
                Log.i("TAG","----------------------"+finished+"---------------");
                mProgresBar.setProgress(finished);

                break;
            }
        }
    };

Oncreate注册广播

  //注册广播接收器
        IntentFilter filter = new IntentFilter();
        filter.addAction(DownLoadService.ACTION_UPDATE);
        registerReceiver(mReceiver,filter);

销毁广播

  @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mReceiver);
    }

暂停按钮

在service中只需要将mTask.isPause=true

else  if (ACTION_STOP.equals(intent.getAction()))
        {
            FileInfo  fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
            Log.i("TAG",fileInfo.toString());
           if (mTask!=null){
               mTask.isPause=true;
           }
        }

因为在Task这个类中,设置为公共的

  public  boolean isPause=false;

在上面的开始下载可以看到一旦这个参数为true就会结束循环

代码下载地址

ps:这个程序有个小问题,下载进度条一旦超过50%,广播传递的进度参数finished就会逐渐减小…….50,49,47……3…1,但实际又是下载成功了的,这点一直搞不定。 本文下载的目标是完美世界dota2.app

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值