文件的断点续传下载在项目中或多或少的会碰到,要实习该效果,先看看会涉及到哪些东西:
1、如果要显示下载进度的话,就要自定义下载进度效果,自定义view是回涉及到的
2、需要将下载的文件存储在sd卡中,android6.0后,对于sd卡的读写权限做了更改,需要动态去申请该权限,涉及到权限的适配
3、下载或暂停时需要将下载的进度存储在数据库中,下次继承下载时先要要从数据库中获取上次下载的进度,从上次的下载进度继续下载,涉及到数据库的增删改查
4、activity和service之间的数据传递和通信,会涉及到BroadcastReceiver广播、Handler等使用
5、如果需要在通知栏显示下载进度等信息,还需要做好android8.0 通知状态栏的适配,该效果没有实现这一点,就没有做这方面的适配
自定义进度条、android6.0权限适配、数据库的增删改查封装在这里就不多说了,可以参考下面的播客:
android酷炫下载进度条
关于android6.0的权限申请
Android数据库面向对象之增、删、改、查
整体思路:
在用户点击开始下载时,会去做权限的申请,如果用户赋予权限会开启一个Serivce,整个下载都是在Service中进行,在Service的onStartCommand()方法中会去开启一个线程通过网络请求获取到文件的大小,然后会在本地创建一个和文件大小的文件夹,用于文件的存储,再通过Handler消息通知去开启一个线程进行文件的下载任务,在开始下载任务后会先在数据库中查询,如果没有任何下载信息会实例化一个新的下载对象,如果有就根据之前的下载进度进行下载,在下载时会通过BroadcastReceiver将下载进度传递给activity去更新下载进度条,如果用户点击了暂停会将当前的下载进度存入数据库中,便于下次下载时获取上次的下载进度,下载完毕后会删除数据库中的下载记录,就根据上面的思路用代码来实现吧。
用户点击开始下载时会调用btnStart()点击事件的方法,申请权限,也会将下载信息的实例对象传递给Service;
public void btnStart(View view) {
//权限申请
PermissionHelper.with(MainActivity.this).
requestPermission(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE}).
requestCode(WRITE_EXTERNAL_REQUEST_DODE).
request();
}
@PermissionSuccess(requestCode =WRITE_EXTERNAL_REQUEST_DODE)
private void doSuccess(){
//通过intent传递参数给service
Intent intent = new Intent(this, DownLoadService.class);
intent.setAction(DownLoadService.ACTION_START);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
@PermissionFail(requestCode =WRITE_EXTERNAL_REQUEST_DODE)
private void doFail(){
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionHelper.requestPermissionsResult(this,requestCode,permissions,grantResults);
}
如果用户点击了暂停会调用btnStop()点击事件方法,同样的也会将下载信息实例对象传递给Service;
public void btnStop(View view) {
//通过intent传递参数给service
Intent intent = new Intent(this, DownLoadService.class);
intent.setAction(DownLoadService.ACTION_STOP);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
开启Service后,在Service中的onStartCommand()方法中获取对应的下载信息实例对象,如果是开始下载就去创建一个线程通过网络请求获取文件的大小,如果是暂停下载的话,就调用DownLoadTask下载任务类中的isPause,将其改为true;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return super.onStartCommand(intent, flags, startId);
}
//获取activity传递过来的参数
if (ACTION_START.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
Log.e(TAG, ACTION_START);
//开启线程
InitThread thread = new InitThread(fileInfo);
thread.start();
} else if (ACTION_STOP.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
Log.e(TAG, ACTION_STOP);
if (mTask != null) {
//暂停下载
mTask.isPause = true;
}
}
return super.onStartCommand(intent, flags, startId);
}
在InitThread这个线程中去获取文件的大小,同时创建一个和文件大小的文件用于存储下载好的文件,再通过handler消息通知去下载该文件;
class InitThread extends Thread {
private FileInfo mFileInfo;
public InitThread(FileInfo fileInfo) {
this.mFileInfo = fileInfo;
}
@Override
public void run() {
super.run();
HttpURLConnection conn = null;
RandomAccessFile faf = null;
int length = -1;
try {
//连接网络文件,
URL url = new URL(mFileInfo.url);
conn = (HttpURLConnection) url.openConnection();
//设置链接超时
conn.setConnectTimeout(6000);
//设置请求方式
conn.setRequestMethod("GET");
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
//获得文件的大小
length = conn.getContentLength();
}
if (length < 0) {
return;
}
Log.e(TAG, length + "");
File dir = new File(DOWNLOAD_PATH);
if (!dir.exists()) {
dir.mkdir();
}
//在本地创建文件
File file = new File(dir, mFileInfo.fileName);
faf = new RandomAccessFile(file, "rwd");
//设置文件的长度
faf.setLength(length);
mFileInfo.length = length;
//通过handler发送消息进行文件的下载
Message message = mHandler.obtainMessage(MSG_INIT, mFileInfo);
mHandler.sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.disconnect();
}
if (faf != null) {
faf.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
这个时候就可以在Handler的handlerMessage()回调方法中根据msg.what去实例化一个DownLoadTask对象并调用downLoad()方法进行下载文件;
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_INIT:
FileInfo fileInfo = (FileInfo) msg.obj;
Log.e(TAG, MSG_INIT + "");
//启动下载任务
mTask = new DownLoadTask(DownLoadService.this, fileInfo);
mTask.downLoad();
break;
}
}
};
当然了,在DownLoadTask的构造方法中会先去创建数据库实例;
public DownLoadTask(Context context, FileInfo fileInfo) {
mContext = context;
mFileInfo = fileInfo;
//创建数据库实例
dao = new ThreadDAOImpl(context);
}
这里就没有对数据库的创建、增、删、改、查进行封装了,如果想封装可以参考上面的博客,不过实现思路还是差不多的,还是继承自系统的SQLiteOpenHelper类,在onCreate()方法和onUpgrade()方法中进行数据库的创建和更新;
public class DBHelper extends SQLiteOpenHelper {
private static volatile DBHelper dbHelper;
//创建表语法
private static final String SQL_CREATE = "create table " + DBConstant.TABLE_NAME + "(_id integer primary key autoincrement," +
DBConstant.THREAD_ID + " integer," + DBConstant.URL + " text," + DBConstant.THREAD_START + " integer," + DBConstant.THREAD_END + " integer," + DBConstant.FINISHED + " integer)";
//删除表语法
private static final String SQL_DROP = "drop table if exists " + DBConstant.TABLE_NAME;
public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
private DBHelper(Context context) {
super(context, DBConstant.DB_NAME, null, DBConstant.VERSION);
}
public static DBHelper getInstance(Context context) {
if (dbHelper == null) {
synchronized (DBHelper.class) {
if (dbHelper == null) {
dbHelper = new DBHelper(context.getApplicationContext());
}
}
}
return dbHelper;
}
@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 {
/**
* 插入线程信息
*
* @param threadInfo
*/
void insertThread(ThreadInfo threadInfo);
/**
* 删除信息
*
* @param url
* @param threadId
*/
void deleteThread(String url, int threadId);
/**
* 更新信息
*
* @param url
* @param threadId
*/
void updateThread(String url, int threadId, int finished);
/**
* 查询信息
*
* @param url
* @return
*/
List<ThreadInfo> queryThread(String url);
/**
* 判断线程信息是否存在
*
* @param url
* @param threadId
* @return
*/
boolean isExists(String url, int threadId);
}
public class ThreadDAOImpl implements ThreadDAO {
private DBHelper dbHelper = null;
public ThreadDAOImpl(Context context) {
dbHelper = DBHelper.getInstance(context);
}
@Override
public void insertThread(ThreadInfo threadInfo) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.execSQL("insert into " + DBConstant.TABLE_NAME + "(thread_id,url,thread_start,thread_end,finished) values(?,?,?,?,?)",
new Object[]{threadInfo.id, threadInfo.url, threadInfo.thread_start, threadInfo.thread_end, threadInfo.finished});
db.close();
}
@Override
public void deleteThread(String url, int threadId) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.execSQL("delete from " + DBConstant.TABLE_NAME + " where url = ? and thread_id = ?",
new Object[]{url, threadId});
db.close();
}
@Override
public void updateThread(String url, int threadId, int finished) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.execSQL("update " + DBConstant.TABLE_NAME + " set finished = ? where url = ? and thread_id = ?",
new Object[]{finished, url, threadId});
db.close();
}
@Override
public List<ThreadInfo> queryThread(String url) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from " + DBConstant.TABLE_NAME + " where url = ?", new String[]{url});
List<ThreadInfo> list = new ArrayList<>();
while (cursor.moveToNext()) {
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.id = cursor.getInt(cursor.getColumnIndex("thread_id"));
threadInfo.url = cursor.getString(cursor.getColumnIndex("url"));
threadInfo.thread_start = cursor.getInt(cursor.getColumnIndex("thread_start"));
threadInfo.thread_end = cursor.getInt(cursor.getColumnIndex("thread_end"));
threadInfo.finished = cursor.getInt(cursor.getColumnIndex("finished"));
list.add(threadInfo);
}
cursor.close();
db.close();
return list;
}
@Override
public boolean isExists(String url, int threadId) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from " + DBConstant.TABLE_NAME + " where url = ? and thread_id = ?", new String[]{url, threadId + ""});
boolean isExits = cursor.moveToNext();
cursor.close();
db.close();
return isExits;
}
}
数据库实例创建好后,就先去数据库中查询是否有下载记录,如果有就接着上次的记录下载,如果没有就创建一个新的下载记录,再创建一个线程用于文件的下载;
/**
* 开始文件的下载
*/
public void downLoad() {
//读取数据库的线程信息
List<ThreadInfo> threadInfos = dao.queryThread(mFileInfo.url);
ThreadInfo threadInfo = null;
if (threadInfos.size() == 0) {
//如果数据库中没有就根据下载文件信息创建一个新的下载信息实例对象
threadInfo = new ThreadInfo();
threadInfo.id = 0;
threadInfo.url = mFileInfo.url;
threadInfo.thread_start = 0;
threadInfo.thread_end = mFileInfo.length;
threadInfo.finished = 0;
} else {
//获取数据库返回的下载信息实例对象
threadInfo = threadInfos.get(0);
}
//创建子线程下载
DownLoadThread downLoadThread = new DownLoadThread(threadInfo);
downLoadThread.start();
}
在DownLoadThread线程中就可以进行文件的下载、暂停后下载记录的更新、利用广播进行下载进度的传递和更新等;
/**
* 下载线程
*/
class DownLoadThread extends Thread {
private ThreadInfo mThreadInfo;
public DownLoadThread(ThreadInfo threadInfo) {
mThreadInfo = threadInfo;
}
@Override
public void run() {
super.run();
//向数据库中插入一条信息
if (!dao.isExists(mThreadInfo.url, mThreadInfo.id)) {
//插入一条新的下载记录信息
dao.insertThread(mThreadInfo);
}
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream input = null;
try {
URL url = new URL(mThreadInfo.url);
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(6000);
conn.setRequestMethod("GET");
//设置下载位置
int start = mThreadInfo.thread_start + mThreadInfo.finished;
conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.thread_end);
//设置一个文件写入位置
File file = new File(DownLoadService.DOWNLOAD_PATH, mFileInfo.fileName);
raf = new RandomAccessFile(file, "rwd");
//设置文件写入位置
raf.seek(start);
Intent intent = new Intent(DownLoadService.ACTION_UPDATE);
mFinished += mThreadInfo.finished;
//开始下载了
if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
//读取数据
input = conn.getInputStream();
byte[] buffer = new byte[1024 * 4];
int len = -1;
while ((len = input.read(buffer)) != -1) {
//写入文件
raf.write(buffer, 0, len);
//下载进度发送广播给activity
mFinished += len;
Log.e("DownLoadService",mFinished+"");
intent.putExtra("finished", mFinished * 100 / mFileInfo.length);
mContext.sendBroadcast(intent);
//下载暂停是要把进度保存在数据库中
if (isPause) {
//暂停时更新数据库中的下载信息
dao.updateThread(mThreadInfo.url, mThreadInfo.id, mFinished);
return;
}
}
//删除线程信息
dao.deleteThread(mThreadInfo.url, mThreadInfo.id);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.disconnect();
}
if (raf != null) {
raf.close();
}
if (input != null) {
input.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
下载进度通过BroadcastReceiver传递后,需要在activity中注册一个广播,在BroadcastReceiver的onReceive()方法中进行进度条的更新,需要注意在activity的onDestory方法中将注册的广播注销掉;
//注册广播接收器
IntentFilter filter = new IntentFilter();
filter.addAction(DownLoadService.ACTION_UPDATE);
registerReceiver(mBroadcastReceiver, filter);
广播的注册是在activity的onCreate方法中注册的;
/**
* 更新ui的广播接收器
*/
BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == DownLoadService.ACTION_UPDATE) {
int finished = intent.getIntExtra("finished", 0);
//设置下载进度
progressBar.setProgress(finished);
int progress = progressBar.getProgress();
if (progress == progressBar.getMax()) {
Toast.makeText(MainActivity.this, "下载完毕", Toast.LENGTH_LONG).show();
}
}
}
};
@Override
protected void onDestroy() {
super.onDestroy();
//注销广播
unregisterReceiver(mBroadcastReceiver);
}
这样一个简单的文件断点续传就实现了。