一,原生的DownloadManager
从Android 2.3(API level 9)开始,Android以Service的方式提供了全局的DownloadManager来系统级地优化处理长时间的下载操作。
DownloadManager支持失败重试、Notification通知等基本特性。但是DownloadManager提供的接口很有限,虽然有暂停状态却没有提供主动暂停的接口。所以需手动实现断点续传,以及需手动实现单文件的多线程(分段)下载,还是比较麻烦。
总结:DownloadManager比较适用于简单的单文件下载。
二,第三方组件
1,FileDownloader
git地址:https://github.com/lingochamp/FileDownloader
中文文档:https://github.com/lingochamp/FileDownloader/blob/master/README-zh.md
描述:在第三方下载组件中比较突出,成熟,稳定,健壮。支持多任务下载,支持多线程下载,支持断点续传,支持替换网络请求框架等。
实际应用:在快手的后台进程里有liulishuo/FileDownloader,因为快手内有下载服务,有可能用于应用内的更新模块,liulishuo的FileDownloader就是这个组件。
2,OkDownload
git地址:https://github.com/lingochamp/okdownload
中文文档:https://github.com/lingochamp/okdownload/blob/master/README-zh.md
描述:继承了所有FileDownloader的优点,甚至做了更多的优化以及更多的特性。
总结:建议在项目中实现多任务多线程下载功能优先考虑上面的第三方组件,而不是重复造轮子。毕竟能在受众群大的商业应用中成熟使用的第三方框架,是经过多次迭代的思想结晶,考虑了多种情况,踩过很多坑。一般自己从头写的话,很难比其的做得更好,考虑得更完善,尤其是在有限的项目开发时间内。
三,原理demo
这里列一个两年前写的demo,用的都是非常基础的东西。考虑的点也比较少,就只是从头实现这个功能。
(一)思路:
1,如何实现多线程下载功能?
(后续都假设1个文件启动3个线程去下载)先起一个初始线程(initThread)去查询目标下载文件的基本信息比如文件长度等,生成对应本地文件。计算每个下载线程(DownloadThread)的下载长度和下载起始结束点,通过线程池启动3个下载线程进行下载并写入到本地文件的对应起始点中,当3个下载线程都下载完成后,这个文件就下载完整了。
2,如何实现暂停功能?
设计一个下载任务类(DownloadTask),开始下载一个文件就新建一个下载任务类DownloadTask。下载任务类负责管理启动多个下载线程开始下载。下载任务类里设置一个公开的暂停标志,在外部可以设置暂停标志的true/false,当下载线程下载文件时,判断到暂停标志为true,则结束本线程即中断下载。
3,如何实现即时展示当前下载进度?
下载任务类里设置一个表示当前已完成下载长度的变量,在下载线程的下载过程中,及时更新累加。设置每500ms计算一次下载进度=当前已完成下载长度/下载文件总长度,并发出广播。外部接收到广播后更新ui。
4,如何实现断点续传功能(暂停/网络故障后再恢复下载从之前下载进度基础上继续下载,而不是重新下载)?
这就必须把下载进程的相关信息本地存储,然后在恢复下载时查询本地存储的数据,获得目前的下载进度,再从那开始下载。
要给每一个下载线程保存一条这个线程相关信息的数据,考虑到当新建n个下载任务时,同时开启3n个下载线程,就需要保存3n条线程信息数据。而在暂停/网络故障/下载失败时,都要更新对应数据,选用本地数据库sqlite来存储数据。
安卓的数据表的操作,基本就是“数据模型+helper帮助类+dao实现类”来一套。
但是注意这里是多线程操作数据表,“增删改查”操作里,“增删改”方法都要加上synchronized修饰词。synchronized是保证线程安全,就是在当多个线程操作同一资源时保证某一时刻只能有一个线程在执行这个任务。
举个例子,想象一个线程是一个人,一张数据表装在一栋房子里,房子有两个门,门a只给不带synchronized方法的人进,门b只给带synchronized方法的人进。门a没什么条件,只要是使用不带synchronized方法的人都可以直接进去。门b却有条件,一次只能进去一个人,并且下一个进去的人要在上一个进去的人出来之后才能进去。
synchronized更多详细资料参考:https://www.jianshu.com/p/d53bf830fa09
https://www.cnblogs.com/yulinfeng/p/11020576.html
(二)当年凑zhuanli画的流程图:
(三)代码:
启动下载
Intent intent = new Intent( mContext, DownloadService.class );
fileInfo.setIsDownload("true");
intent.setAction(IntentAction.ACTION_START);
intent.putExtra( KeyName.FILEINFO_TAG, fileInfo );
mContext.startService(intent); // 通过intent传递信息fileInfo给servers
暂停下载
Intent intent = new Intent( mContext, DownloadService.class );
fileInfo.setIsDownload("false");
intent.setAction(IntentAction.ACTION_PAUSE);
intent.putExtra( KeyName.FILEINFO_TAG, fileInfo );
mContext.startService(intent);
DownloadService
/**
* 用于文件下载的service
*/
public class DownloadService extends Service {
/**
* 定义消息处理handler的标志
*/
private static final int MSG_INIT =0 ;
/**
* 文件下载线程任务的集合
*/
private MapmTask =new LinkedHashMap();
/**
* onStartCommand()接收Activity中StartService发送的信息
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 获取Activity传进来的参数
FileInfo fileInfo =(FileInfo) intent.getSerializableExtra( KeyName.FILEINFO_TAG );// 获取下载文件信息
Log.i("intent", "DownloadService-47行- "+fileInfo.getFileName()+" intent=null: " + (intent==null) );
Log.i("intent", "DownloadService-48行- "+fileInfo.getFileName()+" intent.getSerializableExtra(Config.FILEINFO_TAG):"
+(intent.getSerializableExtra(KeyName.FILEINFO_TAG)==null) );
// 开启多线程下载
if ( IntentAction.ACTION_START.equals(intent.getAction()) ) {
initThread mInitThread =new initThread(fileInfo);
DownloadTask.sExecutorService.execute(mInitThread);// 通过线程池开启初始化线程
// 暂停多线程下载
}else if ( IntentAction.ACTION_PAUSE.equals(intent.getAction()) ) {
DownloadTask tast =mTask.get(fileInfo.getId() ); // 从集合中取出下载任务
if (tast !=null) {
tast.isPause =true ;
}
}else if(IntentAction.ACTION_DELETE.equals(intent.getAction())){
DownloadTask tast =mTask.get(fileInfo.getId() ); // 从集合中取出下载任务
if (tast !=null) {
tast.isDel=true ;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinderonBind(Intent intent) {
return null;
}
/**
* 初始化文件下载线程:创建本地下载位置,并开启下载任务
**/
class initThread extends Thread{
/**
* 下载的文件的所有属性信息
*/
private FileInfomFileInfo =null ;
/**
* 初始化文件下载线程:确保创建本地下载位置,并开启文件下载任务
* @param mFileInfo 下载的文件的所有属性信息
*/
public initThread(FileInfo mFileInfo) {
this.mFileInfo = mFileInfo;
}
//开启开启下载任务
Handlerhandler =new Handler(){
@Override
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case MSG_INIT:
FileInfo fileInfo = (FileInfo) msg.obj ;
// 启动下载任务
DownloadTask task =new DownloadTask( DownloadService.this, fileInfo, DownloadConfig.DONWNLOAD_THREAD_NUM);
task.download();
//把下载任务添加到下载集合中
mTask.put(fileInfo.getId(), task); //将开启下载线程的id和实例添加到map中,在暂停时通过id获取实例,并令它暂停
break;
default:
break;
}
}
};
@Override
public void run(){
HttpURLConnection conn =null ;
RandomAccessFile raf =null ;// 随机访问文件,可以在文件的随机写入,对应断点续传功能
try {
// 连接网络文件
URL url =new URL(mFileInfo.getUrl() );
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);// 设置连接超时时间
conn.setRequestMethod("GET");
// 获取文件长度
int length = -1;
if (conn.getResponseCode()== HttpStatus.SC_OK ) {
length = conn.getContentLength();
Log.i("http", "DownloadService-138行 获取网络数据长度="+ length );
}else{
Log.i("http", "DownloadService-140行 http连接失败!");
}
if ( length <=0 ){
return ;
}
// 判断下载路径是否存在
File dir =new File( DownloadConfig.DOWNLOAD_PATH );
if ( !dir.exists() ){
dir.mkdir();
}
// 在本地创建文件
File file =new File( dir, mFileInfo.getFileName() );
raf =new RandomAccessFile( file, "rwd" );
// 设置文件长度
raf.setLength( length );
mFileInfo.setLength( length );
handler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget(); //将数据发回给handler
//关闭连接
if (raf!=null) {
raf.close();
}if (conn !=null) {
conn.disconnect();
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
DownloadTask
/**
* 下载任务类
**/
public class DownloadTask {
/**
* 上下文
*/
private Context mContext = null ;
/**
* 下载的文件的所有属性信息
*/
private FileInfo mFileInfo = null ;
/**
* 数据(库)访问接口
*/
private ThreadDAO mDAO = null;
/**
* 已下载字节长度
*/
private int mFinishedLen = 0;
/**
* 下载暂停标志
*/
public Boolean isPause = false;
/**
* 下载删除标志
*/
public Boolean isDel = false;
/**
* 默认的下载线程数
*/
private int mThreadNum = 0 ;
/**
* 下载线程集合
*/
private List<DownloadThread> mThreadList = null ;
/**
* 带缓存线程池,s开头表示用到static关键字
*/
public static ExecutorService sExecutorService = Executors.newCachedThreadPool();
/**
* 文件下载的线程任务类
* @param mContext 上下文
* @param mFileInfo 下载的文件的所有属性信息
* @param threadNum 文件分段下载线程数
*/
public DownloadTask(Context mContext, FileInfo mFileInfo, int threadNum) {
this.mContext = mContext;
this.mFileInfo = mFileInfo;
this.mThreadNum = threadNum ;
mDAO = new ThreadDAOIImpl(mContext);
}
/**
* 开始下载文件
*/
public void download() {
List<ThreadInfo> threadInfoList = mDAO.getThreads(mFileInfo.getUrl()); // 读取数据库中文件下载的信息
// 不存下载线程则创建
if( threadInfoList.size()== 0 ){
int length = mFileInfo.getLength()/mThreadNum ; // 获取单个线程下载长度
for (int i = 0; i < mThreadNum; i++) { // 创建线程信息
ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(),length*i, (i+1)*length-1, 0, mFileInfo.getMd5(),"false", "none");
if (i==mThreadNum-1) {
threadInfo.setEnd(mFileInfo.getLength());// 设置最后一个线程的下载长度
}
threadInfoList.add(threadInfo); // 添加线程信息到集合
mDAO.insertThread(threadInfo); // 向数据库插入文件下载的信息
} // 放到run外面,比较不容易产生数据库的死锁
}
// 启动多个线程进行下载
mThreadList = new ArrayList<DownloadThread>();
for (ThreadInfo info : threadInfoList) {
DownloadThread downloadThread = new DownloadThread(info);
if ( info.getFinished()<(info.getEnd()-info.getStart())&& info.getId()==0 ||
info.getFinished()<(info.getEnd()-info.getStart()+1)&& info.getId()!=0 ) { //若未完成下载则下载
DownloadTask.sExecutorService.execute(downloadThread);
mThreadList.add(downloadThread); //添加线程到集合中
}
}
}
/**
* 判断所有线程是否执行完成
* @param threadInfo 下载文件的线程信息
*/
private synchronized void checkAllThreadsFinished(ThreadInfo threadInfo){ // synchronized 同步方法,保证同一时间只有一个线程访问该方法
boolean allFinished = true ; //所有线程下载结束标识
// 遍历线程集合,判断是否都下载完毕
for (DownloadThread thread : mThreadList) {
if (!thread.isFinished) {
allFinished = false ;
break ;
}
}
// 所有线程下载结束:验证md5,相同则存储下载长度,否则清空;发送广播通知UI下载任务结束
if (allFinished) {
MD5Util md5Util = new MD5Util();
DateTools dateTools = new DateTools();
List<ThreadInfo> threadInfosList =mDAO.getThreads(threadInfo.getUrl());
for (ThreadInfo info :threadInfosList) {
if ( !md5Util.isMD5Equal(info) ) { // md5验证失败,清空下载长度
mDAO.updateThread(info.getUrl(), info.getId(), 0, info.getMd5(), "false" ,dateTools.getCurrentTime() );
}else if(info.getId()==0) {
mDAO.updateThread(info.getUrl(), info.getId(),(info.getEnd()-info.getStart()), info.getMd5(), "true", dateTools.getCurrentTime() );
}else if(info.getId()!=0){
mDAO.updateThread(info.getUrl(), info.getId(),(info.getEnd()-info.getStart()+1), info.getMd5(), "true",dateTools.getCurrentTime() );
}
}
Intent intent = new Intent(IntentAction.ACTION_FINISH);
mFileInfo.setOvertime(dateTools.getCurrentTime());
intent.putExtra( KeyName.FILEINFO_TAG, mFileInfo);
mContext.sendBroadcast(intent);
}
}
/**
* 进行文件下载的线程
**/
class DownloadThread extends Thread{
/**
* 文件下载线程的信息
*/
private ThreadInfo mThreadInfo = null ;
/**
* 标识线程是否执行完成
*/
public Boolean isFinished = false ;
/**
* 广播下载进度的间隔时间
*/
private final static int BROADCAST_TIME = 100 ;
/**
* httpUrl连接
*/
HttpURLConnection conn = null ;
/**
* 任意写入文件:RandomAccessFile
*/
RandomAccessFile raf= null;
/**
* 输入流
*/
InputStream input = null ;
/**
* 进行文件下载的线程
* @param mThreadInfo 文件下载线程的信息
**/
public DownloadThread(ThreadInfo mThreadInfo) {
this.mThreadInfo = mThreadInfo;
}
@Override
public void run(){
try {
URL url = new URL(mThreadInfo.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setRequestMethod("GET");
//设置url上资源下载位置/范围
int start = mThreadInfo.getStart() +mThreadInfo.getFinished();
conn.setRequestProperty("Range", "bytes=" +start +"-" + mThreadInfo.getEnd() );
//设置文件本地写入位置
File file = new File( DownloadConfig.DOWNLOAD_PATH, mFileInfo.getFileName() );
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
mFinishedLen +=mThreadInfo.getFinished();
//开始下载啦
if ( conn.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT ){
//读取数据
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);
mFinishedLen += len ;
mThreadInfo.setFinished(mThreadInfo.getFinished() + len);
if (System.currentTimeMillis()-time > BROADCAST_TIME ) { //每500ms刷新一次
time = System.currentTimeMillis();
double progress_result = (double)mFinishedLen / (double)mFileInfo.getLength(); // 计算下载进度
double download_rate = (double)len / (double)(1024*BROADCAST_TIME/1000); // 计算下载速率
Intent intent = new Intent( IntentAction.ACTION_UPDATE);
intent.putExtra( KeyName.FINISHED_TAG,progress_result );
intent.putExtra( KeyName.DOWNLOAD_RATE_TAG,download_rate );
intent.putExtra("id", mFileInfo.getId() );
mContext.sendBroadcast(intent);
}
//下载暂停时,保存下载进度搭配数据库
if (isPause) {
mDAO.updateThread(mThreadInfo.getUrl(), mThreadInfo.getId(), mThreadInfo.getFinished(), mThreadInfo.getMd5(), "false", "none" );
return;
}
//删除下载时,删除数据库内的相关信息
if (isDel) {
if(!mDAO.getThreads(mThreadInfo.getUrl()).isEmpty()) {
mDAO.deleteThread(mThreadInfo.getUrl());
}
// Intent intent = new Intent( IntentAction.ACTION_DELETE);
// mContext.sendBroadcast(intent);
return;
}
// 下载连接获取输入流或文件写入失败,清空文件下载进度,并重新下载
if (raf ==null || input ==null ){
mDAO.updateThread(mThreadInfo.getUrl(), 0, 0, mThreadInfo.getMd5(), "false" , "none");
mDAO.updateThread(mThreadInfo.getUrl(), 1, 0, mThreadInfo.getMd5(), "false" , "none");
mDAO.updateThread(mThreadInfo.getUrl(), 2, 0, mThreadInfo.getMd5(), "false" , "none");
Intent intent = new Intent( IntentAction.ACTION_UPDATE);
intent.putExtra( KeyName.FINISHED_TAG, 0 );
intent.putExtra( KeyName.DOWNLOAD_RATE_TAG, 0 );
intent.putExtra("id", mFileInfo.getId() );
mContext.sendBroadcast(intent);
download();
}
}
isFinished = true ;// 标识线程执行完毕
// 检查下载任务是否执行完毕
checkAllThreadsFinished(mThreadInfo );
}
} catch (Exception e) {
e.printStackTrace();
}finally{//关闭连接
try {
if (raf !=null) {
raf.close();
}else {
System.out.println("DOwnloadTask-280行 RadomAccessFile发生错误");
}if (input !=null) {
input.close();
}else {
System.out.println("DOwnloadTask-280行 inputStream发生错误");
}if (conn !=null) {
conn.disconnect();
}else {
System.out.println("DOwnloadTask-280行 HttpURLConnection发生错误");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileInfo
/**
* 下载的文件的所有属性信息
*/
public class FileInfo implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 下载文件的id
*/
private int id ;
/**
* 下载文件总长度
*/
private int length ;
/**
* 下载文件的web路径
*/
private String url ;
/**
* 下载文件名
*/
private String fileName ;
/**
* 下载文件的 MD5值
*/
private String md5 ;
/**
* 文件下载进度(%)
*/
private double finished ;
/**
* 文件下载速率
*/
private double rate ;
/**
* 下载文件的 MD5值
*/
private String over ;
/**
* 下载文件完成下载的时间
*/
private String overtime ;
public String getOvertime() {
return overtime;
}
public void setOvertime(String overtime) {
this.overtime = overtime;
}
/**
* 下载文件是否正在下载
*/
private String isDownload ;
public FileInfo() {
}
/**
* 存储下载文件的属性信息
* @param id 下载文件的id标识
* @param length 文件总长度
* @param url 文件web路径
* @param fileName 下载文件名
* @param finished 文件下载进度
* @param rate 文件下载速率
* @param md5 文件的md5值
* @param over 文件是否完成下载
* @param isDownload 文件是否正在下载
*/
public FileInfo(int id, int length, String url, String fileName,
int finished,double rate, String md5, String over, String isDownload, String overtime) {
this.id = id;
this.url = url;
this.length = length;
this.fileName = fileName;
this.finished = finished;
this.rate = rate;
this.md5 = md5;
this.over = over ;
this.overtime = overtime ;
this.isDownload = isDownload ;
}
/**
* 获取文件下载状态
*/
public String getIsDownload() {
return isDownload;
}
/**
* 设置文件下载状态
* @param isDownload 文件是否正在下载
*/
public void setIsDownload(String isDownload) {
this.isDownload = isDownload;
}
/**
* 获取文件下载进度(%)
*/
public double getRate() {
return rate;
}
/**
* 存储文件下载进度(%)
*/
public void setRate(double rate) {
this.rate = rate;
}
/**
* 获取文件MD5
*/
public String getMd5() {
return md5;
}
/**
* 存储文件MD5
*/
public void setMd5(String md5) {
this.md5 = md5;
}
/**
* 获取下载文件的id标识
*/
public int getId() {
return id;
}
/**
* 设置下载文件的
*/
public void setId(int id) {
this.id = id;
}
/**
* 获取下载文件的长度
*/
public int getLength() {
return length;
}
/**
* 存入下载文件的
*/
public void setLength(int length) {
this.length = length;
}
/**
* 获取下载文件的URL路径
*/
public String getUrl() {
return url;
}
/**
* 存储下载文件的URL路径
*/
public void setUrl(String url) {
this.url = url;
}
/**
* 获取下载文件的文件名
*/
public String getFileName() {
return fileName;
}
/**
* 存储下载文件的文件名
*/
public void setFileName(String fileName) {
this.fileName = fileName;
}
/**
* 获取下载文件的下载进度
*/
public double getFinished() {
return finished;
}
/**
* 存储下载文件的存储进度
*/
public void setFinished(double finished) {
this.finished = finished;
}
/**
* 获取文件完成下载标识
*/
public String getOver() {
return over;
}
/**
* 存储文件完成下载标识
*/
public void setOver(String over) {
this.over = over;
}
/**
* 获取下载文件的所有属性信息
*/
@Override
public String toString() {
return "FileInfo [id=" + id + ", length=" + length + ", url=" + url
+ ", fileName=" + fileName + ", finished="
+ finished +", rate=" + rate +", md5=" + md5 + "]";
}
}
ThreadInfo
/**
*文件下载线程的信息
**/
public class ThreadInfo {
/**
* 文件下载线程的id标识
*/
private int id ;
/**
* 下载文件的URL路径
*/
private String url ;
/**
* 文件下载的起始字节位置
*/
private int start ;
/**
* 文件下载的结束字节位置
*/
private int end ;
/**
* 文件已下载的字节长度
*/
private int finished ;
/**
* 下载文件的 MD5值
*/
private String md5 ;
/**
* 下载文件是否完成下载
*/
private String over ;
/**
* 下载文件完成下载的时间
*/
private String overtime ;
public ThreadInfo() {
}
/**
* 存储文件下载线程的属性信息
* @param id 文件下载线程id标识
* @param url 下载文件路径
* @param start 文件下载起始字节位置
* @param end 文件下载终止字节位置
* @param finished 文件已下载的字节长度
* @param md5 文件的md5值
* @param over 文件是否完成下载
* @param overtime 文件完成下载的时间
*/
public ThreadInfo(int id, String url, int start, int end, int finished, String md5, String over,String overtime) {
this.id = id;
this.url = url;
this.start = start;
this.end = end;
this.finished = finished;
this.md5 = md5;
this.over = over;
this.overtime = overtime;
}
/**
* 获取文件完成下载的时间
*/
public String getOvertime() {
return overtime;
}
/**
* 设置文件完成下载的时间
*/
public void setOver_time(String overtime) {
this.overtime = overtime;
}
/**
* 获取文件下载线程的id标识
*/
public int getId() {
return id;
}
/**
* 设置文件下载线程的id标识
*/
public void setId(int id) {
this.id = id;
}
/**
* 获取文件下载线程的url
*/
public String getUrl() {
return url;
}
/**
* 设置下载文件的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;
}
/**
* 获取文件MD5
*/
public String getMd5() {
return md5;
}
/**
* 存储文件MD5
*/
public void setMd5(String md5) {
this.md5 = md5;
}
/**
* 获取文件完成下载标识
*/
public String getOver() {
return over;
}
/**
* 存储文件完成下载标识
*/
public void setOver(String over) {
this.over = over;
}
/**
* 获取文件下载线程的所有属性信息
*/
@Override
public String toString() {
return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start
+ ", end =" + end + ", finished=" + finished +", md5=" + md5
+ ", over=" + over +"]";
}
}
DBHelper
/**
* 数据库帮助类
* 多线程操作数据库时注意使用【单例模式】
* 1、构造方法定为private
* 2、定义该类的一个静态对象用以应用
* 3、通过getInstance()方法返回该类对象,
* 使该方法无论调用多少次,该类都是唯一。
*/
public class DBHelper extends SQLiteOpenHelper{
private AtomicInteger mOpenCounter = new AtomicInteger();
private SQLiteDatabase mDatabase;
/**
* 数据库名称
*/
private static final String DB_NAME = "download.db" ;
/**
* 数据库名称
*/
public static final String TABLE_NAME = "thread_info" ;
/**
* 文件下载线程id
*/
public static final String THREAD_ID = "thread_id" ;
/**
* 下载文件的url文件
*/
public static final String URL = "url" ;
/**
* 文件下载的起始位置(字节长度)
*/
public static final String START = "start" ;
/**
* 文件下载的结束位置(字节长度)
*/
public static final String END = "end" ;
/**
* 文件已下载的字节长度
*/
public static final String FINISHED = "finished" ;
/**
* 文件的md5
*/
public static final String MD5 = "md5" ;
/**
* 文件是否完成下载标识
*/
public static final String OVER = "over" ;
/**
* 文件是成下载时间
*/
public static final String OVER_TIME = "over_time" ;
/**
* 数据库帮助类的静态对象引用
*/
private static DBHelper sHelper = null ;
/**
* 数据库版本
*/
private static final int VERSION = 1;
/**
* sql创建保存线程信息表命令句
*/
private static final String SQL_CREATE =
"create table " +TABLE_NAME +" (_id integer primary key autoincrement,"
+ THREAD_ID +" integer, "
+ URL +" text, "
+ START +" integer, "
+ END +" integer,"
+ FINISHED +" integer,"
+ MD5 +" text, "
+ OVER +" text, "
+OVER_TIME+ " text )";
/**
* 删除表命令句
*/
private static final String SQL_DROP = "drop table if exists "+TABLE_NAME;
private DBHelper(Context context) { // 将public改为private,
super(context, DB_NAME, null, VERSION); // 防止在其它地方被new出来,保证db的单例,防止数据库被锁定
}
/**
* 获得类对象sHelper
*/
public static DBHelper getInstance(Context context){ // 单例模式,DBHelper只会被实例化一次
if (sHelper == null ) { // 静态方法访问数据库,无论创建多少个数据库访问对象,
sHelper = new DBHelper(context); // 里面的Helper只有一个,保证程序中只有一个DBHelper对数据库进行访问
}
return sHelper ;
}
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = sHelper.getWritableDatabase();
}
return mDatabase;
}
public synchronized void closeDatabase() {
if(mOpenCounter.decrementAndGet() == 0) {
// Closing database
mDatabase.close();
}
}
/**
* 创建表
*/
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE); //创建表
}
/**
* 更新数据库表
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(SQL_DROP);
db.execSQL(SQL_CREATE);
}
}
ThreadDAO
/**
* 【数据访问接口】
* DAO:data access object 数据访问对象,DAO一定是和数据库的每张表一一对应;
* 是一种应用程序表承接口(api),可以访问其它的结构化查询语言(SQL)数据库;
* J2EE用DAO设计模块,把底层数据访问和高层的商务逻辑分开;典型的DAO有以下几个组件:
* 1、一个DAO工厂类
* 2、一个DAO接口
* 3、一个实现DAO接口的具体类
* 4、数据传递对象
*/
public interface ThreadDAO {
/**
* 插入文件下载信息
* @param threadInfo 下载线程的信息
*/
public void insertThread(ThreadInfo threadInfo);
/**
* 删除文件下载信息
*/
public void deleteThread(String url );
/**
* 更新下载文件下载进度
* @param url 下载线程的URL
* @param thread_id 下载线程的id
* @param finished 下载线程的文件已下载字节数
* @param md5 下载文件的md5
* @param over 文件下载完成标识
* @param over_time 文件下载完成时间
*/
public void updateThread(String url, int thread_id, int finished, String md5, String over, String over_time) ;
/**
* 查询文件的文件下载线程信息
* @param url 下载线程的URL
* @return List<ThreadInfo> 线程信息集
*/
public List<ThreadInfo> getThreads(String url) ;
/**
* 获取所有文件的文件下载信息
* @return List<ThreadInfo> 包含下载线程信息的list集
*/
public List<FileInfo> getDBFileInfoList();
/**
* 文件下载信息是否已结束下载
* @return 当存在下载信息返回true;否则返回false
*/
boolean isDownloadOver(String url);
}
ThreadDAOIImpl
/**
* 【数据库访问接口的实现】
*/
public class ThreadDAOIImpl implements ThreadDAO{
/**
* 数据库帮助类
*/
private DBHelper mDBHelper = null ; // 将DBHelper用private修饰
public ThreadDAOIImpl(Context context) {
mDBHelper = DBHelper.getInstance(context);
}
/**
* 插入文件下载信息】
* 多线程数据库的增、删、改(更新)方法用synchronized修饰,以保证线程安全:
* 保证同一时间段不会有多个(只有一个)线程对数据库进行增删改,需等待线程执
* 行完后再开启线程执行下一个功能;而查询因为不用操作数据库,不会导致 数据库
* 死锁 ,所以不用
* @see com.download.db.ThreadDAO#insertThread(com.download.entities.ThreadInfo)
*/
@Override
public synchronized void insertThread(ThreadInfo threadInfo ){
SQLiteDatabase db = mDBHelper.openDatabase(); // 实例化数据库,设置为【读写】模式
db.execSQL( "insert into " +DBHelper.TABLE_NAME
+ " ( "+DBHelper.THREAD_ID +","
+DBHelper.URL +","
+DBHelper.START +","
+DBHelper.END +","
+DBHelper.FINISHED +","
+DBHelper.MD5 +","
+DBHelper.OVER +","
+DBHelper.OVER_TIME +")"
+ " values(?,?,?,?,?,?,?,?)",
new Object[] {threadInfo.getId(), threadInfo.getUrl(),
threadInfo.getStart(), threadInfo.getEnd(),
threadInfo.getFinished(),threadInfo.getMd5(),
threadInfo.getOver() ,threadInfo.getOvertime() } // 插入数据
);
mDBHelper.closeDatabase(); // 关闭数据库
}
/**
* 【删除文件下载信息】
*/
@Override
public synchronized void deleteThread(String url) {
SQLiteDatabase db = mDBHelper.openDatabase();
db.execSQL( "delete from thread_info where url = ?",
new Object[] {url});
mDBHelper.closeDatabase();
}
/**
* 【更新下载文件下载进度】
*/
@Override
public synchronized void updateThread(String url, int thread_id, int finished, String md5, String over, String over_time) {
SQLiteDatabase db = mDBHelper.openDatabase();
db.execSQL( "update " +DBHelper.TABLE_NAME + " set "
+DBHelper.FINISHED+" = ?where " +DBHelper.URL +" =? and "
+DBHelper.THREAD_ID +" =?",
new Object[] {finished, url, thread_id});
db.execSQL( "update " +DBHelper.TABLE_NAME + " set "
+DBHelper.OVER+" = ?where " +DBHelper.URL +" =? and "
+DBHelper.THREAD_ID +" =?",
new Object[] {over, url, thread_id});
db.execSQL("update " + DBHelper.TABLE_NAME + " set "
+ DBHelper.OVER_TIME + " = ?where " + DBHelper.URL + " =? and "
+ DBHelper.THREAD_ID + " =?",
new Object[]{over_time, url, thread_id});
mDBHelper.closeDatabase();
}
/**
* 获取文件的文件下载信息
* @return List<ThreadInfo> 包含下载线程信息的list集
* @see com.download.db.ThreadDAO#getThreads(java.lang.String)
*/
@Override
public List<ThreadInfo> getThreads(String url) {
SQLiteDatabase db = mDBHelper.getReadableDatabase(); //注意,此处用【只读】模式
List<ThreadInfo> list = new ArrayList<ThreadInfo>();
Cursor cursor = db.rawQuery("select * from " +DBHelper.TABLE_NAME
+ " where " +DBHelper.URL +" =?", new String[]{url} );
while (cursor.moveToNext()) {
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.setId(cursor.getInt(cursor.getColumnIndex(DBHelper.THREAD_ID)));
threadInfo.setUrl(cursor.getString(cursor.getColumnIndex(DBHelper.URL)));
threadInfo.setStart(cursor.getInt(cursor.getColumnIndex(DBHelper.START)));
threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex(DBHelper.END)));
threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex(DBHelper.FINISHED)));
threadInfo.setMd5(cursor.getString(cursor.getColumnIndex(DBHelper.MD5)));
threadInfo.setOver(cursor.getString(cursor.getColumnIndex(DBHelper.OVER)));
list.add(threadInfo);
}
cursor.close();
db.close();
return list;
}
/**
* 获取数据库中所有下载文件的信息
* @return List<ThreadInfo> 包含下载线程信息的list集
*/
@Override
public List<FileInfo> getDBFileInfoList() {
URLTools urlTools = new URLTools();
List<FileInfo> list = new ArrayList<FileInfo>();
SQLiteDatabase db = mDBHelper.getReadableDatabase(); //注意,此处用【只读】模式
//查询数据库文件下载信息
Cursor cursor = db.rawQuery("select * from " +DBHelper.TABLE_NAME, null );
while( cursor.moveToNext() ){
FileInfo fileInfo = new FileInfo();
if( fileInfo.getId()==0 ){
long finish = 0; //文件已下载的字节数
fileInfo.setOver( cursor.getString(cursor.getColumnIndex(DBHelper.OVER)) );
fileInfo.setOvertime( cursor.getString(cursor.getColumnIndex(DBHelper.OVER_TIME)) );
fileInfo.setMd5( cursor.getString(cursor.getColumnIndex(DBHelper.MD5)) );
fileInfo.setUrl( cursor.getString(cursor.getColumnIndex(DBHelper.URL)) );
String fileName = urlTools.getURLFileName( fileInfo.getUrl(), "/" );
fileInfo.setFileName( fileName );
fileInfo.setIsDownload("false");
//获取百分比下载进度
for (int i = 0; i < DownloadConfig.DONWNLOAD_THREAD_NUM; i++) {
finish += cursor.getInt( cursor.getColumnIndex(DBHelper.FINISHED) );
if ( i < DownloadConfig.DONWNLOAD_THREAD_NUM -1 ){
cursor.moveToPosition( cursor.getPosition() +1 );
}
}
long finished = finish*100/( cursor.getInt(cursor.getColumnIndex(DBHelper.END)) );
fileInfo.setFinished( (int)finished);
list.add(fileInfo);
}
}
cursor.close();
db.close();
return list;
}
/**
* 文件下载是否已完成
* @return 当存在下载信息返回true;否则返回false
*/
@Override
public boolean isDownloadOver(String url) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from " +DBHelper.TABLE_NAME
+" where " +DBHelper.URL +" =? and " +DBHelper.THREAD_ID +" = ?",
new String[]{url, 1+"" } );
boolean over = false;
if (cursor.moveToNext()) {
String overStr = cursor.getString(cursor.getColumnIndex(DBHelper.OVER)) ;
try {
String name = URLDecoder.decode(cursor.getString(cursor.getColumnIndex(DBHelper.URL)), "UTF-8") ;
if (overStr.equals("true")) {
over = true ;
Log.i("db", name+"文件是否下载完成:"+ over );
}else {
over = false ;
Log.i("db",name+ "文件是否下载完成:"+ over );
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
cursor.close();
db.close();
return over;
}