效果图
首先先上一下效果图 如果有需要的朋友可以往下看我的具体实现:
整个下载的过程都在后台服务中进行,因此实现了一个activity将任务加入下载 在另一个activity中显示进度并能控制暂停,同时结合了sqlite进行缓存,将任务列表缓存到本地,下次打开app任务依然存在并能继续
实现的思路
1.创建一个sqlite的表 将任务列表中的 例如文件名 url 等信息进行缓存
2. 封装下载工具类
3. 创建后台服务 将下载的开始暂停等方法写到服务中
代码具体实现
一 创建DBHelper 并封装一些方法
public class DBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "download.db";
private static final int VERSION = 1;
//state 1 是正在下载 2 是暂停 3是已经完成
//thread_id(与文件id相同) 下载url 文件总长度 下载状态 title文件标题
private static final String SQL_CREATE = "create table if not exists thread_info(" +
"thread_id integer,url text,filelength integer,state integer,title text)";
private static final String SQL_DROP = "drop table if exists thread_info";
public DBHelper(Context context) {
super(context, DB_NAME, null, VERSION);
}
@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);
}
}
封装增删改查的方法
public interface ThreadDao {
//插入线程信息
void insertThread(ThreadInfo threadInfo);
//删除线程
void deleteThread(String url, int thread_id);
//更新线程信息
void updateThread(String url, int thread_id,int state);
//根据url查询文件的线程信息
ThreadInfo getThreadsByUrl(String url);
List<ThreadInfo> getAllThreads();
//系统被杀死的时候没有完成的任务
List<ThreadInfo> getAllContinueThreads();
//线程信息是否存在
boolean isExists(String url, int thread_id);
}
具体的sql 语句实现
public class ThreadDaoImpl implements ThreadDao {
private DBHelper mHelper = null;
public ThreadDaoImpl(Context context) {
mHelper = new DBHelper(context);
}
@Override
public synchronized void insertThread(ThreadInfo threadInfo) {
SQLiteDatabase db = mHelper.getWritableDatabase();
db.execSQL("insert into thread_info(thread_id,url,filelength,state,title) values (?,?,?,?,?)",
new Object[]{threadInfo.getId(), threadInfo.getUrl(), threadInfo.getFilelength(), threadInfo.getState(), threadInfo.getTitle()});
db.close();
}
@Override
public synchronized void deleteThread(String url, int thread_id) {
SQLiteDatabase db = mHelper.getWritableDatabase();
db.execSQL("delete from thread_info where thread_id = ? and url = ?",
new Object[]{thread_id, url});
db.close();
}
@Override
public synchronized void updateThread(String url, int thread_id, int state) {
SQLiteDatabase db = mHelper.getWritableDatabase();
db.execSQL("update thread_info set state = ? where thread_id = ? and url = ?",
new Object[]{state,thread_id, url});
db.close();
}
@Override
public synchronized ThreadInfo getThreadsByUrl(String url) {
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info where url = ?", new String[]{url});
ThreadInfo threadInfo = null;
while (cursor.moveToNext()) {
threadInfo = new ThreadInfo();
threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
threadInfo.setFilelength(cursor.getInt(cursor.getColumnIndex("filelength")));
threadInfo.setState(cursor.getInt(cursor.getColumnIndex("state")));
threadInfo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
}
cursor.close();
db.close();
return threadInfo;
}
@Override
public List<ThreadInfo> getAllThreads() {
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info", new String[]{});
ThreadInfo threadInfo = null;
List<ThreadInfo> list = new ArrayList<>();
while (cursor.moveToNext()) {
threadInfo = new ThreadInfo();
threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
threadInfo.setFilelength(cursor.getInt(cursor.getColumnIndex("filelength")));
threadInfo.setState(cursor.getInt(cursor.getColumnIndex("state")));
threadInfo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
list.add(threadInfo);
}
cursor.close();
db.close();
return list;
}
@Override
public List<ThreadInfo> getAllContinueThreads() {
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info where state = ?", new String[]{"1"});
ThreadInfo threadInfo = null;
List<ThreadInfo> list = new ArrayList<>();
while (cursor.moveToNext()) {
threadInfo = new ThreadInfo();
threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
threadInfo.setFilelength(cursor.getInt(cursor.getColumnIndex("filelength")));
threadInfo.setState(cursor.getInt(cursor.getColumnIndex("state")));
threadInfo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
list.add(threadInfo);
}
cursor.close();
db.close();
return list;
}
@Override
public synchronized boolean isExists(String url, int thread_id) {
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info where url = ? and thread_id = ?",
new String[]{url, thread_id + ""});
while (cursor.moveToNext()) {
cursor.close();
db.close();
return true;
}
return false;
}
}
二 创建实体类
我们需要两个实体类 第一个实体类是做文件列表假数据用的 不需要state(是暂停还是正在下载) 第二个实体类是线程下载用的
文件的实体类
public class FileInfo implements Serializable {
private int id;
private String url;
private String fileName;
private int length;
public FileInfo() {
}
public FileInfo(int id, String url, String fileName, int length) {
this.id = id;
this.url = url;
this.fileName = fileName;
this.length = length;
}
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 class ThreadInfo {
private int id;
private String url;
private long filelength;
private int state;
private String title;
public ThreadInfo() {
}
public ThreadInfo(int id, String url, long filelength, int state, String title) {
this.id = id;
this.url = url;
this.filelength = filelength;
this.state = state;
this.title = title;
}
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 long getFilelength() {
return filelength;
}
public void setFilelength(long filelength) {
this.filelength = filelength;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
三 创建下载工具类
下面我们创建一个下载的工具类 DownloadTask 断点下载的核心就是RandomAccessFile 这个类 通过seek() 这个方法能跳过已经下载的部分 另一个重点就是在请求头中加上range 下面看一下具体的代码
//下载任务类
public class DownloadTask {
private Context context = null;
private FileInfo fileInfo = null;
private ThreadDao dao = null;
public boolean isPause = false;
public DownloadTask(Context context, FileInfo fileInfo) {
this.context = context;
this.fileInfo = fileInfo;
dao = new ThreadDaoImpl(context);
}
public void download() {
//读取数据库的线程任务信息
ThreadInfo threadInfo = dao.getThreadsByUrl(fileInfo.getUrl());
if (threadInfo == null) {
//如果是空的初始化线程任务对象
threadInfo = new ThreadInfo(fileInfo.getId(), fileInfo.getUrl(), fileInfo.getLength(), 1, fileInfo.getFileName());
}
//创建子线程开始下载
new DownloadThread(threadInfo).start();
}
//下载线程
class DownloadThread extends Thread {
ThreadInfo threadInfo = null;
private File file;
public DownloadThread(ThreadInfo threadInfo) {
this.threadInfo = threadInfo;
}
@Override
public void run() {
super.run();
//向数据库插入线程任务信息
if (!dao.isExists(threadInfo.getUrl(), threadInfo.getId())) {
//之前不存在这个线程任务就插入到数据库
dao.insertThread(threadInfo);
}
HttpURLConnection httpURLConnection = null;
RandomAccessFile raf = null;
InputStream inputStream = null;
try {
Intent intent = new Intent(DownloadService.ACTION_UPDATA);
//设置文件写入位置 同时判断手机中是否已经有下载的相同文件
file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName());
if (file.length() == fileInfo.getLength()){
//已经有这个文件了
//下载完成之后发送广播
intent.putExtra("finished", 100 + "");
intent.putExtra("fileId", fileInfo.getId());
context.sendBroadcast(intent);
//下载完成之后更新线程任务信息
dao.updateThread(threadInfo.getUrl(), threadInfo.getId(), 3);
return;
}
raf = new RandomAccessFile(file, "rwd");
raf.seek(file.length());
//开始下载
URL url = new URL(threadInfo.getUrl());
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(3000);
httpURLConnection.setRequestMethod("GET");
httpURLConnection.setRequestProperty("RANGE", "bytes=" + file.length() + "-" );
inputStream = httpURLConnection.getInputStream();
byte[] bytes = new byte[1024 * 10];
int len;
long time = System.currentTimeMillis();
while ((len = inputStream.read(bytes)) != -1) {
//写入文件
raf.write(bytes, 0, len);
//下载进度发送广播给activity
if (System.currentTimeMillis() - time > 300) {
time = System.currentTimeMillis();
intent.putExtra("finished", file.length() * 100 / fileInfo.getLength() + "");
intent.putExtra("fileId", fileInfo.getId());
context.sendBroadcast(intent);
}
//下载暂停保存下载进度
if (isPause) {
dao.updateThread(threadInfo.getUrl(), threadInfo.getId() , 2);
return;
}
}
//下载完成之后发送广播
intent.putExtra("finished", 100 + "");
intent.putExtra("fileId", fileInfo.getId());
context.sendBroadcast(intent);
//下载完成之后更新线程任务信息
dao.updateThread(threadInfo.getUrl(), threadInfo.getId(), 3);
} catch (Exception e) {
e.printStackTrace();
isPause = true;
dao.updateThread(threadInfo.getUrl(), threadInfo.getId(), 2);
Intent intent = new Intent(DownloadService.ACTION_ERRO);
intent.putExtra("fileId", fileInfo.getId());
context.sendBroadcast(intent);
} finally {
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
在这个类的构造方法中需要传FileInfo,因为我们在往sql表中插入数据的时候需要从FileInfo中拿到一些属性进行插入 或者查询 下面将会在服务中进行传入
四 创建后台服务
在后台服务中 我们控制DownloadTask的开始和暂停 将每一个任务存在一个map中,同事在这个服务中 我们同时加入了一个请求下载文件的长度
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_UPDATA = "ACTION_UPDATA";
public static final String ACTION_ERRO = "ACTION_ERRO";
public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/ADEMO";
public static final int MSG_INIT = 0;
private DownloadTask downloadTask;
//下载任务的集合
public static Map<Integer,DownloadTask> mTasks = new LinkedHashMap<>();
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INIT:
FileInfo fileInfo = (FileInfo) msg.obj;
Log.d("DownloadService", "MSG_INIT:" + fileInfo);
//启动下载任务
downloadTask = new DownloadTask(DownloadService.this,fileInfo);
downloadTask.download();
mTasks.put(fileInfo.getId(),downloadTask);
break;
}
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//获得Activity的参数
if (ACTION_START.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
new InitThread(fileInfo).start();
} else if (ACTION_STOP.equals(intent.getAction())){
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
//从集合中取出下载任务
DownloadTask downloadTask = mTasks.get(fileInfo.getId());
if (downloadTask != null){
downloadTask.isPause = true;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
class InitThread extends Thread {
private FileInfo mFileInfo = null;
public InitThread(FileInfo mFileInfo) {
this.mFileInfo = mFileInfo;
}
@Override
public void run() {
HttpURLConnection httpURLConnection = null;
RandomAccessFile raf = null;
try {
URL url = new URL(mFileInfo.getUrl());
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(3000);
httpURLConnection.setRequestMethod("GET");
int length = -1;
if (httpURLConnection.getResponseCode() == 200) {
length = httpURLConnection.getContentLength();
}
if (length <= 0) {
return;
}
File dir = new File(DOWNLOAD_PATH);
if (!dir.exists()) {
dir.mkdir();
}
mFileInfo.setLength(length);
Message message = handler.obtainMessage(MSG_INIT, mFileInfo);
message.sendToTarget();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
主要的核心代码就展示完了 具体的一些细节 我会在文末贴上源码的下载地址
五 一些补充
因为我们是在后台服务中进行下载 如何处理用户手动强行杀死这个进程,当进程杀死的时候也许还有线程正在进行 线程并未执行完毕,因此 我们可以通过我们sql中的一个属性state进行判断 首先判断服务中的Map是否为空,如果为空的话再去sql中查询哪些是state为正在下载的任务,那么我们将这些任务继续下载
//强行杀死进程重启之后筛选未完成的任务继续下载
if (DownloadService.mTasks.size() == 0){
List<ThreadInfo> allContinueThreads = dao.getAllContinueThreads();
for (ThreadInfo allContinueThread : allContinueThreads) {
Intent intent = new Intent(this, DownloadService.class);
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(allContinueThread.getTitle());
fileInfo.setId(allContinueThread.getId());
fileInfo.setUrl(allContinueThread.getUrl());
intent.putExtra("fileInfo", fileInfo);
intent.setAction(DownloadService.ACTION_START);
startService(intent);
}
}
上面的具体效果为下面的效果图