上篇博客寫的是單個文件單個線程的斷點續(xù)傳下載(文件斷點續(xù)傳下載(一))侨颈,這里是在之前的基礎(chǔ)上進行了擴展,變成了多文件芯义,多線程同時下載/暫停哈垢,同時添加了通知欄下載狀態(tài)顯示,雖然代碼是基于之前的扛拨,還是做了不少改動耘分。
1、activity中顯示的是一個列表绑警,點擊事件的觸發(fā)是在列表的條目中求泰,就需要在點擊開始時,將點擊事件通過接口回調(diào)到activity中计盒,進行權(quán)限的申請
2渴频、當多任務(wù)、多線程同時操作時北启,利用BroadcastReceiver進行通信會導致界面不有點卡卜朗,就改用了Messenger+Handler進行通信
3拔第、需要對Notification做8.0的適配
4、利用TimerTask聊替、Timer進行消息的輪詢
5楼肪、改用線程池對線程的管理
先看下列表適配器中的邏輯,很簡單,
public class FileListAdapter extends BaseAdapter {
private Context mContext;
private List<FileInfo> list;
public FileListAdapter(Context context, List<FileInfo> list) {
this.mContext = context;
this.list = list;
}
@Override
public int getCount() {
return list == null ? 0 : list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
final FileInfo fileInfo = list.get(position);
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.file_list_adapter_item, parent, false);
viewHolder.tvFileName = convertView.findViewById(R.id.tv_file_name);
viewHolder.progressBar = convertView.findViewById(R.id.progress_bar);
viewHolder.btnStop = convertView.findViewById(R.id.btn_stop);
viewHolder.btnStart = convertView.findViewById(R.id.btn_start);
viewHolder.progressBar.setMax(100);
viewHolder.tvFileName.setText(fileInfo.fileName);
viewHolder.btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.doStart(position, fileInfo);
}
}
});
viewHolder.btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.doStop(position, fileInfo);
}
}
});
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.progressBar.setProgress(fileInfo.finished);
return convertView;
}
/**
* 更新列表項中條目下載進度
*/
public void updateProgress(List<FileInfo> fileInfos) {
this.list = fileInfos;
notifyDataSetChanged();
}
class ViewHolder {
TextView tvFileName;
ProgressBarView progressBar;
Button btnStop, btnStart;
}
private onItemClick listener;
public void setOnItemClick(onItemClick listener) {
this.listener = listener;
}
public interface onItemClick {
/**
* 開始下載
*
* @param position
*/
void doStart(int position, FileInfo fileInfo);
/**
* 暫停下載
*
* @param position
*/
void doStop(int position, FileInfo fileInfo);
}
}
不過有點細節(jié)需要注意的掺冠,賦值一次后不需要變動的數(shù)據(jù)淤井,就不需要數(shù)據(jù)復用了,這樣會提高一些性能铺罢,這里文件名稱的賦值、ProgressBar的初始化、開始/暫停的點擊事件就沒有進行復用当纱,然后在回調(diào)事件中去做權(quán)限的申請、開啟下載/暫停下載的邏輯處理踩窖。
之前是通過startService的方式開啟一個Service的坡氯,這里沒有采用該方式,用的是綁定服務(wù)的方式洋腮,在onCreate()方法中綁定一個Service箫柳;
//綁定service
Intent intent = new Intent(this, DownLoadService.class);
bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE);
Service綁定后,就會回調(diào)ServiceConnection中的onServiceConnected()方法啥供,在該方法中利用Messenger同時在Service的onBind()方法中也利用Messenger進行activity和Service數(shù)據(jù)通信雙向綁定悯恍;
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//綁定回調(diào)
//獲得service中的Messenger
mServiceMessenger = new Messenger(service);
//創(chuàng)建activity中的Messenger
Messenger messenger = new Messenger(mHandler);
//創(chuàng)建一個消息
Message message = new Message();
message.what = DownLoadService.MSG_BIND;
message.replyTo = messenger;
//使用service的messenger發(fā)送activity中的Messenger
try {
mServiceMessenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
//解綁回調(diào)
}
};
@Override
public IBinder onBind(Intent intent) {
//創(chuàng)建一個Messenger對象
Messenger messenger = new Messenger(mHandler);
//返回Messenger的binder
return messenger.getBinder();
}
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_INIT:
mSGINIT(msg);
break;
case MSG_BIND:
//處理綁定的Messenger
mActivityMessenger = msg.replyTo;
break;
case MSG_START:
mSGSTART(msg);
break;
case MSG_STOP:
mSGSTOP(msg);
break;
}
}
};
實現(xiàn)了activity和service的通信綁定,在點擊開始或者暫停時就可以Messenger給service發(fā)送消息了伙狐;
private void doStart(FileInfo fileInfo) {
//將開始下載的文件id存儲在集合中
if (fileIds.contains(fileInfo.id)) {
//正在下載提示用戶 并返回
Toast.makeText(this, "該文件正在下載中...", Toast.LENGTH_LONG).show();
return;
}
//將文件id添加到集合中
fileIds.add(fileInfo.id);
Message message = new Message();
message.what = DownLoadService.MSG_START;
message.obj = fileInfo;
try {
mServiceMessenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
Log.e("doStart-->", "doStart");
}
private void doStop(FileInfo fileInfo) {
//暫停下載時將存儲的下載文件id移除
for (int i = 0; i < fileIds.size(); i++) {
Integer integer = fileIds.get(i);
if (integer == fileInfo.id) {
fileIds.remove(integer);
break;
}
}
Log.e("doStop-->", "doStop");
Message message = new Message();
message.what = DownLoadService.MSG_STOP;
message.obj = fileInfo;
try {
mServiceMessenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
在開始下載時需要將下載文件的id緩存的集合中涮毫,用于管理用戶多次點擊同一個正在下載的文件,在暫停下載或下載完成后在從集合中移除對應(yīng)的文件id贷屎,消息發(fā)送后罢防,在Service中Handler的handerMessage()方法中根據(jù)msg.whate進行處理。
開始下載時還是和之前一樣唉侄,在子線程中獲取文件的大小咒吐,在本地創(chuàng)建文件夾,然后通過handler通知進行下載美旧,只是改成了利用線程池進行線程的管理渤滞;
/**
* 開始下載
*
* @param msg
*/
private void mSGSTART(Message msg) {
FileInfo fileInfo = (FileInfo) msg.obj;
Log.e(TAG, ACTION_START);
//開啟線程
InitThread thread = new InitThread(fileInfo);
DownLoadTask.sExecutorService.execute(thread);
}
在下載的時候需要對多線程、多任務(wù)進行管理榴嗅,同時開始時通過消息告訴activity去開啟消息通知欄妄呕,顯示下載進度;
/**
* 初始化處理
*
* @param msg
*/
private void mSGINIT(Message msg) {
FileInfo fileInfo = (FileInfo) msg.obj;
Log.e(TAG, MSG_INIT + "");
//啟動下載任務(wù)
DownLoadTask task = new DownLoadTask(DownLoadService.this, fileInfo, 1, mActivityMessenger);
task.downLoad();
//把下載任務(wù)添加到集合中
mTasks.put(fileInfo.id, task);
//給Activity發(fā)送消息
Message message = new Message();
message.what = MSG_START;
message.obj = fileInfo;
try {
mActivityMessenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
在activity的handler中就可以通知開啟通知狀態(tài)欄嗽测,
/**
* 開始下載通知通知欄
*
* @param msg
*/
private void mSGSTART(Message msg) {
//顯示一個通知
FileInfo info = (FileInfo) msg.obj;
if (info != null) {
mNotificationUtil.showNotification(info);
}
}
/**
* 通知的工具類
*/
public class NotificationUtil {
//實例化Notification通知管理類
private NotificationManager mNotificationManager;
//管理Notification的集合
private Map<Integer, Notification.Builder> mNotification;
private Context mContext;
public NotificationUtil(Context context) {
mContext = context;
mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
mNotification = new HashMap<>();
}
/**
* 顯示notification
*/
public void showNotification(FileInfo fileInfo) {
//是否存在該文件下載通知
if (!mNotification.containsKey(fileInfo.id)) {
//android8.0 notification適配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT);
channel.canBypassDnd();//可否繞過請勿打擾模式
channel.enableLights(true);//有新消息時手機有閃光
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);//鎖屏顯示通知
channel.setLightColor(Color.argb(100, 100, 100, 100));//指定閃光時的燈光顏色
channel.canShowBadge();//有新消息時桌面角標顯示
channel.enableVibration(true);//振動
AudioAttributes audioAttributes = channel.getAudioAttributes();//獲取系統(tǒng)響鈴
channel.getGroup();//獲取通知渠道組
channel.setBypassDnd(true);//設(shè)置可以繞過請勿打擾模式
channel.setVibrationPattern(new long[]{100, 100, 200});//振動模式
channel.shouldShowLights();//是否會閃燈
mNotificationManager.createNotificationChannel(channel);
}
//不存在就新增
//創(chuàng)建通知的對象
Notification.Builder notification;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
//android8.0 notification適配
notification = new Notification.Builder(mContext)
.setChannelId("channel_id");
} else {
notification = new Notification.Builder(mContext);
}
//設(shè)置滾動文字
notification.setTicker(fileInfo.fileName + "開始下載")
.setWhen(System.currentTimeMillis())//設(shè)置通知顯示的時間
.setSmallIcon(R.mipmap.ic_launcher_round)//設(shè)置圖標
.setContentTitle(fileInfo.fileName)
.setContentText(fileInfo.fileName + "下載中...")
.setAutoCancel(true);//設(shè)置通知的特性 FLAG_AUTO_CANCEL點擊通知欄自動消失
//設(shè)置點擊通知欄的操作
Intent intent = new Intent(mContext, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
notification.setContentIntent(pendingIntent);
//發(fā)出通知廣播
mNotificationManager.notify(fileInfo.id, notification.build());
//把通知加到集合中
mNotification.put(fileInfo.id, notification);
}
}
/**
* 取消通知
*
* @param id
*/
public void cancleNotification(int id) {
//取消通知
mNotificationManager.cancel(id);
//同時刪除集合中的通知
mNotification.remove(id);
}
/**
* 更新通知進度
*
* @param id
* @param progress
*/
public void updateNotification(int id, int progress) {
Notification.Builder notification = mNotification.get(id);
if (notification != null) {
//更新進度條
notification.setProgress(100, progress, false);
//重新發(fā)送通知
mNotificationManager.notify(id, notification.build());
}
}
}
消息通知欄是通過NotificationUtil工具類來管理的绪励,調(diào)用showNotification()方法創(chuàng)建并顯示一個通知狀態(tài)欄肿孵,在進度更新時調(diào)用updateNotification()方法,下載完成或者取消通知狀態(tài)欄調(diào)用cancleNotification()就可以了疏魏,NotificationUtil類中已經(jīng)對Notification做了8.0的適配處理停做,關(guān)于Notification的8.0適配網(wǎng)上很多資料,不過需要注意在調(diào)用setChannelId()時設(shè)置的id要和NotificationChannel創(chuàng)建時傳入的id一致大莫;
NotificationChannel channel = new NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT);
在DownLoadTask中的downLoad()中還是要根據(jù)數(shù)據(jù)庫查詢的結(jié)果進行下載蛉腌,數(shù)據(jù)庫中如果有下載記錄,直接將查詢到的下載記錄傳入到下載線程中只厘,如果沒有就需要新增一個下載任務(wù)實例烙丛,在新增的時候需要注意,要根據(jù)傳入的單個任務(wù)的下載線程個數(shù)來弄羔味,要處理好每個線程的開始下載位置和結(jié)束下載位置河咽;
public void downLoad() {
//讀取數(shù)據(jù)庫的線程信息
List<ThreadInfo> threadInfos = dao.queryThread(mFileInfo.url);
if (threadInfos.size() == 0) {
//獲得每個線程下載的長度 分線程下載,多個線程下載一個文件
int length = mFileInfo.length / mThreadCount;
for (int i = 0; i < mThreadCount; i++) {
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.id = i;
threadInfo.url = mFileInfo.url;
threadInfo.thread_start = length * i;
threadInfo.thread_end = (i + 1) * length - 1;
threadInfo.finished = 0;
if (i == mThreadCount - 1) {
//最后一個線程下載的結(jié)束位置
threadInfo.thread_end = mFileInfo.length;
}
//添加到線程集合信息
threadInfos.add(threadInfo);
//向數(shù)據(jù)庫中插入一條信息
if (!dao.isExists(threadInfo.url, threadInfo.id)) {
dao.insertThread(threadInfo);
}
}
}
threadList = new ArrayList<>();
//啟動多個線程進行下載
for (ThreadInfo threadInfo : threadInfos) {
DownLoadThread downLoadThread = new DownLoadThread(threadInfo);
DownLoadTask.sExecutorService.execute(downLoadThread);
threadList.add(downLoadThread);
}
//啟動定時任務(wù)
mTimer.schedule(timerTask, 1000, 1000);
}
在開始的同時開啟一個Timer進行輪詢赋元;
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
if (isPause) {
//暫停時要將定時器清除掉
cancelTimer();
}
//發(fā)送消息修改activity進度
Message message = new Message();
message.what = DownLoadService.MSG_UPDATE;
message.arg1 = mFinished * 100 / mFileInfo.length;
message.arg2 = mFileInfo.id;
Log.e("DownLoadService", mFinished + "");
try {
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
/**
* 清除定時器
*/
private void cancelTimer(){
//取消定時器
timerTask.cancel();
timerTask = null;
mTimer.cancel();
mTimer = null;
}
在下載任務(wù)進行的同時會每隔一段時間回調(diào)TimerTask中的run方法忘蟹,通過Messenger向activity傳遞下載的進度,在暫停下載是需要將Timer移除并置為null搁凸。
在DownLoadThread線程中下載完畢后做法和之前一樣媚值,多了要將Timer移除并置為null;
/**
* 下載線程
*/
class DownLoadThread extends Thread {
private ThreadInfo mThreadInfo;
//線程是否執(zhí)行完畢
public boolean isFinished = false;
public DownLoadThread(ThreadInfo threadInfo) {
mThreadInfo = threadInfo;
}
@Override
public void run() {
super.run();
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);
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;
//累加每個線程下載的進度
int currentFinished = mThreadInfo.finished;
mThreadInfo.finished = currentFinished + len;
//下載暫停是要把進度保存在數(shù)據(jù)庫中
if (isPause) {
dao.updateThread(mThreadInfo.url, mThreadInfo.id, mThreadInfo.finished);
return;
}
}
//標識線程執(zhí)行完畢
isFinished = true;
//檢查下載任務(wù)是否執(zhí)行完畢
checkAllThreadsFinished();
}
} 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();
}
}
}
}
/**
* 判斷是否所有線程執(zhí)行完畢
*/
private synchronized void checkAllThreadsFinished() {
boolean allFinished = true;
//遍歷線程集合判斷線程是否執(zhí)行完畢
for (DownLoadThread downLoadThread : threadList) {
if (!downLoadThread.isFinished) {
allFinished = false;
break;
}
}
if (allFinished) {
cancelTimer();
//發(fā)送消息告訴activity下載完畢
Message message = new Message();
message.what = DownLoadService.MSG_FINISHED;
message.obj = mFileInfo;
try {
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
//刪除線程信息
dao.deleteThread(mFileInfo.url);
}
}
在activity更新下載進度時要根據(jù)對應(yīng)的文件id去更新進度坪仇;
/**
* 下載進度更新
*
* @param msg
*/
private void mSGUPDATE(Message msg) {
int finished = msg.arg1;
int id = msg.arg2;
updataItem(id, finished);
//更新通知進度
mNotificationUtil.updateNotification(id, finished);
}
/**
* 下載完成后更新
*
* @param msg
*/
private void mSGFINISHED(Message msg) {
//更新進度為0
FileInfo fileInfo = (FileInfo) msg.obj;
//下載完成后移除存儲的文件id
for (int i = 0; i < fileIds.size(); i++) {
Integer integer = fileIds.get(i);
if (integer == fileInfo.id) {
fileIds.remove(integer);
break;
}
}
if (fileInfo != null) {
updataItem(fileInfo.id, fileInfo.length);
}
Log.e("fileInfo.length-->", fileInfo.length + "");
Toast.makeText(MainActivity.this, fileInfo.fileName + "下載完畢", Toast.LENGTH_LONG).show();
//下載完畢后取消通知
mNotificationUtil.cancleNotification(fileInfo.id);
}
private void updataItem(int id, int finished) {
for (int i = 0; i < lists.size(); i++) {
FileInfo fileInfo = lists.get(i);
if (fileInfo.id == id) {
lists.remove(i);
fileInfo.finished = finished;
lists.add(i, fileInfo);
break;
}
}
adapter.updateProgress(lists);
}