在我们的开发过程中,经常会用到下载功能,也经常伴有显示下载进度的需求,什么下载文件啊,更新版本啊等等吧,今天给大家介绍一个带进度更新下载的开源框架—-android-downloader(下载地址),并跟大家一起看一下它的源码,熟悉一下它的实现过程。
封装单个下载任务包含的信息,包括要下载文件的名称、文件的下载地址、要下载到本地的路径、开始结束时间、文件类型、文件大小、下载监听(DownloadListener)等等,还有就是当前下载任务的开始,停止等方法。
下载任务回调类,包括onAdd(DownloadTask task)、onDelete(DownloadTask task)、onStop(DownloadTask task)、onStart()、onProgressUpdate(Integer... values)、onSuccess(DownloadTask task)、onCancelled()、onError(Throwable thr)、onFinish()方法。
真正实现下载的类,继承自AsyncTask类。
继承自Exception类的自定义错误提示类。
下载中的各种状态,包括STATUS_PENDING、STATUS_RUUUING、STATUS_STOPPED、STATUS_FINISHED、STATUS_FAILED、STATUS_DETELED几种状态。
下载文件的类型,包括Type_unknown、type_text、type_image、type_music、type_video、type_app几种类型。
OrmLite数据库操作相关类,保存下载任务信息,这里先不讲,知道有这么回事就行了,还是主要将下载更新相关实现。
至于怎么用大家可以网上去查一下,可以结合Listview一起用,我在这里只跟大家分析一下它的实现过程,,实现过程明白了,用起来也就得心应手了。很简单,大致的过程就是我们需要自己去new一个DownloadListener并实现里边的方法(在这里实现具体的更新操作),实例化DownLoadManager对象,当要建立下载任务时,每一个下载任务创建一个DownLoadTask对象并赋值(name、path、url等等),将当前对象与DownloadListener对象一起添加到DownLoadMagager中如下代码:
DownloadTask task = new DownloadTask(KnowledgeDocListActivity.this);
task.setUrl(Settings.DOMAINNAME + docEntity.getUrl());
task.setName(docEntity.getDocName());
task.setPath(Settings.WORDFILE + docEntity.getDocName());
task.setId(docEntity.getId());
mDownloadManager.add(task, listener);
那么,让我们看一看DownloadManager的add方法中做了什么:
/**
* Add Task
*
* @param task DownloadTask
* @return
*/
public void add(DownloadTask task, DownloadListener listener) {
Log.i("Add Task");
//判断task的可用性;
if (task == null || !task.isValid()) {
OnResult(POST_MESSAGE.ERROR, task, listener, DownloadException.DOWNLOAD_TASK_NOT_VALID);
return;
}
if (task.getContext() == null) {
//如果我们没有对task的Context属性赋值,在这里赋值;
//可以通过上边代码得知,在new DownloadTask(KnowledgeDocListActivity.this)的时候将Context传过来的;
task.setContext(context);
}
ISql iSql = new ISqlImpl(context);
DownloadTask temptask = null;
try {
//根据传入的task到OrmLite数据库查找;
temptask = iSql.queryDownloadTask(task);
if (temptask == null || !temptask.isValid() || !temptask.isComplete()) {
//判断task的name、path、url属性是否都存在;
if (task.isComplete()) {
iSql.addDownloadTask(task);
//发送添加task事件;
OnResult(POST_MESSAGE.ADD, task, listener, -1);
Log.i("The Task is stored in the sqlite.");
} else {
//如果没有name等属性,开启异步任务去网上获取,这里只是获取,没有下载;
task.start(context, listener, true);
}
} else {
//数据库中已经存在这个下载任务;
task.setDownloadTask(temptask);
OnResult(POST_MESSAGE.ADD, task, listener, -1);
Log.i("The Task is already stored in the sqlite.");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
看他的OnResult(Post_Message.*,task,listener,Integer)方法:
/**
* deal with the result
*
* @param message POST_MESSAGE
* @param listener DownloadListener
* @param code code
*/
@SuppressWarnings("rawtypes")
private void OnResult(final POST_MESSAGE message, final DownloadTask task, final DownloadListener listener, final Integer code) {
if (context == null || !(context instanceof Activity)) {
Log.w("The context is null or invalid!");
return;
}
((Activity) context).runOnUiThread(new Runnable() {
public void run() {
if (listener == null) {
return;
}
switch (message) {
case ADD:
listener.onAdd(task);
break;
case DELETE:
listener.onDelete(task);
break;
case START:
listener.onStart();
break;
case FINISH:
listener.onFinish();
break;
case STOP:
listener.onStop(task);
break;
case ERROR:
listener.onError(new DownloadException(code));
break;
}
}
});
}
private DownloadListener listener = new DownloadListener<Integer, DownloadTask>() {
@Override
public void onAdd(DownloadTask downloadTask) {
super.onAdd(downloadTask);
LogUtils.d("onAdd()");
mDownloadTasklist.add(downloadTask);
LogUtils.d("" + downloadTask);
mDocListAdapter.notifyDataSetChanged();
}
@Override
public void onDelete(DownloadTask downloadTask) {
super.onDelete(downloadTask);
LogUtils.d("onDelete()");
}
@Override
public void onStop(DownloadTask downloadTask) {
super.onStop(downloadTask);
LogUtils.d("onStop()");
}
/**
* Runs on the UI thread before doInBackground(Params...).
*/
@Override
public void onStart() {
super.onStart();
UIUtils.makeToast(KnowledgeDocListActivity.this, getResources().getString(R.string.start_download), AppMsg.STYLE_ALERT).show();
}
/**
* Runs on the UI thread after publishProgress(Progress...) is invoked. The
* specified values are the values passed to publishProgress(Progress...).
*
* @param values The values indicating progress.
*/
@Override
public void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
mDocListAdapter.notifyDataSetChanged();
LogUtils.d("onProgressUpdate");
}
/**
* Runs on the UI thread after doInBackground(Params...). The specified
* result is the value returned by doInBackground(Params...). This method
* won't be invoked if the task was cancelled.
*
* @param downloadTask The result of the operation computed by
* doInBackground(Params...).
*/
@Override
public void onSuccess(DownloadTask downloadTask) {
super.onSuccess(downloadTask);
LogUtils.d("异步URL:" + downloadTask.getUrl());
for (int i = 0; i < mDocList.size(); i++) {
LogUtils.d("docListURL:" + i + ":" + mDocList.get(i).getUrl());
if (downloadTask.getUrl().contains(mDocList.get(i).getUrl())) {
mDocList.get(i).setIsLocalHave(true);
break;
}
}
UIUtils.makeToast(KnowledgeDocListActivity.this, getResources().getString(R.string.download_success), AppMsg.STYLE_ALERT).show();
mDocListAdapter.notifyDataSetChanged();
LogUtils.d("onSuccess()");
}
/**
* Applications should preferably override onCancelled(Object). This method
* is invoked by the default implementation of onCancelled(Object). Runs on
* the UI thread after cancel(boolean) is invoked and
* doInBackground(Object[]) has finished.
*/
@Override
public void onCancelled() {
super.onCancelled();
LogUtils.d("onCancelled()");
}
@Override
public void onError(Throwable thr) {
super.onError(thr);
LogUtils.d("onError():" + thr.getMessage());
UIUtils.makeToast(KnowledgeDocListActivity.this, getResources().getString(R.string.down_fail), AppMsg.STYLE_ALERT).show();
}
/**
* Runs on the UI thread after doInBackground(Params...) when the task is
* finished or cancelled.
*/
@Override
public void onFinish() {
super.onFinish();
LogUtils.d("onFinish()");
}
};
里边有一些我的操作,就不删除了,可能会对大家理解有帮助,好,再看看我们DownLoadManager的Add方法中的其他操作:task.isComplete():
/**
* url, name and path is not empty.
*
* @return true is valid,otherwise not.
*/
public boolean isComplete() {
return !TextUtils.isEmpty(url) && !TextUtils.isEmpty(name) && !TextUtils.isEmpty(path);
}
task.start(context, listener, true):
/**
* Start the Task
*
* @param context Context
* @param listener DownloadListener
*/
@SuppressWarnings({
"rawtypes", "unchecked"
})
protected void start(Context context, DownloadListener listener, boolean isOnlyGetHead) {
//Get context,which will be used to communicate with sqlite.
if (this.context == null && context != null) {
this.context = context;
}
if (task != null) {
task.cancel(false);
}
<span style="white-space:pre"> </span>//AsycDownloadTask是继承了AsyncTask的一个类,
<span style="white-space:pre"> </span>//所以在这里他开启了异步去获取信息;
task = new AsycDownloadTask(listener, isOnlyGetHead);
task.execute(this);
}
看一下AsycDownloadTask的doInBackground(DownloadTask ... tasks)方法,也是最主要的一个方法:
/**
* TODO if error occurs,carry it out. if (listener != null) {
* listener.onError(new Throwable()); }
*/
protected DownloadTask doInBackground(DownloadTask... tasks) {
if (tasks.length <= 0) {
Log.e("There is no DownloadTask.");
return null;
}
DownloadTask task = tasks[0];
if (task == null || !task.isValid()) {
//发送task异常错误信息;
SendError(task, DownloadException.DOWNLOAD_TASK_NOT_VALID);
Log.e("The task is not valid,or the url of the task is not valid.");
return null;
}
String path = task.getPath();
File file = new File(path);
InputStream in = null;
RandomAccessFile out = null;
HttpURLConnection connection = null;
try {
long range = file.length();
long size = task.getSize();
long curSize = range;
String filename = task.getName();
String contentType = task.getMimeType();
//本地文件信息与数据库保存task信息一致,说明下载了这个任务;
if (task.getStatus() == DownloadStatus.STATUS_FINISHED && size == range) {
Log.i("The DownloadTask has already been downloaded.");
return task;
}
<span style="white-space:pre"> </span> //根据url获取网络文件相关信息;
String urlString = task.getUrl();
String cookies = null;
while (true) {
URL url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("User-Agent", "Snowdream Mobile");
connection.setRequestProperty("Connection", "Keep-Alive");
if (cookies != null && cookies != "") {
connection.setRequestProperty("Cookie", cookies);
}
connection.setRequestMethod("GET");
if (range > 0) {
connection.setRequestProperty("Range", "bytes=" + range +
"-");
}
//http auto redirection
//see: http://www.mkyong.com/java/java-httpurlconnection-follow-redirect-example/
boolean redirect = false;
boolean success = false;
// normally, 3xx is redirect
int status = connection.getResponseCode();
Log.i("HTTP STATUS CODE: " + status);
switch (status) {
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_PARTIAL:
success = true;
String transfer_encoding = connection.getHeaderField("Transfer-Encoding");
if (!TextUtils.isEmpty(transfer_encoding)
&& transfer_encoding.equalsIgnoreCase("chunked")) {
mode = MODE_TRUNKED;
Log.i("HTTP MODE: TRUNKED");
} else {
mode = MODE_DEFAULT;
Log.i("HTTP MODE: DEFAULT");
}
String accept_ranges = connection.getHeaderField("Accept-Ranges");
if (!TextUtils.isEmpty(accept_ranges)
&& accept_ranges.equalsIgnoreCase("bytes")) {
Log.i("Accept-Ranges: bytes");
} else {
range = 0;
Log.i("Accept-Ranges: none");
}
break;
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_SEE_OTHER:
redirect = true;
// get redirect url from "location" header field
urlString = connection.getHeaderField("Location");
// get the cookie if need, for login
cookies = connection.getHeaderField("Set-Cookie");
Log.i("Redirect Url : " + urlString);
break;
default:
success = false;
break;
}
if (!redirect) {
if (!success) {
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
Log.e("Http Connection error. ");
return null;
}
Log.i("Successed to establish the http connection.Ready to download...");
break;
}
}
if (range == 0) {
//set the whole file size
size = connection.getContentLength();
//赋值文件大小字段;
task.setSize(size);
if (contentType != connection.getContentType()) {
contentType = connection.getContentType();
//赋值类型;
task.setMimeType(contentType);
}
//auto get filename
if (TextUtils.isEmpty(filename)) {
String disposition = connection.getHeaderField("Content-Disposition");
if (disposition != null) {
// extracts file name from header field
final String FILENAME = "filename=";
final int startIdx = disposition.indexOf(FILENAME);
final int endIdx = disposition.indexOf(';', startIdx);
filename = disposition.substring(startIdx + FILENAME.length(), endIdx > 0 ? endIdx : disposition.length());
} else {
// extracts file name from URL
filename = urlString.substring(urlString.lastIndexOf("/") + 1,
urlString.length());
}
//赋值文件名称;
task.setName(filename);
}
//auto get filepath
if (TextUtils.isEmpty(path)) {
path = STORE_PATH + filename;
file = new File(path);
<span style="white-space:pre"> </span>//赋值path;
task.setPath(path);
}
task.setStartTime(System.currentTimeMillis());
<span style="white-space:pre"> </span>//下载任务添加到数据库;
SaveDownloadTask(task, task.getStatus());
Log.i("The Task is stored in the sqlite.");
<span style="white-space:pre"> </span>//如果是只获取信息,不下载文件,则发送调用DownLoadLisener的add方法,
<span style="white-space:pre"> </span>//并不进行后续下载操作;
if (isOnlyGetHead) {
SendAdd(task);
return null;
}
}
File dir = file.getParentFile();
if (!dir.exists() && !dir.mkdirs()) {
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
Log.e("The directory of the file can not be created!");
return null;
}
task.setStatus(DownloadStatus.STATUS_RUNNING);
SaveDownloadTask(task, task.getStatus());
Log.i("DownloadTask " + task);
out = new RandomAccessFile(file, "rw");
out.seek(range);
in = new BufferedInputStream(connection.getInputStream());
byte[] buffer = new byte[1024];
int nRead = 0;
int progress = -1;
boolean isFinishDownloading = true;
//下载操作;
while ((nRead = in.read(buffer, 0, 1024)) > 0) {
out.write(buffer, 0, nRead);
curSize += nRead;
if (size != 0) {
progress = (int) ((curSize * 100) / size);
}
//这里更新了进度条,注意注意... ...
publishProgress(progress);
Log.i("cur size:" + (curSize) + " total size:" + (size) + " cur progress:" + (progress));
if (isCancelled()) {
task.setStatus(DownloadStatus.STATUS_STOPPED);
isFinishDownloading = false;
break;
}
if (task.getStatus() != DownloadStatus.STATUS_RUNNING) {
isFinishDownloading = false;
break;
}
}
if (!isFinishDownloading) {
Log.w("The DownloadTask has not been completely downloaded.");
SaveDownloadTask(task, task.getStatus());
return null;
}
//when the mode is MODE_TRUNKED,set the latest size.
if (size == 0 && curSize != 0) {
task.setSize(curSize);
}
range = file.length();
size = task.getSize();
Log.i("range: " + range + " size: " + size);
if (range != 0 && range == size) {
Log.i("The DownloadTask has been successfully downloaded.");
task.setFinishTime(System.currentTimeMillis());
SaveDownloadTask(task, DownloadStatus.STATUS_FINISHED);
return task;
} else {
Log.i("The DownloadTask failed to downloaded.");
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
return null;
}
} catch (MalformedURLException e) {
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
e.printStackTrace();
} catch (ProtocolException e) {
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
e.printStackTrace();
} catch (FileNotFoundException e) {
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
e.printStackTrace();
} catch (IOException e) {
SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (connection != null) {
connection.disconnect();
}
}
return null;
}
看一下当前类里边的SendError与SendAdd方法:
private void SendError(DownloadTask task, Integer code) {
Log.e("Errors happen while downloading.");
SaveDownloadTask(task, DownloadStatus.STATUS_FAILED);
<span style="white-space:pre"> </span>//调用了当前类里边的一个sHandler对象;
sHandler.obtainMessage(
MESSAGE_POST_ERROR,
new AsyncTaskResult(this, task,
code)).sendToTarget();
}
private void SendAdd(DownloadTask task) {
sHandler.obtainMessage(
MESSAGE_POST_ADD,
new AsyncTaskResult(this, task,
-1)).sendToTarget();
}
看一下sHandler是个什么东东:
private static class InternalHandler extends Handler {
@Override
public void handleMessage(Message msg) {
AsyncTaskResult result = (AsyncTaskResult) msg.obj;
if (result == null || result.mTask == null || result.mTask.isCancelled()) {
Log.i("The asyncTask is not valid or cancelled!");
return;
}
switch (msg.what) {
case MESSAGE_POST_ERROR:
<span style="white-space:pre"> </span>//调用当前类的OnError方法;
((AsycDownloadTask) result.mTask).OnError(result.mDownloadTask, result.mData);
break;
case MESSAGE_POST_ADD:
//调用当前类的OnAdd方法;
((AsycDownloadTask) result.mTask).OnAdd(result.mDownloadTask);
break;
default:
break;
}
}
}
继续向下看对应的方法:
/**
* throw error
*
* @param task task
* @param code The code of the exception
*/
private void OnError(DownloadTask task, Integer code) {
if (listener != null) {
listener.onError(new DownloadException(code));
}
}
/**
* inform Add
*
* @param task task
*/
private void OnAdd(DownloadTask task) {
if (listener != null && listener instanceof DownloadListener) {
((DownloadListener) listener).onAdd(task);
}
}
可以发现到现在还是调用了DownloadListener对应的方法去进行相关操作;下面看一下下载过程中的更新方法:
publishProgress(progress);
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
得到Handler对象发送消息,看getHandler方法:
private static Handler getHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler();
}
return sHandler;
}
}
很简单的单例,下边看看这个Handler在哪:
public InternalHandler() {
super(Looper.getMainLooper());
}
主线程啊,看看他发送消息后的操作吧:
private static class InternalHandler extends Handler {
public InternalHandler() {
super(Looper.getMainLooper());
}
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
//最终在这里调用了更新操作;
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
好了,到这里添加向DownLoadMagager中添加task就说完了,其实下载的时候是调用了DownLoadMagager的start方法,在start方法中也是调用了DownloadTask的start方法跟DownLoadMagager的添加方法中调用DownloadTask的start方法差不多,只不过这次最后一个参数传的是false,最后一个参数的含义就是是不是只获取文件信息,下边看一下实现:
**
* Start Task
*
* @param task
* @param listener
* @return
*/
@SuppressWarnings("rawtypes")
public boolean start(DownloadTask task, DownloadListener listener) {
Log.i("Start Task");
boolean ret = false;
if (task == null) {
OnResult(POST_MESSAGE.ERROR, task, listener, DownloadException.DOWNLOAD_TASK_NOT_VALID);
return ret;
}
if (task.getContext() == null) {
task.setContext(context);
}
ISql iSql = new ISqlImpl(context);
DownloadTask temptask = null;
try {
temptask = iSql.queryDownloadTask(task);
if (temptask == null) {
add(task, listener);
} else if (!temptask.equals(task)) {
task.setDownloadTask(temptask);
}
switch (task.getStatus()) {
case DownloadStatus.STATUS_RUNNING:
OnResult(POST_MESSAGE.START, task, listener, -1);
OnResult(POST_MESSAGE.FINISH, task, listener, -1);
Log.i("The Task is already Running.");
break;
default:
if (listener != null) {
//这里就跟添加task时一样了,注意最后一个参数;
task.start(context, listener, false);
}
break;
}
ret = true;
} catch (SQLException e) {
e.printStackTrace();
}
return ret;
}
好,到现在为止添加跟下载就说完了,还有stop方法等等,也不难,感兴趣的可以自己看一下,我这里就不再说了。上边的实现代码很简单,我也加了注释,希望这篇文章对大家了解android-downloader有帮助。