课程设计做的是一个类似网盘的东西,用的是新浪微盘的API接口进行存储(百度的PCS已关闭好不爽。。),里面有一个功能就用到了典型的断点续传了,这里那这个东西做一个小练习,后面再把这个技术加到项目里来。

做这个东西大概用的几个技术知识点,大概就是:

  1. Activity之间Intent的传输。
  2. Service的使用。
  3. 线程的使用,包括handler的消息的处理。
  4. 网络通信的操作。
  5. 数据库数据的存储。
  6. 广播技术的使用。

整个功能的操作流程如下图所示:


首先通过Activity将下载文件的信息(主要是文件的地址和文件名)传送给service,service接收到intent,开启一个子线程去获取文件url的具体信息(文件的长度等等),然后再发送Message消息给service的handler,handler再调用下载类进行下载。在下载过程中也是通过开启子线程的方式去下载文件(这里注意:一切的网络通信都必须开启线程去操作),下载的进度通过广播的方式放送给activity,activity接受广播更新进度条的进度。如果单击停止下载,会将下载的进度信息存储在数据库,下载继续下载在从数据库获取下载进度,然后继续下载。

文字说的有点绕,原谅我不擅长表达,这里直接贴出代码吧~

首先是文件的bean


package com.linxiaosheng.test3;

import java.io.Serializable;

public class FileInfo implements Serializable{
	private int id;
	private String url;
	private String fileName;
	private int length;
	private int finished;
	public FileInfo() {
		super();
	}
	public FileInfo(int id, String url, String fileName, int length,
			int finished) {
		super();
		this.id = id;
		this.url = url;
		this.fileName = fileName;
		this.length = length;
		this.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 String getFileName() {
		return fileName;
	}
	public void setFileName(String fileName) {
		this.fileName = fileName;
	}
	public int getLength() {
		return length;
	}
	public void setLength(int length) {
		this.length = length;
	}
	public int getFinished() {
		return finished;
	}
	public void setFinished(int finished) {
		this.finished = finished;
	}
	@Override
	public String toString() {
		return "FileInfo [id=" + id + ", url=" + url + ", fileName=" + fileName
				+ ", length=" + length + ", finished=" + finished + "]";
	}
	
}


进度信息的bean


package com.linxiaosheng.test3;

public class ThreadInfo {
	private int id;
	private String url;
	private int start;
	private int end;
	private int finished;
	public ThreadInfo() {
		super();
	}
	public ThreadInfo(int id, String url, int start, int end, int finished) {
		super();
		this.id = id;
		this.url = url;
		this.start = start;
		this.end = end;
		this.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;
	}
	@Override
	public String toString() {
		return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start
				+ ", end=" + end + ", finished=" + finished + "]";
	}
	
}



   主界面activity

package com.linxiaosheng.test3;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity {
	private TextView textView1;
	private ProgressBar progressBar1;
	private Button button2;
	private Button button1;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.fragment_main);
		textView1=(TextView)findViewById(R.id.textView1);
		progressBar1=(ProgressBar)findViewById(R.id.progressBar1);
		progressBar1.setProgress(0);
		progressBar1.setMax(100);
		button1=(Button)findViewById(R.id.button1);
		button2=(Button)findViewById(R.id.button2);
		final FileInfo fileInfo=new FileInfo(0,"http://www.imooc.com/mobile/imooc.apk", "imooc.apk", 0, 0);
		textView1.setText(fileInfo.getFileName());
		button1.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				Intent intent= new Intent(MainActivity.this,DownloadService.class);
				intent.setAction(DownloadService.ACTION_START);
				intent.putExtra("fileInfo", fileInfo);
				startService(intent);
			}
		});
		button2.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				Intent intent= new Intent();
				intent.setAction(DownloadService.ACTION_STOP);
				intent.setClass(MainActivity.this, DownloadService.class);
				startService(intent);
			}
		});
		IntentFilter filter=new IntentFilter(DownloadService.ACTION_UPDATE);
		registerReceiver(receiver, filter);
	}
	protected void onDestroy() {
		super.onDestroy();
		unregisterReceiver(receiver);
	};
	
	BroadcastReceiver receiver=new BroadcastReceiver(){

		@Override
		public void onReceive(Context context, Intent intent) {
			// TODO Auto-generated method stub
			if(DownloadService.ACTION_UPDATE.equals(intent.getAction())){
				int finished=intent.getIntExtra("finished", 0);
				Log.i("sysout", finished+"");
				progressBar1.setProgress(finished);
			}
		}
		
	};

}



   下载的Service:

package com.linxiaosheng.test3;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

import org.apache.http.HttpConnection;
import org.apache.http.HttpStatus;

import android.app.Service;
import android.content.Intent;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;

public class DownloadService extends Service {
	public static final String ACTION_START = "ACTION_START";
	public static final String ACTION_STOP = "ACTION_STOP";
	public static final String ACTION_UPDATE="ACTION_UPDATE";
	public static final String DOWNLOAD_PATH=Environment.getExternalStorageDirectory().getAbsolutePath()+"/downloads/";
	public static final int MSG_INIT=0;
	private DownloadTask downloadTask;
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		// TODO Auto-generated method stub
		if(ACTION_START.equals(intent.getAction())){
			FileInfo fileInfo=(FileInfo) intent.getSerializableExtra("fileInfo");
			Log.i("sysout", "开始下载");
			new MyThread(fileInfo).start();
		}
		if(ACTION_STOP.equals(intent.getAction())){
			Log.i("sysout", "停止下载");
			if(downloadTask!=null){
				downloadTask.pause=true;
			}
		}
		return super.onStartCommand(intent, flags, startId);
	}
	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}

	Handler handler=new Handler(){
		@Override
		public void handleMessage(Message msg) {
			// TODO Auto-generated method stub
			switch(msg.what){
			case MSG_INIT:
				FileInfo fileInfo=(FileInfo)msg.obj;
				Log.i("sysout", fileInfo.toString());
				downloadTask=new DownloadTask(DownloadService.this, fileInfo);
				downloadTask.download();
				if(downloadTask!=null){
					downloadTask.pause=false;
				}
				break;
			}
			
		}
	};
	/**
	 * 用于初始化mfileInfo,联网更新mfileInfo
	 * 一切的联网操作都要在子线程中执行
	 */
	class MyThread extends Thread{
		private FileInfo mfileInfo;
		public MyThread(FileInfo mfileInfo) {
			// TODO Auto-generated constructor stub
			this.mfileInfo=mfileInfo;
		}
		@Override
		public void run() {
			// TODO Auto-generated method stub
			RandomAccessFile accessFile=null;
			HttpURLConnection connection=null;
			try {
				URL httpurl=new URL(mfileInfo.getUrl());
				connection=(HttpURLConnection) httpurl.openConnection();
				connection.setConnectTimeout(5000);
				connection.setRequestMethod("GET");
				File dir=new File(DOWNLOAD_PATH);
				if(!dir.exists()){
					dir.mkdir();
				}
				File file=new File(DOWNLOAD_PATH,mfileInfo.getFileName());
				int length=-1;
				//判断网络连接是否成功
				if(connection.getResponseCode()==HttpStatus.SC_OK){
					//设置网络文件的长度
					length=connection.getContentLength();
				}
				if(length<=0)
					return;
				
				accessFile=new RandomAccessFile(file, "rwd");
				accessFile.setLength(length);
				mfileInfo.setLength(length);
				//发送消息
				Message msg=handler.obtainMessage(MSG_INIT,mfileInfo);
				msg.sendToTarget();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				if(accessFile!=null){
					try {
						accessFile.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				if(connection!=null){
					connection.disconnect();
				}
			}
		}
	}
}



数据库接口:

package com.linxiaosheng.test3;

import java.util.List;

/**
 * 线程接口
 */
public interface IThreadDAO {
	/**
	 * 插入线程信息
	 * @param threadInfo
	 */
	public void insertThread(ThreadInfo threadInfo);
	/**
	 * 删除线程信息
	 * @param url
	 * @param thread_id
	 */
	public void removeThread(String url,int thread_id);
	/**
	 * 查询线程信息
	 * @param url
	 * @param thread_id
	 * @return 
	 */
	public ThreadInfo getThreadById(String url,int thread_id);
	/**
	 * 更新线程完成进度
	 * @param url
	 * @param thread_id
	 * @param finished
	 */
	public void updateThread(String url,int thread_id,int finished);
	/**
	 * 获得线程列表
	 * @return
	 */
	public List<ThreadInfo> getThreads();
	
	public boolean isExistThread(String url,int thread_id);
}



数据库实现类

package com.linxiaosheng.test3;

import java.util.ArrayList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public class ThreadImplDAO implements IThreadDAO{

	private DBHelper dbHelper=null;
	public ThreadImplDAO(Context context){
		dbHelper=new DBHelper(context);
	}
	@Override
	public void insertThread(ThreadInfo threadInfo) {
		// TODO Auto-generated method stub
		SQLiteDatabase db=dbHelper.getWritableDatabase();
		ContentValues values=new ContentValues();
		values.put("id", threadInfo.getId());
		values.put("url", threadInfo.getUrl());
		values.put("start", threadInfo.getStart());
		values.put("end", threadInfo.getEnd());
		values.put("finished", threadInfo.getFinished());
		db.insert("tb_thread", null, values);
		db.close();
	}

	@Override
	public void removeThread(String url, int thread_id) {
		// TODO Auto-generated method stub
		SQLiteDatabase db=dbHelper.getWritableDatabase();
		db.delete("tb_thread", "url=? and id=?", new String[]{url,thread_id+""});
		db.close();
	}

	@Override
	public ThreadInfo getThreadById(String u, int thread_id) {
		// TODO Auto-generated method stub
		SQLiteDatabase db=dbHelper.getReadableDatabase();
		Cursor cursor=db.query("tb_thread", new String[]{"id","url","start","end","finished"},"url=? and id=?",new String[]{u,thread_id+""}, null,null,null);
		ThreadInfo info=null;
		while(cursor.moveToNext()){
			
			int id=cursor.getInt(cursor.getColumnIndex("id"));
			String url=cursor.getString(cursor.getColumnIndex("url"));
			int start=cursor.getInt(cursor.getColumnIndex("start"));
			int end=cursor.getInt(cursor.getColumnIndex("end"));
			int finished=cursor.getInt(cursor.getColumnIndex("finished"));
			info=new ThreadInfo(id,url,start,end,finished);
		}
		db.close();
		return info;
	}

	@Override
	public void updateThread(String url, int thread_id, int finished) {
		// TODO Auto-generated method stub
		SQLiteDatabase db=dbHelper.getWritableDatabase();
		ContentValues values=new ContentValues();
		values.put("finished", finished);
		db.update("tb_thread", values, "url=? and id=?", new String[]{url,thread_id+""});
		db.close();
	}

	@Override
	public List<ThreadInfo> getThreads() {
		// TODO Auto-generated method stub
		List<ThreadInfo> list=new ArrayList<ThreadInfo>();
		SQLiteDatabase db=dbHelper.getReadableDatabase();
		Cursor cursor=db.query("tb_thread", new String[]{"id","url","start","end","finished"},null, null, null, null, null);
		while(cursor.moveToNext()){
			int id=cursor.getInt(cursor.getColumnIndex("id"));
			String url=cursor.getString(cursor.getColumnIndex("url"));
			int start=cursor.getInt(cursor.getColumnIndex("start"));
			int end=cursor.getInt(cursor.getColumnIndex("end"));
			int finished=cursor.getInt(cursor.getColumnIndex("finished"));
			ThreadInfo info=new ThreadInfo(id,url,start,end,finished);
			list.add(info);
		}
		return list;
	}

	@Override
	public boolean isExistThread(String url, int thread_id) {
		// TODO Auto-generated method stub
		SQLiteDatabase db=dbHelper.getReadableDatabase();
		Cursor cursor=db.query("tb_thread", new String[]{"id","url","start","end","finished"},"url=? and id=?",new String[]{url,thread_id+""}, null,null,null);
		return cursor.moveToNext();
	}

}



DBHelper:

package com.linxiaosheng.test3;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DBHelper extends SQLiteOpenHelper{

	private static final String DB_NAME = "mydb";
	private static final int VERSION = 1;
	private static final String CREATE_TABLE = "create table tb_thread (id integer primary key,url text,start integer,end integer,finished integer)";
	private static final String DROP_TABLE = "drop table if exists tb_thread";
	public DBHelper(Context context) {
		super(context, DB_NAME, null, VERSION);
		// TODO Auto-generated constructor stub
		
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		// TODO Auto-generated method stub
		db.execSQL(CREATE_TABLE);
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		// TODO Auto-generated method stub
		db.execSQL(DROP_TABLE);
		db.execSQL(CREATE_TABLE);
		Log.i("sysout", "删除表");
	}
	
}

下载任务类:

package com.linxiaosheng.test3;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;

import org.apache.http.HttpStatus;

import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class DownloadTask {
	private Context mContexta=null;
	private FileInfo mFileInfo=null;
	private IThreadDAO threadDAO=null;
	private int mFinished=0;
	public static boolean pause=false;
	private ThreadInfo thread;
	public DownloadTask(Context mContexta,FileInfo mFileInfo){
		this.mContexta=mContexta;
		this.mFileInfo=mFileInfo;
		this.threadDAO=new ThreadImplDAO(mContexta);
	}
	public void download(){
		//读取数据库线程的信息
		List<ThreadInfo> list=threadDAO.getThreads();
		if(list.size()==0){
			thread=new ThreadInfo(0,mFileInfo.getUrl(),0,mFileInfo.getLength(),0);
			Log.i("sysout", "创建线程"+thread.toString());
		}else{
			thread=list.get(0);
			Log.i("sysout", "取得数据库线程"+thread.toString());
		}
		new DownloadThread(thread).start();
	}
	class DownloadThread extends Thread{
		private ThreadInfo threadInfo=null;
		public DownloadThread(ThreadInfo threadInfo) {
			super();
			this.threadInfo = threadInfo;
		}

		@Override
		public void run() {
			//向数据库插入线程信息
			if(!threadDAO.isExistThread(threadInfo.getUrl(), threadInfo.getId())){
				threadDAO.insertThread(threadInfo);
			}
			InputStream inputStream=null;
			HttpURLConnection connection=null;
			RandomAccessFile accessFile=null;
			try {
				URL url=new URL(threadInfo.getUrl());
				connection=(HttpURLConnection) url.openConnection();
				connection.setConnectTimeout(5000);
				connection.setRequestMethod("GET");
				//设置下载位置
				int start=threadInfo.getStart()+threadInfo.getFinished();
				//设置请求属性,设置下载的范围,避免从头开始下载
				Log.i("sysout", "byte="+start+"-"+threadInfo.getEnd());
				//注意这里是bytes带有s的
				connection.setRequestProperty("Range", "bytes="+start+"-"+threadInfo.getEnd());
				//设置下载文件
				File file=new File(DownloadService.DOWNLOAD_PATH,mFileInfo.getFileName());
				accessFile=new RandomAccessFile(file, "rwd");
				//设置文件的写入位置
				accessFile.seek(start);
				//更新
				Intent intent=new Intent();
				intent.setAction(DownloadService.ACTION_UPDATE);
				//设置已完成进度
				mFinished+=threadInfo.getFinished();
				Log.i("sysout","回复码"+connection.getResponseCode());
				//这里是选择性下载的,返回的状态码是206
				if(connection.getResponseCode()==HttpStatus.SC_PARTIAL_CONTENT){
					//读取数据
					inputStream=connection.getInputStream();
					byte[] b=new byte[1024*4];
					int len=-1;
					long time=System.currentTimeMillis();
					while((len=inputStream.read(b))!=-1){
						accessFile.write(b,0,len);
						//把下载的进度广播给activity
						mFinished+=len;
						Log.i("sysout","已完成进度"+mFinished);
						threadInfo.setFinished(mFinished);<span >															//已完成进度等于文件长度直接更新进度条,退出下载
<span >						</span>if(mFinished==mFileInfo.getLength()){
<span >							</span>intent.putExtra("finished", 100);
<span >							</span>mContexta.sendBroadcast(intent);
<span >							</span>break;
<span >						</span>}							</span>
						if(System.currentTimeMillis()-time>500){
							intent.putExtra("finished", mFinished*100/mFileInfo.getLength());
							time=System.currentTimeMillis();
							mContexta.sendBroadcast(intent);
						}
						Log.i("sysout","状态pause"+pause);
						if(pause){
							threadDAO.updateThread(threadInfo.getUrl(),threadInfo.getId(), threadInfo.getFinished());
							return;
						}
					}
					//下载完毕,删除线程信息
					threadDAO.removeThread(threadInfo.getUrl(), threadInfo.getId());
							
				}
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				if(inputStream!=null){
					try {
						inputStream.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				if(accessFile!=null){
					try {
						accessFile.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				if(connection!=null){
					connection.disconnect();
				}
			}
			
			
			
		}
	}
}

主界面的布局文件


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.linxiaosheng.test3.MainActivity$PlaceholderFragment" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="下载进度" />

    <ProgressBar
        android:id="@+id/progressBar1"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textView1"
        android:layout_alignRight="@+id/button2"
        android:layout_below="@+id/textView1"
        android:max="100"
        android:progress="0" />

    <Button
        android:id="@+id/button2"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/button1"
        android:layout_alignBottom="@+id/button1"
        android:layout_alignParentRight="true"
        android:layout_marginRight="14dp"
        android:text="停止" />

    <Button
        android:id="@+id/button1"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/progressBar1"
        android:layout_toLeftOf="@+id/button2"
        android:text="开始" />

</RelativeLayout>



还有AndroidManifest.xml,这里我们要开启网络通信和写入存储卡的权限,还有把service给加上去

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.linxiaosheng.test3"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".DownloadService"></service>
    </application>

</manifest>

结果截图:

基本上代码都在上面了,这里我在补充几点我在做的过程中遇到的几个问题:

  1. 还是上面说到的,网络通信要单独开启子线程进行操作,因为网络通信是一个耗时耗资源的操作,放在主线程阻碍的运行,这个好像是在4.0以后android就明确要求了,不然会报错的。
  2. 同时下载的操作最好是放在service上去操作,虽然放在activity上也是可行的,但是因为activity是可见的,很容易遭到用户的关闭或者因为activity的切换被回收器回收了,而service是不可见的,同时它的安全级别是比较高的,不至于被回收器所回收,所以可以保证下载可以正常执行。
  3. 断点续传到底是怎么实现的呢?主要是结合数据存储和网络分段请求。在网络连接的使用用到了connection.setRequestProperty("Range", "bytes="+start+"-"+threadInfo.getEnd());这里的意图是指定请求下载的范围,注意后面的网络请求状态的判断要用HttpStatus.SC_PARTIAL_CONTENT,即返回的是206。同时这里也用到一个很重要的类就是RandomAccessFile accessFile=new RandomAccessFile(file, "rwd");   RandomAccessFile 的特点就是可以随机写入,通过查询上一次下载的进度,继续在文件的上次写入的位置继续写入,避免了每次下载都要重新写入。还有一个就是每次暂停,都会把当前的下载进度更新到数据库,下次下载的时候就可以拿出来用了。
  4. 进度条是怎么更新的?这里进度条的更新是通过发送广播的方式,然后由activity来接受广播,再更新进度条,这里也可以通过handler消息的放送,主线程做消息的处理来更新进度条的进度(有时间试一下)。

好,改讲的都讲了,只不过这里只是用到了单个文件的断点续传,后面再更细多个文件同时的断点续传。