课程设计做的是一个类似网盘的东西,用的是新浪微盘的API接口进行存储(百度的PCS已关闭好不爽。。),里面有一个功能就用到了典型的断点续传了,这里那这个东西做一个小练习,后面再把这个技术加到项目里来。
做这个东西大概用的几个技术知识点,大概就是:
- Activity之间Intent的传输。
- Service的使用。
- 线程的使用,包括handler的消息的处理。
- 网络通信的操作。
- 数据库数据的存储。
- 广播技术的使用。
整个功能的操作流程如下图所示:
首先通过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>
结果截图:
基本上代码都在上面了,这里我在补充几点我在做的过程中遇到的几个问题:
- 还是上面说到的,网络通信要单独开启子线程进行操作,因为网络通信是一个耗时耗资源的操作,放在主线程阻碍的运行,这个好像是在4.0以后android就明确要求了,不然会报错的。
- 同时下载的操作最好是放在service上去操作,虽然放在activity上也是可行的,但是因为activity是可见的,很容易遭到用户的关闭或者因为activity的切换被回收器回收了,而service是不可见的,同时它的安全级别是比较高的,不至于被回收器所回收,所以可以保证下载可以正常执行。
- 断点续传到底是怎么实现的呢?主要是结合数据存储和网络分段请求。在网络连接的使用用到了connection.setRequestProperty("Range", "bytes="+start+"-"+threadInfo.getEnd());这里的意图是指定请求下载的范围,注意后面的网络请求状态的判断要用HttpStatus.SC_PARTIAL_CONTENT,即返回的是206。同时这里也用到一个很重要的类就是RandomAccessFile accessFile=new RandomAccessFile(file, "rwd"); RandomAccessFile 的特点就是可以随机写入,通过查询上一次下载的进度,继续在文件的上次写入的位置继续写入,避免了每次下载都要重新写入。还有一个就是每次暂停,都会把当前的下载进度更新到数据库,下次下载的时候就可以拿出来用了。
- 进度条是怎么更新的?这里进度条的更新是通过发送广播的方式,然后由activity来接受广播,再更新进度条,这里也可以通过handler消息的放送,主线程做消息的处理来更新进度条的进度(有时间试一下)。
好,改讲的都讲了,只不过这里只是用到了单个文件的断点续传,后面再更细多个文件同时的断点续传。