到华为后,信息管理特别严格,文件不能外发,所以好久都没写博客了,今天周日,老婆非要我学习,就闲来无事,写一篇博客,呵呵……
前段时间,项目中提到了断点下载apk并静默安装的需求,本打算用应用市场成熟的经验,结果人家不给借用,就只能自己写了,在网上找了一些资源,并自己封装了一下,就成了今天这篇博客的内容。
断点下载的主要实现就是用SQlite数据库记录下断点时下载的记录,然后再次下载时,查到数据库的记录,再从断点处开始下载,本例中用的是多线程,好了,废话不多说,直接上代码。
这里需要多说两句,大家的项目中以后如果涉及到多线程的,请一定要多考虑一些场景,比如非UI线程中逻辑处理是否全面、各分支场景处理是否得当、下载过程中的各种状态是否可以控制、控制的是否正确等等,问题比较多,大家一定要控制好,否则测试时应该会发现许多问题。
package com.genius.download.logic;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DBNAME = "itcast.db";
private static final int VERSION = 1;
public DBOpenHelper(Context context) {
super(context, DBNAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS filedownlog");
onCreate(db);
}
}
DownloadHelper为下载的管理类,是本人封装后的,构建对象后就可以直接执行下载:
package com.genius.download.logic;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Context;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
public class DownloadHelper {
private final String TAG = getClass().getSimpleName();
/***
* 钱包的文件存储目录
*/
private String walletFileDir = Environment.getExternalStorageDirectory()
+ File.separator + "HwWallet/app";
private FileService fileService;
/** 已下载文件长度 **/
private int downloadSize = 0;
/** 原始文件长度 **/
private int fileSize = 0;
/** 线程数 **/
private DownloadThread[] threads = new DownloadThread[5];
/** 本地保存文件 */
private File saveFile;
/** 缓存各线程下载的长度 */
private Map
data = new ConcurrentHashMap
();
/** 每条线程下载的长度 */
private int block;
/** 下载路径 **/
private String downloadUrl;
private boolean isFinish = false;// 下载未完成
public DownloadHelper(final Context context, final String downloadUrl) {
this.downloadUrl = downloadUrl;
fileService = new FileService(context);
}
public void download(final DownloadListener listener) {
new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
scheduleTaskForChildThread(listener);
executeDownload(listener);
} catch (Exception e) {
Log.i(TAG, "occur exception in download.");
listener.onDownloadFailed(-1);
throw new RuntimeException("don't connection this url");
}
}
}.start();
}
/**
* 为子线程分配任务 方法表述 void
*/
private void scheduleTaskForChildThread(DownloadListener listener) {
try {
File dir = new File(walletFileDir);
if (!dir.exists()) {
dir.mkdirs();
}
URL url = new URL(downloadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
fileSize = conn.getContentLength();// 根据响应获取文件大小
if (fileSize <= 0) {
listener.onDownloadFailed(-1);
throw new RuntimeException("Unkown file size ");
}
listener.onGetFileSize(fileSize);
Log.i(TAG, "file name is = " + getReallyFileName(downloadUrl));
saveFile = new File(dir, getReallyFileName(downloadUrl));/* 保存文件 */
Log.i(TAG, "save file path is " + saveFile.getPath());
Map
logdata = fileService.getData(downloadUrl);// 获取每条线程已经下载的文件长度
if (logdata == null || logdata.size() == 0) {
for (int i = 0; i < threads.length; i++) {
data.put(i + 1, 0);
}
} else {
for (Map.Entry
entry : logdata.entrySet()) {
data.put(entry.getKey(), entry.getValue());
}
}
block = (fileSize % threads.length) == 0 ? fileSize
/ threads.length : fileSize / threads.length + 1;
Log.i(TAG, "this.block == " + block);
if (data.size() == threads.length) {
for (int i = 0; i < threads.length; i++) {
downloadSize += data.get(i + 1);
}
Log.i(TAG, "completed download size is == " + downloadSize);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void executeDownload(DownloadListener listener) throws Exception {
try {
URL url = new URL(this.downloadUrl);
if (this.data.size() != this.threads.length) {
this.data.clear();
for (int i = 0; i < this.threads.length; i++) {
this.data.put(i + 1, 0);
}
}
for (int i = 0; i < this.threads.length; i++) {
int downLength = this.data.get(i + 1);
if (downLength < this.block
&& this.downloadSize < this.fileSize) { // 该线程未完成下载时,继续下载
this.threads[i] = new DownloadThread(this, url,
this.saveFile, this.block, this.data.get(i + 1),
i + 1, listener);
this.threads[i].setPriority(7);
this.threads[i].start();
} else {
this.threads[i] = null;
}
}
this.fileService.save(this.downloadUrl, this.data);
while (!isFinish) {
// 循环判断是否下载完毕
Thread.sleep(900);
isFinish = true;// 假定下载完成
for (int i = 0; i < this.threads.length; i++) {
if (this.threads[i] != null && !this.threads[i].isFinish()) {
isFinish = false;// 下载没有完成
if (this.threads[i].getDownLength() == -1) {// 如果下载失败,再重新下载
this.threads[i] = new DownloadThread(this, url,
this.saveFile, this.block,
this.data.get(i + 1), i + 1, listener);
this.threads[i].setPriority(7);
this.threads[i].start();
}
}
}
if (listener != null) {
listener.onDownloadSize(this.downloadSize);
}
}
listener.onDownloadCompleted();
fileService.delete(this.downloadUrl);
} catch (RuntimeException e) {
listener.onDownloadFailed(-1);
throw new Exception("file download fail");
}
listener.onDownloadSize(this.downloadSize);
}
private String getReallyFileName(String url) {
String filename = "";
URL myURL;
HttpURLConnection conn = null;
if (url == null || url.length() < 1) {
return "";
}
try {
myURL = new URL(url);
conn = (HttpURLConnection) myURL.openConnection();
conn.connect();
conn.getResponseCode();
URL absUrl = conn.getURL();// 获得真实Url
filename = matchFileName(conn);
if (!TextUtils.isEmpty(filename)) {
Log.i(TAG, "file name is = " + filename);
return filename;
}
filename = conn.getHeaderField("Content-Disposition");// 通过Content-Disposition获取文件名,这点跟服务器有关,需要灵活变通
if (filename == null || filename.length() < 1) {
filename = absUrl.getFile();
}
} catch (MalformedURLException e) {
Log.w(TAG, "MalformedURLException in download");
} catch (IOException e) {
Log.w(TAG, "IOException in download");
} finally {
if (conn != null) {
conn.disconnect();
conn = null;
}
}
return filename;
}
/**
* 获取文件名
*/
private String matchFileName(HttpURLConnection conn) {
String name = "";
for (int i = 0;; i++) {
String mine = conn.getHeaderField(i);
if (mine == null)
break;
if ("content-disposition".equals(conn.getHeaderFieldKey(i)
.toLowerCase())) {
Matcher m = Pattern.compile(".*filename=(.*)").matcher(
mine.toLowerCase());
if (m.find()) {
name = m.group(1);
name = name.substring(1, name.length() - 1);
Log.i(TAG, "matches, name is " + name);
break;
}
}
}
return name;
}
/**
* 获取线程数
*/
public int getThreadSize() {
return threads.length;
}
/**
* 获取文件大小
*
* @return
*/
public int getFileSize() {
return fileSize;
}
/**
* 累计已下载大小
*
* @param size
*/
protected synchronized void append(int size) {
downloadSize += size;
}
/**
* 更新指定线程最后下载的位置
*
* @param threadId
* 线程id
* @param pos
* 最后下载的位置
*/
protected void update(int threadId, int pos) {
this.data.put(threadId, pos);
}
/**
* 保存记录文件
*/
protected synchronized void saveLogFile() {
this.fileService.update(this.downloadUrl, this.data);
}
public void cancelDownload() {
for (DownloadThread thread : threads) {
thread.cancelDownload();
}
}
/**
* 获取下载的文件的保存路径 方法表述
*
* @return String
*/
public String getSaveFilePath() {
if (this.saveFile != null) {
return this.saveFile.getPath();
}
return "";
}
}
DownloadThread即为管理线程任务的,网上找一下,应该可以找到很多类似的资源:
package com.genius.download.logic;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import android.util.Log;
public class DownloadThread extends Thread {
private final String TAG = getClass().getSimpleName();
private static final int BUFFER_SIZE = 1024;
private File saveFile;
private URL downUrl;
private int block;
/* 下载开始位置 */
private int threadId = -1;
private int downLength;
private boolean finish = false;
private boolean cancel = false;
private DownloadHelper mDownloadHelper;
private DownloadListener listener;
public DownloadThread(DownloadHelper downloader, URL downUrl,
File saveFile, int block, int downLength, int threadId,
DownloadListener listener) {
this.mDownloadHelper = downloader;
this.downUrl = downUrl;
this.saveFile = saveFile;
this.block = block;
this.downLength = downLength;
this.threadId = threadId;
this.listener = listener;
}
@Override
public void run() {
if (downLength < block) {// 未下载完成
try {
HttpURLConnection http = (HttpURLConnection) downUrl
.openConnection();
http.setConnectTimeout(5 * 1000);
http.setRequestMethod("GET");
http.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
http.setRequestProperty("Accept-Language", "zh-CN");
http.setRequestProperty("Referer", downUrl.toString());
http.setRequestProperty("Charset", "UTF-8");
int startPos = block * (threadId - 1) + downLength;// 开始位置
int endPos = block * threadId - 1;// 结束位置
// by ngj
if (threadId == mDownloadHelper.getThreadSize()) {// 最后一个线程不要-1
endPos += 1;
}
http.setRequestProperty("Range", "bytes=" + startPos + "-"
+ endPos);// 设置获取实体数据的范围
http.setRequestProperty(
"User-Agent",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
http.setRequestProperty("Connection", "Keep-Alive");
InputStream is = http.getInputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int offset = 0;
Log.i(TAG, "Thread " + this.threadId
+ " start download from position " + startPos
+ " end at " + endPos);
RandomAccessFile raf = new RandomAccessFile(this.saveFile,
"rwd");
raf.seek(startPos);
while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
if (cancel) {
Log.i(TAG, "is canel download = " + cancel);
break;
}
raf.write(buffer, 0, offset);
downLength += offset;
mDownloadHelper.update(this.threadId, downLength);
mDownloadHelper.saveLogFile();
mDownloadHelper.append(offset);
}
raf.close();
is.close();
http.disconnect();
Log.i(TAG, "Thread " + this.threadId + " download finish");
this.finish = true;
} catch (IOException e) {
Log.w(TAG, "occur ioexception.");
listener.onDownloadFailed(-1);
this.downLength = -1;
}
}
}
public void cancelDownload() {
cancel = true;
}
/**
* 下载是否完成
*
* @return
*/
public boolean isFinish() {
return finish;
}
/**
* 已经下载的内容大小
*
* @return 如果返回值为-1,代表下载失败
*/
public long getDownLength() {
return downLength;
}
}
DownloadListener就是下载时的同步回调,可以监测到目标文件大小、获取到当前下载的字节数、下载完成、下载失败的监听:
package com.genius.download.logic;
/**
* **************************************************************** 文件名称 :
* DownloadProgressListener.java 作 者 : mWX284740 创建时间 : 2015年8月19日 下午2:34:14
* 文件描述 : 下载文件的回调接口 版权声明 : Copyright (C) 2008-2011 华为技术有限公司(Huawei Tech.Co.,Ltd)
* 修改历史 : 2015年8月19日 1.00 初始版本
*****************************************************************
*/
public interface DownloadListener {
/**
* 下载了多少字节
* @param size
*/
public void onDownloadSize(int size);
/**
* 下载完成
*/
public void onDownloadCompleted();
/**
* 下载失败
* @param returnCode
*/
public void onDownloadFailed(int returnCode);
/**
* 获取到目标文件大小
* @param size
*/
public void onGetFileSize(int size);
}
本代码中还是有一些问题,因为涉及到多线程,各种场景的考虑不够全面,以后再行优化,不对的地方,还请大家谅解,谢谢!
项目源码