为了实现文件的断点续传,会用到的知识:
- 简单的文件布局
- 子线程与Handle
- 广播
- 服务
- 数据库表操作
- 网络和文件传输
简单的文件布局
如图中所示,有两个按钮和ProgresBar(横向)
id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
/>
在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 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 getThreads(String url) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
List list = new ArrayList();
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 list = new ArrayList();
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 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();
}
DownloadThread是DownloadTaskd的一个内部线程类
这个线程的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