
文件的斷點續(xù)傳下載在項目中或多或少的會碰到,要實習(xí)該效果,先看看會涉及到哪些東西:
1、如果要顯示下載進度的話,就要自定義下載進度效果,自定義view是回涉及到的
2、需要將下載的文件存儲在sd卡中,android6.0后,對于sd卡的讀寫權(quán)限做了更改,需要動態(tài)去申請該權(quán)限,涉及到權(quán)限的適配
3、下載或暫停時需要將下載的進度存儲在數(shù)據(jù)庫中,下次繼承下載時先要要從數(shù)據(jù)庫中獲取上次下載的進度,從上次的下載進度繼續(xù)下載,涉及到數(shù)據(jù)庫的增刪改查
4、activity和service之間的數(shù)據(jù)傳遞和通信,會涉及到BroadcastReceiver廣播、Handler等使用
5、如果需要在通知欄顯示下載進度等信息,還需要做好android8.0 通知狀態(tài)欄的適配,該效果沒有實現(xiàn)這一點,就沒有做這方面的適配
自定義進度條、android6.0權(quán)限適配、數(shù)據(jù)庫的增刪改查封裝在這里就不多說了,可以參考下面的播客:
android酷炫下載進度條
關(guān)于android6.0的權(quán)限申請
Android數(shù)據(jù)庫面向?qū)ο笾?、刪、改、查
整體思路:
在用戶點擊開始下載時,會去做權(quán)限的申請,如果用戶賦予權(quán)限會開啟一個Serivce,整個下載都是在Service中進行,在Service的onStartCommand()方法中會去開啟一個線程通過網(wǎng)絡(luò)請求獲取到文件的大小,然后會在本地創(chuàng)建一個和文件大小的文件夾,用于文件的存儲,再通過Handler消息通知去開啟一個線程進行文件的下載任務(wù),在開始下載任務(wù)后會先在數(shù)據(jù)庫中查詢,如果沒有任何下載信息會實例化一個新的下載對象,如果有就根據(jù)之前的下載進度進行下載,在下載時會通過BroadcastReceiver將下載進度傳遞給activity去更新下載進度條,如果用戶點擊了暫停會將當(dāng)前的下載進度存入數(shù)據(jù)庫中,便于下次下載時獲取上次的下載進度,下載完畢后會刪除數(shù)據(jù)庫中的下載記錄,就根據(jù)上面的思路用代碼來實現(xiàn)吧。
用戶點擊開始下載時會調(diào)用btnStart()點擊事件的方法,申請權(quán)限,也會將下載信息的實例對象傳遞給Service;
public void btnStart(View view) {
//權(quán)限申請
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傳遞參數(shù)給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);
}
如果用戶點擊了暫停會調(diào)用btnStop()點擊事件方法,同樣的也會將下載信息實例對象傳遞給Service;
public void btnStop(View view) {
//通過intent傳遞參數(shù)給service
Intent intent = new Intent(this, DownLoadService.class);
intent.setAction(DownLoadService.ACTION_STOP);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
開啟Service后,在Service中的onStartCommand()方法中獲取對應(yīng)的下載信息實例對象,如果是開始下載就去創(chuàng)建一個線程通過網(wǎng)絡(luò)請求獲取文件的大小,如果是暫停下載的話,就調(diào)用DownLoadTask下載任務(wù)類中的isPause,將其改為true;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return super.onStartCommand(intent, flags, startId);
}
//獲取activity傳遞過來的參數(shù)
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這個線程中去獲取文件的大小,同時創(chuàng)建一個和文件大小的文件用于存儲下載好的文件,再通過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 {
//連接網(wǎng)絡(luò)文件,
URL url = new URL(mFileInfo.url);
conn = (HttpURLConnection) url.openConnection();
//設(shè)置鏈接超時
conn.setConnectTimeout(6000);
//設(shè)置請求方式
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();
}
//在本地創(chuàng)建文件
File file = new File(dir, mFileInfo.fileName);
faf = new RandomAccessFile(file, "rwd");
//設(shè)置文件的長度
faf.setLength(length);
mFileInfo.length = length;
//通過handler發(fā)送消息進行文件的下載
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()回調(diào)方法中根據(jù)msg.what去實例化一個DownLoadTask對象并調(diào)用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 + "");
//啟動下載任務(wù)
mTask = new DownLoadTask(DownLoadService.this, fileInfo);
mTask.downLoad();
break;
}
}
};
當(dāng)然了,在DownLoadTask的構(gòu)造方法中會先去創(chuàng)建數(shù)據(jù)庫實例;
public DownLoadTask(Context context, FileInfo fileInfo) {
mContext = context;
mFileInfo = fileInfo;
//創(chuàng)建數(shù)據(jù)庫實例
dao = new ThreadDAOImpl(context);
}
這里就沒有對數(shù)據(jù)庫的創(chuàng)建、增、刪、改、查進行封裝了,如果想封裝可以參考上面的博客,不過實現(xiàn)思路還是差不多的,還是繼承自系統(tǒng)的SQLiteOpenHelper類,在onCreate()方法和onUpgrade()方法中進行數(shù)據(jù)庫的創(chuàng)建和更新;
public class DBHelper extends SQLiteOpenHelper {
private static volatile DBHelper dbHelper;
//創(chuàng)建表語法
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);
//再創(chuàng)建
db.execSQL(SQL_CREATE);
}
}
在對數(shù)據(jù)庫進行增刪改查操作是還是先定義一個接口,該接口提供了增刪改查的方法,具體的在實現(xiàn)類中進行邏輯處理;
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;
}
}
數(shù)據(jù)庫實例創(chuàng)建好后,就先去數(shù)據(jù)庫中查詢是否有下載記錄,如果有就接著上次的記錄下載,如果沒有就創(chuàng)建一個新的下載記錄,再創(chuàng)建一個線程用于文件的下載;
/**
* 開始文件的下載
*/
public void downLoad() {
//讀取數(shù)據(jù)庫的線程信息
List<ThreadInfo> threadInfos = dao.queryThread(mFileInfo.url);
ThreadInfo threadInfo = null;
if (threadInfos.size() == 0) {
//如果數(shù)據(jù)庫中沒有就根據(jù)下載文件信息創(chuàng)建一個新的下載信息實例對象
threadInfo = new ThreadInfo();
threadInfo.id = 0;
threadInfo.url = mFileInfo.url;
threadInfo.thread_start = 0;
threadInfo.thread_end = mFileInfo.length;
threadInfo.finished = 0;
} else {
//獲取數(shù)據(jù)庫返回的下載信息實例對象
threadInfo = threadInfos.get(0);
}
//創(chuàng)建子線程下載
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();
//向數(shù)據(jù)庫中插入一條信息
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");
//設(shè)置下載位置
int start = mThreadInfo.thread_start + mThreadInfo.finished;
conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.thread_end);
//設(shè)置一個文件寫入位置
File file = new File(DownLoadService.DOWNLOAD_PATH, mFileInfo.fileName);
raf = new RandomAccessFile(file, "rwd");
//設(shè)置文件寫入位置
raf.seek(start);
Intent intent = new Intent(DownLoadService.ACTION_UPDATE);
mFinished += mThreadInfo.finished;
//開始下載了
if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
//讀取數(shù)據(jù)
input = conn.getInputStream();
byte[] buffer = new byte[1024 * 4];
int len = -1;
while ((len = input.read(buffer)) != -1) {
//寫入文件
raf.write(buffer, 0, len);
//下載進度發(fā)送廣播給activity
mFinished += len;
Log.e("DownLoadService",mFinished+"");
intent.putExtra("finished", mFinished * 100 / mFileInfo.length);
mContext.sendBroadcast(intent);
//下載暫停是要把進度保存在數(shù)據(jù)庫中
if (isPause) {
//暫停時更新數(shù)據(jù)庫中的下載信息
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);
//設(shè)置下載進度
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);
}
這樣一個簡單的文件斷點續(xù)傳就實現(xiàn)了。