安卓多线程下载文件(二)
前言
接着 安卓多线程下载文件(一)的内容,接下来实现文件的下载功能部分;
如果有需要的读者,请先看 安卓多线程下载文件(一)
一、设计下载信息分配任务类
1.下载之前,先通过网络访问 url 来获取被下载的文件的总长度:
伪代码:
URL url = new URL(downloadUrl);
connection = (HttpURLConnection) url.openConnection();
//省略部分代码
...
connection.getContentLength() //获取总长度
//省略部分代码
2.为了满足 暂停下载的需求,设计了一个是否暂停的标记 hasPaused ,通过暴露set方法来改变标记的值,达到暂停的目的;
同理 取消下载 也是这样设计;
3.为了能及时的更新下载进度,所以使用Handler发送进度 给需要的界面来显示;
之前的设计是每条线程各自计算自己的下载进度,但是发现这样做,最后计算总进度的时候不好计算;
于是换个思路:每条线程只记录自己下载了多少字节,
总的下载进度 = 所有线程已下载的字节数之和 除以 被下载的文件的总字节数;
处于下载状态下,使用Handler ,每过一定的时间(比如1秒钟),去获取所有线程 已经下载的字节 ,加起来 再 除以 总长度 就能算出下载进度了。
4.一般情况下,被下载的文件的地址 url 是唯一的,所以可以根据url 来查询数据库 ,如果数据库中没有对应的线程信息,那么需要从0开始下载,并把必要的线程信息插入数据库;
有信息的话,则根据信息来分配需要几条线程来下载,每条线程负责下载多少字节; 这样就达到了断点续下载的功能;
5.当点击暂停下载按钮后,把暂停标记hasPaused赋值为true,并把相关的信息(比如线程id,已下载的字节数等等)按url和id更新数据库中对应的记录;
其它细节还有很多,就不在一一拆分;直接上菜:
1.1 FromZeroDownloadTask
package com.linkpoon.mixed.downloadutil;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/***
* 如果要下载的文件在本地已经存在,
* 且大小跟源头文件大小相等,
* 就删掉,
* 然后重新下载;
* If the file you want to download already exists locally,
* And the size is the same as the source file,
* Just delete it,
* Then download again;
* */
public class FromZeroDownloadTask implements Runnable {
private static final String TAG = "FromZeroDownloadTask";
private static final int TYPE_SUCCESS = 0;
private static final int TYPE_FAILED = 1;
private static final int TYPE_PAUSED = 2;
private static final int TYPE_CANCELED = 3;
private WeakReference<Context> weakReference;
private ThreadInfoManager dbManager;
private int threadCount;// 用几条线程下载这个资源?
private long needDownTotalLength; //下载的文件的总长度
private Handler handler; //消息传递者 用来更新ui
private String url; //从哪里下载 ?
private String fileSavedName; //下载得到的文件的名字
private String fileSavePath; //下载得到的文件保存到哪里?
private File myFile = null; // 下载得到的文件
private boolean hasDownloading = false;// 是否正在下载
private boolean hasCanceled = false;// 是否已经取消
private boolean hasPaused = false;//是否已经暂停
public boolean isHasDownloading() {
return hasDownloading;
}
public void setHasDownloading(boolean hasDownloading) {
this.hasDownloading = hasDownloading;
}
public boolean isHasCanceled() {
return hasCanceled;
}
public void setHasCanceled(boolean hasCanceled) {
this.hasCanceled = hasCanceled;
}
public boolean isHasPaused() {
return hasPaused;
}
public void setHasPaused(boolean hasPaused) {
this.hasPaused = hasPaused;
}
private boolean downloadSuccess = false;// 是否下载成功
private List<DownloadRunnable> runnableList;
private final Handler handlerProgress = new Handler();
private final Runnable runnableProgress = new Runnable() {
@Override
public void run() {
onProgressUpdate(0);//更新进度
handlerProgress.postDelayed(runnableProgress, 1000);// 每1000毫秒更新一次进度
}
};
private long lastProgress = 0;
/***
*
* @param urlFrom 从哪里下载 ? 资源的网址
* @param fileSavePath 下载后得到的文件保存到哪里
* @param fileSavedName 下载后得到的文件叫什么名字
* @param handler 消息传递者,用来更新UI
* @param manager 数据库管理者,用来保存断点的,以便暂停后继续下载
* @param threadCount 用几条线程来下载这个资源
*
*/
public FromZeroDownloadTask(Context context, String urlFrom, String fileSavePath, String fileSavedName, Handler handler, ThreadInfoManager manager, int threadCount) {
this.weakReference = new WeakReference<>(context);
this.url = urlFrom;
this.fileSavePath = fileSavePath;
this.fileSavedName = fileSavedName;
this.dbManager = manager;
this.handler = handler;
this.threadCount = threadCount;
}
@Override
public void run() {
try {
LogUtil.i(TAG, "被下载的文件路径path=" + url);
needDownTotalLength = getTargetFileContentLength(url); // 得到被下载文件大小
LogUtil.i(TAG, "获取被下载文件的总长度" + needDownTotalLength);
if (needDownTotalLength <= 0) {
// 目标文件不存在 或者大小为0
onPostExecute(TYPE_FAILED);
return;
}
if (threadCount <= 0) {
// 使用的线程数量不能少于1条,所以重新指定为1条
threadCount = 1;
}
long perPartSize;
if (threadCount == 1) {//如果只用一条线程来下载,变成单线程了
perPartSize = needDownTotalLength;
} else {
perPartSize = needDownTotalLength / threadCount + 1;// 有可能出现不能整除的情况,所以+1
}
LogUtil.i(TAG, "分给" + threadCount + "条线程,平均每部分" + perPartSize);
// 从数据库 根据url 获取线程信息
List<ThreadInfo> threads = dbManager.getThreads(weakReference.get(), url); // 从数据库 根据url 获取线程信息
int thSum = threads.size();
myFile = new File(fileSavePath, fileSavedName);
if (!myFile.exists()) {
//本地指定路径下 没有目标文件
if (thSum > 0) {
// 本地没有文件,数据库的记录已经没有意义,需要删掉记录
dbManager.deleteData(weakReference.get(), url);
}
boolean b = myFile.createNewFile();
LogUtil.i(TAG, "创建文件结果" + b);
startFrom0(perPartSize, threads);// 从0开始下载
}else {
//本地指定路径下 已经存在 要下载的文件
LogUtil.i(TAG, "已经存在的文件长度" + myFile.length());
if (needDownTotalLength == myFile.length()) {
// 需要下载的字节数 跟 本地存在的文件字节数 一样长
// 接下来,删掉本地文件,重新下
boolean b = myFile.delete();
if (b) {
LogUtil.i(TAG, "删掉本地已经存在的文件 成功!");
} else {
LogUtil.i(TAG, "删掉本地已经存在的文件 失败!");
}
if (thSum > 0) {
// 删掉 数据库中的线程信息 记录
dbManager.deleteData(weakReference.get(), url);
}
boolean ok = myFile.createNewFile();
LogUtil.i(TAG, "创建文件结果" + ok);
startFrom0(perPartSize, threads);// 从0开始下载
}else {
//已经下了一部分,但没有下载完成,只需要从上次暂停的位置,接着下载就行
LogUtil.i(TAG, "开始寻找暂停位置,接着下");
runnableList = new ArrayList<>(thSum);
for (ThreadInfo threadInfo : threads) {
int threadId = threadInfo.getThreadId();
long startPosition = threadInfo.getStartPosition();
long endPosition = threadInfo.getEndPosition();
long downloadedByte = threadInfo.getDownloadedLength();
RandomAccessFile accessFile = new RandomAccessFile(myFile, "rw");
DownloadRunnable quietDownloadRunnable = new DownloadRunnable(threadId, url, startPosition, endPosition, downloadedByte, accessFile);
runnableList.add(quietDownloadRunnable); //把下载任务保存到集合中
}
startTaskByThreadPool();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (hasCanceled) {
delMyFile();// 取消下载
}
}
}
private void startFrom0(long perPartSize, List<ThreadInfo> threads) throws FileNotFoundException {
runnableList = new ArrayList<>(threadCount);
threads.clear();// 先清空记录
for (int i = 0; i < threadCount; i++) {
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.setThreadId(i);
long startPosition = i * perPartSize;//计算每条线程的下载的开始位置
threadInfo.setStartPosition(startPosition);
long endPosition = startPosition + perPartSize;//计算下载的结束位置
if (i == threadCount - 1) {
endPosition = needDownTotalLength; //如果是最后一条线程,结束位置 就是 总长度
}
threadInfo.setEndPosition(endPosition);
threadInfo.setUrl(url);
threadInfo.setDownloadedLength(0);
threadInfo.setTotalLength(needDownTotalLength);
threads.add(threadInfo);// 添加一条线程信息记录
//LogUtil.i(TAG, "ThreadId =" + i + " 开始位置=" + startPosition + " 结束位置=" + endPosition);
dbManager.insertData(weakReference.get(), threadInfo); // 把 threadInfo 插入数据库
RandomAccessFile accessFile = new RandomAccessFile(myFile, "rw");
DownloadRunnable quietDownloadRunnable = new DownloadRunnable(i, url, startPosition, endPosition, 0, accessFile);
runnableList.add(quietDownloadRunnable);
}
startTaskByThreadPool();
}
private void startTaskByThreadPool() {
if (!runnableList.isEmpty()) {
LogUtil.i(TAG, "即将新建线程执行任务");
for (DownloadRunnable r : runnableList) {
Thread thread = new Thread(r);
thread.start();// 调用start 来执行,不能直接调用run
//TODO 这里可以优化,使用线程池来执行任务,而不是新建线程来执行
}
setHasDownloading(true);//设置已经下载的标志
handlerProgress.removeCallbacks(runnableProgress);
handlerProgress.postDelayed(runnableProgress, 500);// 开始下载后,计算下载进度
}
}
protected void onProgressUpdate(int value) {
if (downloadSuccess) {
handlerProgress.removeCallbacks(runnableProgress);// 下载完成了,移除计算进度的任务
return;
}
long progress = calProgress();
//LogUtil.i(TAG, "下载进度progress=" + progress);
if (progress >= 99 && checkAllDownloadFinished()) {
LogUtil.i(TAG, "下载完成!");
lastProgress = 100;
dealProgressUpdate(needDownTotalLength, needDownTotalLength, 100); //已经下载完成了,更新进度为100%
onPostExecute(TYPE_SUCCESS);
} else {
lastProgress = progress;
dealProgressUpdate(getDownloadedByte(), needDownTotalLength, progress); //更新进度
}
}
protected void onPostExecute(int code) {
switch (code) {
case TYPE_SUCCESS:
downloadSuccess = true;
notifyRealDownloadComplete(); // 下载完成
setHasDownloading(false);// 下载完成
handlerProgress.removeCallbacks(runnableProgress);// 下载完成 移除计算进度的runnable
//handlerProgress.removeCallbacksAndMessages(null);// 下载完成 移除计算进度的runnable和消息
break;
case TYPE_FAILED:
dealTaskFailed();// 下载失败
setHasDownloading(false);// 下载失败
handlerProgress.removeCallbacks(runnableProgress);// 下载失败 移除计算进度的runnable
//handlerProgress.removeCallbacksAndMessages(null);// 下载失败 移除计算进度的runnable和消息
break;
case TYPE_PAUSED:
dealTaskPaused();
setHasDownloading(false);// 下载暂停
handlerProgress.removeCallbacks(runnableProgress);// 下载暂停 移除计算进度的runnable
//handlerProgress.removeCallbacksAndMessages(null);// 下载暂停 移除计算进度的runnable和消息
break;
case TYPE_CANCELED:
dealTaskCanceled();
setHasDownloading(false);// 下载取消
handlerProgress.removeCallbacks(runnableProgress);// 下载取消 移除计算进度的runnable
//handlerProgress.removeCallbacksAndMessages(null);// 下载取消 移除计算进度的runnable和消息
break;
default:
break;
}
}
private void dealTaskCanceled() {
if (handler == null) {
return;
}
Message msg = new Message();
msg.what = DownloadConstants.MSG_NORMAL_CANCELED;
msg.obj = url;
handler.sendMessage(msg);//发送取消下载的消息
LogUtil.i(TAG, "dealTaskCanceled 发送取消下载的消息 ");
}
private void dealTaskPaused() {
if (handler == null) {
return;
}
Message msg = new Message();
msg.what = DownloadConstants.MSG_NORMAL_PAUSE;
msg.obj = url;
handler.sendMessage(msg);//发送暂停下载的消息
LogUtil.i(TAG, "dealTaskPause 发送暂停下载的消息 MSG_NORMAL_PAUSE ");
}
private void dealProgressUpdate(long downedByte, long totalByte, long progress) {
if (handler == null) {
return;
}
Message msg = new Message();
msg.what = DownloadConstants.MSG_NORMAL_UPDATE_PROGRESS;
msg.obj = url;
Bundle bundle = new Bundle();
bundle.putLong(DownloadConstants.BUNDLE_KEY_DOWNED_BYTE, downedByte);
bundle.putLong(DownloadConstants.BUNDLE_KEY_TOTAL_BYTE, totalByte);
bundle.putLong(DownloadConstants.BUNDLE_KEY_PROGRESS, progress);
bundle.putString(DownloadConstants.BUNDLE_KEY_DOWNLOAD_URL, url);
msg.setData(bundle);
handler.sendMessage(msg);//发出进度更新的消息
// LogUtil.i(TAG, "dealProgressUpdate 发出进度更新的消息");
}
/***
* 处理下载失败的情况
* 发消息通知ui界面显示下载失败等
* 从数据库清除 对应的下载
*/
private void dealTaskFailed() {
if (handler == null) {
return;
}
Message msg = new Message();
msg.what = DownloadConstants.MSG_NORMAL_FAILED;
msg.obj = url;
handler.sendMessage(msg);//发送下载任务失败的消息
LogUtil.i(TAG, "dealTaskFailed() 发送下载任务失败的消息 MSG_NORMAL_FAILED");
if (null != dbManager) {
// 从数据库清除
dbManager.deleteData(weakReference.get(), url); // 从数据库清除
LogUtil.i(TAG, "dealTaskFailed() dbManager.deleteData 从数据库清除 " + url);
}
}
private void notifyRealDownloadComplete() {
if (handler == null) {
return;
}
Message msg = new Message();
msg.what = DownloadConstants.MSG_NORMAL_COMPLETE;
msg.obj = url;
Bundle bundle = new Bundle();
File apkFile = new File(fileSavePath, fileSavedName);
bundle.putString(DownloadConstants.BUNDLE_KEY_FILED_PATH, apkFile.getAbsolutePath());
msg.setData(bundle);
LogUtil.i(TAG, "notifyRealDownloadComplete 发送下载完成的消息");
handler.sendMessage(msg);//发送下载完成的消息
if (null != dbManager) {
// 从数据库清除
dbManager.deleteData(weakReference.get(), url); // 从数据库清除
LogUtil.i(TAG, "notifyRealDownloadComplete() dbManager.deleteData 从数据库清除 " + url);
}
}
/**
* 获取下载完成的字节数
*/
public long getDownloadedByte() {
if (runnableList == null) {
return 0;
}
// 统计多条线程已经下载的总大小
long sumSize = 0;
if (!runnableList.isEmpty()) {
for (DownloadRunnable quietDownloadRunnable : runnableList) {
if (quietDownloadRunnable != null) {
sumSize += quietDownloadRunnable.getDownloadedByte();
}
}
}
// 返回已经完成的字节数
return sumSize;
}
/**
* 两个 long 类型的数值 相除
* 结果会自动取整 导致结果为0
* 解决:
* 在做除的操作时,
* 被除数先乘以1.0
* 再去除以除数,
* 这样得到的结果就是小数而不会取整为零。
*/
private long calProgress() {
if (needDownTotalLength <= 0) {
// 除数不能为0
return -1;
}
try {
return Math.round(((getDownloadedByte() * 1.0) / needDownTotalLength) * 100);
} catch (NumberFormatException e) {
e.printStackTrace();
}
return -1;
}
public boolean checkAllDownloadFinished() {
if (runnableList != null && !runnableList.isEmpty()) {
for (DownloadRunnable r : runnableList) {
if (!r.isDownloadFinish()) {//只要其中一个没完成 那就是没完成;必须全部都完成才算完成
return false;
}
}
return true;
}
return false;
}
/**
* 暂停下载
*/
public void pauseDownload() {
setHasPaused(true);
if (runnableList != null && !runnableList.isEmpty()) {
for (DownloadRunnable r : runnableList) {
r.pauseDownload();// 需要把下载的线程全部暂停下来,后面在把保存已经下载的字节数,不然会出现已下载的字节数不准的问题,导致恢复下载后,下载得到的文件不完整,安装失败的问题
}
onPostExecute(TYPE_PAUSED);// 通知UI更新为暂停状态
for (DownloadRunnable r : runnableList) {
ThreadInfo info = new ThreadInfo();
info.setDownloadedLength(r.getDownloadedByte());
info.setTotalLength(needDownTotalLength);
info.setEndPosition(r.getEndPosition());
info.setStartPosition(r.getStartPosition());
info.setThreadId(r.getThreadId());
info.setUrl(r.getRulPath());
if (null != dbManager) {
dbManager.updateData(weakReference.get(), info);// 暂停的时候要更新数据库,把必要的信息记录下来,以便继续下载的时候指定从哪个位置接着下
}
}
}
}
/**
* 取消下载
*/
public void cancelDownload() {
setHasCanceled(true);
if (runnableList != null && !runnableList.isEmpty()) {
for (DownloadRunnable r : runnableList) {
r.cancelDownload();
}
onPostExecute(TYPE_CANCELED);// 通知UI更新为取消状态
}
delMyFile();
}
public void delMyFile() {
try {
if (myFile != null && myFile.exists()) {
boolean isDelOk = myFile.delete();
LogUtil.i(TAG, myFile.getAbsolutePath() + "删除结果" + isDelOk);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取下载的目标文件的长度(大小)
*/
private int getTargetFileContentLength(String downloadUrl) {
HttpURLConnection connection = null;
try {
URL url = new URL(downloadUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(30000);
connection.setReadTimeout(30000);
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "*/*");
//connection.setRequestProperty("Accept-Language", "zh-CN");
connection.setRequestProperty("Charset", "UTF-8");
connection.setRequestProperty("Connection", "Keep-Alive");
connection.connect();
if (connection.getResponseCode() == 200) {
// 请求成功
return connection.getContentLength();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
}
return 0;
}
}
接下来编写一个类 来从网络上下载文件的功能,
因为多线程下载同一个文件的前提是 服务器支持 范围请求
即 urlConnection.setRequestProperty(“Range”, “bytes=” + startValue + “-” + endValue);
1.2 DownloadRunnable 类
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownloadRunnable implements Runnable {
private static final String TAG = Global.TAG_PREFIX + "downloadRunnable";
private int threadId; //线程的id 只是用来标记是第几条线程
private String rulPath;// 从哪里下载 从网上的某个网址? 还是服务器的网址?
// 当前线程的开始下载位置
private long startPosition;
// 当前线程的结束下载位置
private long endPosition;
private RandomAccessFile accessFile;
// 定义该线程已下载的字节数
private long downloadedByte = 0;
private HttpURLConnection urlConnection = null;
private InputStream inputStream = null;
private boolean isCanceled = false;
private boolean isPaused = false;
private boolean downloadFinish = false;// 是否下载完成
public int getThreadId() {
return threadId;
}
public String getRulPath() {
return rulPath;
}
public long getStartPosition() {
return startPosition;
}
public long getEndPosition() {
return endPosition;
}
public RandomAccessFile getAccessFile() {
return accessFile;
}
public boolean isCanceled() {
return isCanceled;
}
public boolean isPaused() {
return isPaused;
}
public DownloadRunnable(int threadId, String rulPath, long startPosition, long endPosition, long downloadedByte, RandomAccessFile randomAccessFile) {
this.threadId = threadId;
this.rulPath = rulPath;
this.startPosition = startPosition;
this.endPosition = endPosition;
this.downloadedByte = downloadedByte;
this.accessFile = randomAccessFile;
this.downloadFinish = false;
}
/**
* 获取已经下载好的字节数
*/
public long getDownloadedByte() {
return this.downloadedByte;
}
public boolean isDownloadFinish() {
return downloadFinish; //获取是否下载完成的标记
}
/**
* 暂停下载
*/
public void pauseDownload() {
isPaused = true;
}
/**
* 取消下载
*/
public void cancelDownload() {
isCanceled = true;
}
@Override
public void run() {
try {
// 开始的字节数= 该线程开始的位置+已经下载的字节数
long startValue = startPosition + downloadedByte;
long endValue = endPosition;
LogUtil.i(TAG, "任务编号" + threadId + ",开始位置startPosition=" + startPosition + ",结束位置endPosition=" + endPosition + ",已下载字节数downloadedByte=" + downloadedByte);
LogUtil.i(TAG, "任务编号" + threadId + ",真正需要下载的范围:从startValue=" + startValue + "到endValue=" + endValue);
if (startValue >= endValue) {// 开始的位置 大于等于 结束的位置 ,说明已经下载完成了
this.downloadFinish = true;
LogUtil.i(TAG, rulPath + ",任务编号" + threadId + "下载完成");
return;
}
URL url = new URL(rulPath);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setReadTimeout(30 * 1000);// 读取超时时间30秒
urlConnection.setConnectTimeout(30 * 1000);// 连接超时时间30秒
String requestMethod = "GET"; //下载文件 是用GET 请求的
urlConnection.setRequestMethod(requestMethod);
urlConnection.setDoInput(true);
urlConnection.setDoOutput(false);
urlConnection.setRequestProperty("Range", "bytes=" + startValue + "-" + endValue);
urlConnection.setRequestProperty("Accept", "*/*");// 接受所有类型的文件
urlConnection.setRequestProperty("Charset", "UTF-8");
urlConnection.setRequestProperty("Connection", "Keep-Alive");
//LogUtil.i(TAG, "任务编号" + numId + "开始请求 urlConnection.connect()");
urlConnection.connect();
int responseCode = urlConnection.getResponseCode();
LogUtil.i(TAG, "任务编号" + threadId + "响应码=" + responseCode);
// Range 请求 返回的响应码 是206
if (responseCode == 200 || responseCode == 206) { //请求成功
inputStream = urlConnection.getInputStream();
// 跳过 startPosition + 已经下载的字节数 个字符,表明该线程只下载自己负责那部分文件
accessFile.seek(startValue);
LogUtil.i(TAG, "任务编号" + threadId + "跳过字节数" + startValue);
byte[] by = new byte[1024];
int hasRead = 0;
// 读取网络数据,并写入本地文件
while ((hasRead = inputStream.read(by)) != -1) {
if (isCanceled) {
this.downloadFinish = false;
return;
}
if (isPaused) {
this.downloadFinish = false;
return;
}
mySleep(25); //必须让线程睡个几毫秒,解决多线程下载,出现线程处于运行状态但进度条不动的问题,也解决CPU占用率高的问题
accessFile.write(by, 0, hasRead);
// 累计该线程下载的总大小
downloadedByte += hasRead;
}
}
accessFile.close();
this.downloadFinish = true;
LogUtil.i(TAG, rulPath + ",任务编号" + threadId + "下载完成,downloadedByte=" + downloadedByte);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeRes();
}
}
public void mySleep(long times) {
try {
Thread.sleep(times);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void closeRes() {
try {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (inputStream != null) {
inputStream.close();
}
if (accessFile != null) {
accessFile.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.3 定义消息传递编号
public class DownloadConstants {
public static final int MSG_NORMAL_INIT = 1;//初始化
public static final int MSG_NORMAL_SERVICE_BIND = 2;//服务绑定
public static final int MSG_NORMAL_PREPARE = 3;//准备
public static final int MSG_NORMAL_WAITING = 4;//等待中
public static final int MSG_NORMAL_START = 5;//开始
public static final int MSG_NORMAL_UPDATE_PROGRESS = 6;//进度更新
public static final int MSG_NORMAL_PAUSE = 7;//暂停
public static final int MSG_NORMAL_RESUME_RUN = 8;//被暂停后恢复运行
public static final int MSG_NORMAL_FAILED = 9;//下载失败
public static final int MSG_NORMAL_COMPLETE = 10;//下载完成
public static final int MSG_NORMAL_CANCELED = 11;//下载取消
public static final String BUNDLE_KEY_DOWNED_BYTE = "downed";
public static final String BUNDLE_KEY_TOTAL_BYTE = "total";
public static final String BUNDLE_KEY_PROGRESS = "progress";
public static final String BUNDLE_KEY_DOWNLOAD_URL = "downloadUrl";
public static final String BUNDLE_KEY_FILE_ID = "fileId";
public static final String BUNDLE_KEY_THREAD_ID = "threadId";
public static final String BUNDLE_KEY_FILED_PATH = "filedPath";
}
二、设计下载服务
下载文件一般都是比较耗时的,所以需要开启一个服务在后台慢慢下;
2.1 NewVersionDownLoadService
直接新建一个NewVersionDownLoadService 类继承 Service;
对开始下载,暂停下载,取消下载 三个方法做一些包装;
package com.linkpoon.mixed.service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import com.linkpoon.mixed.R;
import com.linkpoon.mixed.activity.NewVersionDownLoadDetailActivity;
import com.linkpoon.mixed.bean.ServerVersion;
import com.linkpoon.mixed.downloadutil.FromZeroDownloadTask;
import com.linkpoon.mixed.global.Constant;
import com.linkpoon.mixed.manager.ThreadInfoManager;
import com.linkpoon.mixed.util.ToastUtil;
/**
* 下载服务
*/
public class NewVersionDownLoadService extends Service {
private final DownloadBinder mBinder = new DownloadBinder();
private FromZeroDownloadTask downloadTask;
private String downloadUrl;
private String fileSavePath;//下载的文件 保存下来时取的路径 ,保存在什么地方?
private String fileSaveName;//下载的文件 保存下来时取的名字
private ServerVersion mServerVersion;
private Handler msgHandler;
private int currentProgress;
private boolean hasStartForeground = false;
public NewVersionDownLoadService() {
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!hasStartForeground) {
hasStartForeground = true;
startForeground(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_preparing_for_download), 0));
}
// return super.onStartCommand(intent, flags, startId);
return Service.START_NOT_STICKY;
}
public class DownloadBinder extends Binder {
public NewVersionDownLoadService getNewVersionDownLoadService() {
return NewVersionDownLoadService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public boolean isDownloading() {
if (downloadTask != null) {
return downloadTask.isHasDownloading();
}
return false;
}
public boolean isHasCanceled() {
if (downloadTask != null) {
return downloadTask.isHasCanceled();
}
return false;
}
/**
* 开始下载
*
* @param fileSavePath 下载得到的文件保存在什么地方
* @param threadCount 用几条线程 来下载 同一个文件
*/
public void startDownLoad(String fileSavePath, ServerVersion serverVersion, int threadCount, Handler handler) {
if (this.downloadTask == null) {
// 任务不存在,新建
this.downloadUrl = serverVersion.getVersionApkUrl();
this.fileSaveName = serverVersion.getVersionName();
this.fileSavePath = fileSavePath;
this.mServerVersion = serverVersion;
this.msgHandler = handler;
ThreadInfoManager dbManager = new ThreadInfoManager();
this.downloadTask = new FromZeroDownloadTask(this, downloadUrl, fileSavePath, fileSaveName, msgHandler, dbManager, threadCount);
}
if (!downloadTask.isHasDownloading()) {
downloadTask.setHasDownloading(true);
Thread thread = new Thread(downloadTask);
thread.setName("thread_new_version_1");
thread.start();// 开启线程来执行
startForeground(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_preparing_for_download), 0));
} else {
String downloadingApk = getString(R.string.str_downloading);
ToastUtil.showText(NewVersionDownLoadService.this, downloadingApk);
}
}
/**
* 暂停下载
*/
public void pauseDownLoad() {
if (downloadTask != null) {
downloadTask.pauseDownload();
getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_in_the_pause), currentProgress));
String pauseDownloadApk = getString(R.string.str_suspended_download);
ToastUtil.showText(NewVersionDownLoadService.this, pauseDownloadApk);
}
}
/**
* 取消下载
*/
public void cancelDownLoad() {
if (downloadTask != null) {
downloadTask.cancelDownload();
stopForeground(true);
getNotificationManager().cancel(Constant.ID_NOTIFICATION_APK_DOWNLOAD);
String cancelDownloadApk = getString(R.string.str_down_load_apk_canceled);
ToastUtil.showText(NewVersionDownLoadService.this, cancelDownloadApk);
}
}
public void onProgress(long downloadByte, long totalByte, int progress) {
currentProgress = progress;
getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_new_version_apk_downloading), progress));
}
public void onSuccess() {
stopForeground(true);
getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_down_load_apk_success), -1));
stopDownLoadServiceDelayed();
downloadTask = null;
}
public void onFailed() {
stopForeground(true);
getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_down_load_apk_failed), -1));
downloadTask = null;
}
public void onPaused() {
getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_suspended_download), currentProgress));
}
public void onCanceled() {
getNotificationManager().cancel(Constant.ID_NOTIFICATION_APK_DOWNLOAD);//下载被取消了
}
private NotificationManager getNotificationManager() {
return (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
}
private Notification getNotification(String contentText, int mProgress) {
String channelId = "test_download_id";
String channelName = "test_new_version";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//安卓8.0及之后版本适配
final int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
channel.enableLights(true);//设置提示灯
channel.setLightColor(Color.RED);//设置提示灯颜色
channel.setShowBadge(true);//显示logo
channel.setDescription("");//设置描述
channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); //设置锁屏可见 VISIBILITY_PUBLIC=可见
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build();
channel.setSound(null, audioAttributes);//不发出声音,注意这个设置只在第一次安装时生效,如果需要改动后生效,得卸载应用重新安装
getNotificationManager().createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
builder.setChannelId(channelId);
builder.setContentTitle(getString(R.string.str_down_load_new_version_apk)); // 设置标题
if (mProgress >= 0) {
builder.setContentText(mProgress + "%");
builder.setProgress(100, mProgress, false);
}
// builder.setSubText(null); // 设置内容下面一小行的文字
// builder.setTicker(""); // 设置收到通知时在顶部显示的文字信息
builder.setContentText(contentText);
builder.setWhen(System.currentTimeMillis()); // 设置通知时间,一般设置的是收到通知时的System.currentTimeMillis()
builder.setSmallIcon(R.drawable.ic_stat_name); // 设置小图标,在接收到通知的时候顶部也会显示这个小图标
builder.setOnlyAlertOnce(true);//只提醒一次
builder.setSound(null);//不发出提示音
builder.setOngoing(true);
// 设置为true,表示它为一个正在进行的通知。他们通常是用来表示
// 一个后台任务,用户积极参与(如播放音乐)或以某种方式正在等待,
// 因此占用设备(如一个文件下载,同步操作,主动网络连接)
builder.setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.drawable.vector_drawable_ic_download_112c68)); // 设置大图标
builder.setAutoCancel(false); // 用户点击Notification点击面板后是否让通知取消(默认不取消)
builder.setPriority(NotificationCompat.PRIORITY_DEFAULT);// 设置优先级
Intent intent = new Intent(this, NewVersionDownLoadDetailActivity.class);
if (fileSavePath != null) {
intent.putExtra(NewVersionDownLoadDetailActivity.INTENT_KEY_PATH, fileSavePath);
}
if (mServerVersion != null) {
intent.putExtra(NewVersionDownLoadDetailActivity.INTENT_KEY_VERSION, mServerVersion);
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1216, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
Notification notification = builder.build();
return notification;
}
/**
* 延迟关闭服务
*/
private void stopDownLoadServiceDelayed() {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopSelf();
}
}, 10000);
}
}
三、注册服务
在清单文件AndroidManifest.xml 中注册NewVersionDownLoadService;
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".app.App"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:replace="android:allowBackup"
tools:targetApi="n">
<!-- tools:replace="android:allowBackup,android:icon" -->
<activity
android:name=".activity.MainActivity"
android:configChanges="keyboardHidden|orientation"
android:exported="true"
android:launchMode="standard"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.NewVersionDownLoadService"
android:exported="false" />
</application>
</manifest>
剩下的请接着看 安卓多线程下载文件(三)