上一篇主要介绍了多任务断点续传下载的主要思路,这一篇将结合几个主要的类具体的说下是如何实现的。
先看ServiceManager类
/**
* Copyright (c) www.bugull.com
*/
package com.ldw.downloader.service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.ldw.downloader.aidl.IDownloadService;
import com.ldw.downloader.utils.DownloadConstants;
public class ServiceManager {
private final String TAG = ServiceManager.class.getSimpleName();
private Context mContext;
private ServiceConnection mConn;
private IDownloadService mService;
private static ServiceManager mManager;
public static ServiceManager getInstance(Context context) {
if(mManager == null) {
mManager = new ServiceManager(context);
}
return mManager;
}
private ServiceManager(Context context) {
this.mContext = context;
initConn();
}
private void initConn() {
mConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IDownloadService.Stub.asInterface(service);
}
};
connectService();
}
public void connectService() {
Intent intent = new Intent(DownloadConstants.SERVICE_ACTION);
mContext.bindService(intent, mConn, Context.BIND_AUTO_CREATE);
}
public void disConnectService() {
mContext.unbindService(mConn);
mContext.stopService(new Intent(DownloadConstants.SERVICE_ACTION));
}
public void addTask(String url) {
if(mService != null) {
try {
mService.addTask(url);
} catch (RemoteException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
public void pauseTask(String url) {
if(mService != null) {
try {
mService.pauseTask(url);
} catch (RemoteException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
public void deleteTask(String url) {
if(mService != null) {
try {
mService.deleteTask(url);
} catch (RemoteException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
public void continueTask(String url) {
if(mService != null) {
try {
mService.continueTask(url);
} catch (RemoteException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
}
DownloadService类:
/**
* Copyright (c) www.bugull.com
*/
package com.ldw.downloader.service;
import com.ldw.downloader.aidl.IDownloadService;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
/**
* @author longdw(longdawei1988@gmail.com)
*
* 2014-1-13
*/
public class DownloadService extends Service {
private DownloadControl mControl;
@Override
public IBinder onBind(Intent intent) {
return new ServiceStub();
}
@Override
public void onCreate() {
super.onCreate();
mControl = new DownloadControl(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
private class ServiceStub extends IDownloadService.Stub {
@Override
public void addTask(String url) throws RemoteException {
if(!TextUtils.isEmpty(url)) {
mControl.addTask(url);
}
}
@Override
public void pauseTask(String url) throws RemoteException {
if(!TextUtils.isEmpty(url)) {
mControl.pauseTask(url);
}
}
@Override
public void deleteTask(String url) throws RemoteException {
if(!TextUtils.isEmpty(url)) {
mControl.deleteTask(url);
}
}
@Override
public void continueTask(String url) throws RemoteException {
if(!TextUtils.isEmpty(url)) {
mControl.continueTask(url);
}
}
}
}
这里我修改了原作者的做法,改为AIDL,其实性能上没什么变化,只是原文写的是每添加一个下载任务就启动一次service并携带上相应的参数,这种做法我感觉不简洁,就改成使用AIDL来操作service,可能有人会有疑问,为什么不直接调用DownloadControl里面的方法,我的回答是都可以,用Service的好处是在程序退出后进程不容易被系统杀死,还有个好处是在程序退出后也可以控制下载任务,比方说在状态栏中来控制等。
AIDL的具体知识这里就不多说了,网上资料很多,想要开启下载任务很简单,只需要调用ServiceManager中相应的方法就行了。
具体看下下面两个类,一个是核心控制DownloadControl类,控制下载,接收下载的状态数据(进度、速度等);一个是下载类DownloadTask类,真正负责下载并将下载的状态存入数据库中。
DownloadControl类:
/**
* Copyright (c) www.bugull.com
*/
package com.ldw.downloader.service;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
import com.ldw.downloader.db.DownloadDao;
import com.ldw.downloader.service.DownloadTask.DownloadTaskListener;
import com.ldw.downloader.utils.DownloadConstants;
import com.ldw.downloader.utils.MyIntents;
import com.ldw.downloader.utils.NetworkUtils;
import com.ldw.downloader.utils.StorageUtils;
/**
* 下载核心控制器
*
* @author longdw(longdawei1988@gmail.com)
*
* 2014-1-13
*/
public class DownloadControl extends Thread {
private static final String TAG = DownloadControl.class.getSimpleName();
private static final int MAX_TASK_COUNT = 100;
private static final int MAX_DOWNLOAD_THREAD_COUNT = 3;
private Context mContext;
/** 等待下载的下载队列 */
private TaskQueue mTaskQueue;
/** 正在下载的任务 */
private List<DownloadTask> mDownloadingTasks;
/** 已经暂停的任务 */
private List<DownloadTask> mPausedTasks;
private boolean isRunning = false;
private DownloadDao mDao;
public DownloadControl(Context context) {
mContext = context;
mDao = new DownloadDao(context);
mTaskQueue = new TaskQueue();
mDownloadingTasks = new ArrayList<DownloadTask>();
mPausedTasks = new ArrayList<DownloadTask>();
try {
StorageUtils.mkdir();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
while (isRunning) {
DownloadTask task = mTaskQueue.poll();
mDownloadingTasks.add(task);
task.execute();
}
}
private DownloadTask newDownloadTask(String url)
throws MalformedURLException {
DownloadTaskListener listener = new DownloadTaskListener() {
@Override
public void updateProgress(DownloadTask task) {
Intent updateIntent = new Intent(
DownloadConstants.RECEIVER_ACTION);
updateIntent.putExtra(MyIntents.TYPE, MyIntents.Types.PROCESS);
// updateIntent.putExtra(MyIntents.PROCESS_SPEED,
// task.getDownloadSpeed());
// updateIntent.putExtra("speed",
// task.getDownloadSpeed());
long percent = task.getDownloadPercent();
System.out.println(percent);
mDao.updateCurrentSizeByUrl(task.getUrl(), task.getDownloadSize());
// updateIntent.putExtra("progress",String.valueOf(percent));
updateIntent.putExtra(MyIntents.PROCESS_PROGRESS,
String.valueOf(percent));
updateIntent.putExtra(MyIntents.URL, task.getUrl());
// LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(updateIntent);
mContext.sendBroadcast(updateIntent);
}
@Override
public void finishDownload(DownloadTask task) {
completeTask(task, MyIntents.Types.COMPLETE);
}
@Override
public void errorDownload(DownloadTask task, Throwable error) {
errorTask(task, error);
}
};
return new DownloadTask(mContext, url, StorageUtils.FILE_ROOT, listener);
}
public void addTask(String url) {
if (!StorageUtils.isSDCardPresent()) {
Toast.makeText(mContext, "未发现SD卡", Toast.LENGTH_LONG).show();
return;
}
if (!StorageUtils.isSdCardWrittenable()) {
Toast.makeText(mContext, "SD卡不能读写", Toast.LENGTH_LONG).show();
return;
}
if (getTotalTaskCount() >= MAX_TASK_COUNT) {
Toast.makeText(mContext, "任务列表已满", Toast.LENGTH_LONG).show();
return;
}
try {
addTask(newDownloadTask(url));
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage(), e);
}
}
private void addTask(DownloadTask task) {
waitTask(task);
mTaskQueue.offer(task);
if (!this.isAlive()) {
isRunning = true;
this.start();
}
}
public void pauseTask(String url) {
for (int i = 0; i < mDownloadingTasks.size(); i++) {
DownloadTask task = mDownloadingTasks.get(i);
if (task != null && task.getUrl().equals(url)) {
pauseTask(task);
break;
}
}
}
private void pauseTask(DownloadTask task) {
if (task != null) {
task.pause();
String url = task.getUrl();
try {
mDownloadingTasks.remove(task);
task = newDownloadTask(url);
mPausedTasks.add(task);
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
public void deleteTask(String url) {
DownloadTask task;
// 如果是正在下载的任务删除了
for (int i = 0; i < mDownloadingTasks.size(); i++) {
task = mDownloadingTasks.get(i);
if (task != null && task.getUrl().equals(url)) {
File file = new File(StorageUtils.FILE_ROOT
+ NetworkUtils.getFileNameFromUrl(task.getUrl()));
if (file.exists())
file.delete();
task.delete();
completeTask(task, MyIntents.Types.DELETE);
break;
}
}
// 如果是待下载的任务删除了
for (int i = 0; i < mTaskQueue.size(); i++) {
task = mTaskQueue.get(i);
if (task != null && task.getUrl().equals(url)) {
mTaskQueue.remove(task);
break;
}
}
// 如果是暂停的任务删除了
for (int i = 0; i < mPausedTasks.size(); i++) {
task = mPausedTasks.get(i);
if (task != null && task.getUrl().equals(url)) {
mPausedTasks.remove(task);
break;
}
}
}
/**
* addTask到真正开始下载有个等待时间
*/
private void waitTask(DownloadTask task) {
Intent nofityIntent = new Intent(DownloadConstants.RECEIVER_ACTION);
nofityIntent.putExtra(MyIntents.TYPE, MyIntents.Types.WAIT);
nofityIntent.putExtra(MyIntents.URL, task.getUrl());
mContext.sendBroadcast(nofityIntent);
}
private void completeTask(DownloadTask task, int type) {
if (mDownloadingTasks.contains(task)) {
mDownloadingTasks.remove(task);
Intent nofityIntent = new Intent(DownloadConstants.RECEIVER_ACTION);
nofityIntent.putExtra(MyIntents.TYPE, type);
nofityIntent.putExtra(MyIntents.URL, task.getUrl());
mContext.sendBroadcast(nofityIntent);
}
}
private void errorTask(DownloadTask task, Throwable error) {
if(mDownloadingTasks.contains(task)) {
mDownloadingTasks.remove(task);
Intent errorIntent = new Intent(DownloadConstants.RECEIVER_ACTION);
errorIntent.putExtra(MyIntents.TYPE, MyIntents.Types.ERROR);
if (error != null) {
// errorIntent.putExtra(MyIntents.ERROR_CODE, error);
errorIntent.putExtra(MyIntents.ERROR_INFO,
error.getMessage());
}
errorIntent.putExtra(MyIntents.URL, task.getUrl());
mContext.sendBroadcast(errorIntent);
}
}
public void continueTask(String url) {
for (int i = 0, length = mPausedTasks.size(); i < length; i++) {
DownloadTask task = mPausedTasks.get(i);
if (task != null && task.getUrl().equals(url)) {
continueTask(task);
break;
}
}
}
private void continueTask(DownloadTask task) {
if (task != null) {
mPausedTasks.remove(task);
mTaskQueue.offer(task);
}
}
private int getTotalTaskCount() {
return mTaskQueue.size() + mDownloadingTasks.size()
+ mPausedTasks.size();
}
class TaskQueue {
private Queue<DownloadTask> taskQueue;
public TaskQueue() {
taskQueue = new LinkedList<DownloadTask>();
}
public void offer(DownloadTask task) {
taskQueue.offer(task);
}
public DownloadTask poll() {
DownloadTask task = null;
while (mDownloadingTasks.size() >= MAX_DOWNLOAD_THREAD_COUNT
|| (task = taskQueue.poll()) == null) {
try {
Thread.sleep(1000); // sleep
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return task;
}
public DownloadTask get(int position) {
if (position >= size()) {
return null;
}
return ((LinkedList<DownloadTask>) taskQueue).get(position);
}
public int size() {
return taskQueue.size();
}
public boolean remove(int position) {
return taskQueue.remove(get(position));
}
public boolean remove(DownloadTask task) {
return taskQueue.remove(task);
}
}
}
核心思想上一篇我已经用流程图简单的说明了下,可以对照着看该类。首先声明了一个QUEUE来保存待下载的任务,用户每添加一个下载任务就会往该queue中自动加入,主要看TaskQueue内部类中的poll方法,该方法不断的从QUEUE中取下载任务,一旦task任务没了或者当前正在下载的任务个数达到了设定的MAX_DOWNLOAD_THREAD_COUNT上限就开始等1s钟,而run方法中poll到了任务后就往downloadingTask(保存正在下载的任务)中加入一个任务。另外pause,delete,continue方法实现原理都很类似。该类中还定义了一个回调用来负责监听下载类DownloadTask中下载的状态。
DownloadTask:
/**
* Copyright (c) www.bugull.com
*/
package com.ldw.downloader.service;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.os.AsyncTask;
import com.ldw.downloader.db.DownloadDao;
import com.ldw.downloader.error.DownloadException;
import com.ldw.downloader.http.AndroidHttpClient;
import com.ldw.downloader.model.Downloader;
import com.ldw.downloader.utils.DownloadConstants;
import com.ldw.downloader.utils.NetworkUtils;
import com.ldw.downloader.utils.StatusCode;
import com.ldw.downloader.utils.StorageUtils;
public class DownloadTask extends AsyncTask<Void, Integer, Long> {
public interface DownloadTaskListener {
public void updateProgress(DownloadTask task);
public void finishDownload(DownloadTask task);
public void errorDownload(DownloadTask task, Throwable error);
}
private static final String TAG = DownloadTask.class.getSimpleName();
private static final String TEMP_SUFFIX = ".download";
private static final int BUFFER_SIZE = 8 * 1024;
private DownloadTaskListener mListener;
private String mUrl;
private Context mContext;
private File mFile;
private File mTempFile;
/** 文件大小 */
private long mTotalSize;
/** 之前已经没有下载完的文件大小 */
private long mPreviousFileSize;
/** 下载的大小 */
private long mDownloadSize;
/** 下载百分比 */
private long mDownloadPercent;
/** 下载速度 */
private long mDownloadSpeed;
/** 开始下载的时间 */
private long mStartTime;
private boolean mInterrupt = false;
private Throwable mError = null;
private AndroidHttpClient mHttpClient;
private DownloadDao mDao;
private final class ProgressReportingRandomAccessFile extends
RandomAccessFile {
private int progress = 0;
public ProgressReportingRandomAccessFile(File file, String mode)
throws FileNotFoundException {
super(file, mode);
}
@Override
public void write(byte[] buffer, int byteOffset, int byteCount)
throws IOException {
super.write(buffer, byteOffset, byteCount);
progress += byteCount;
publishProgress(progress);
}
}
public DownloadTask(Context context, String url, String savedPath,
DownloadTaskListener l) throws MalformedURLException {
mContext = context;
mUrl = url;
mListener = l;
URL u = new URL(url);
String name = (new File(u.getFile())).getName();
mFile = new File(savedPath, name);
mTempFile = new File(savedPath, name + TEMP_SUFFIX);
mDao = new DownloadDao(context);
Downloader d = new Downloader();
d.setUrl(url);
d.setName(name);
d.setSavedPath(savedPath);
mDao.save(d);
}
@Override
protected void onPreExecute() {
mStartTime = System.currentTimeMillis();
}
int i = 0;
@Override
protected Long doInBackground(Void... params) {
long result = -1;
try {
result = download();
} catch (NetworkErrorException e) {
mError = e;
} catch (DownloadException e) {
mError = e;
} catch (IOException e) {
mError = e;
}
return result;
}
@Override
protected void onPostExecute(Long result) {
if (result == -1 || mInterrupt || mError != null) {
//下载过程中遇到错误就重置下载状态
mDao.updateStatusByUrl(mUrl, DownloadConstants.STATUS_PAUSE);
if (mListener != null) {
mListener.errorDownload(this, mError);
}
return;
}
/*
* finish download
*/
mTempFile.renameTo(mFile);
//下载完成更新下载状态为等待安装状态
mDao.updateStatusByUrl(mUrl, DownloadConstants.STATUS_INSTALL);
if (mListener != null) {
mListener.finishDownload(this);
}
}
public void pause() {
onCancelled();
mDao.updateStatusByUrl(mUrl, DownloadConstants.STATUS_PAUSE);
}
public void delete() {
onCancelled();
mDao.deleteByUrl(mUrl);
}
@Override
protected void onCancelled() {
super.onCancelled();
mInterrupt = true;
}
@Override
protected void onProgressUpdate(Integer... values) {
if (values.length > 1) {// 下载开始后会走到这里
mTotalSize = values[1];
//避免暂停后然后继续下载会短暂的出现0%的情况需要计算下载百分比
mDownloadPercent = (mDownloadSize + mPreviousFileSize) * 100 / mTotalSize;
if (mListener != null) {
mListener.updateProgress(this);
}
mDao.updateStatusByUrl(mUrl, DownloadConstants.STATUS_DOWNLOADING);
mDao.updateTotalSizeByUrl(mUrl, mTotalSize);
} else {
mDownloadSize = values[0];
long totalTime = System.currentTimeMillis() - mStartTime;
long tempSize = mDownloadSize + mPreviousFileSize;
// mDao.updateCurrentSizeByUrl(mUrl, tempSize);
mDownloadSpeed = mDownloadSize / totalTime;// kbps
long temp = tempSize * 100 / mTotalSize;
if(mDownloadPercent != temp) {
mDownloadPercent = temp;
if (mListener != null) {
mListener.updateProgress(this);
}
}
// mDownloadPercent = tempSize * 100 / mTotalSize;
// if (mListener != null) {
// mListener.updateProgress(this);
// }
}
}
private long download() throws NetworkErrorException, IOException,
DownloadException {
/*
* check net work
*/
if (!NetworkUtils.isNetworkAvailable(mContext)) {
throw new NetworkErrorException();
}
/*
* check file length
*/
mHttpClient = AndroidHttpClient.newInstance(TAG);
HttpGet httpGet = new HttpGet(mUrl);
HttpResponse response = mHttpClient.execute(httpGet);
mTotalSize = response.getEntity().getContentLength();
if (mTotalSize < 1024) {
throw new DownloadException(StatusCode.ERROR_URL);
}
if (mFile.exists() && mFile.length() == mTotalSize) {
throw new DownloadException(StatusCode.ERROR_FILE_EXIST);
} else if (mTempFile.exists()) {// 已经下载过了,断点下载
mPreviousFileSize = mTempFile.length() - 1;
httpGet.addHeader("Range", "bytes=" + mPreviousFileSize + "-");
mHttpClient.close();
mHttpClient = AndroidHttpClient.newInstance(TAG);
response = mHttpClient.execute(httpGet);
}
/*
* check memory
*/
long storage = StorageUtils.getAvailableStorage();
if (mTotalSize - mTempFile.length() > storage) {
throw new DownloadException(StatusCode.ERROR_NOMEMORY);
}
/*
* start download
*/
RandomAccessFile accessFile = new ProgressReportingRandomAccessFile(
mTempFile, "rw");
// 提交当前下载文件大小
publishProgress(0, (int) mTotalSize);
InputStream inputStream = response.getEntity().getContent();
int bytesCopied = copy(inputStream, accessFile);
if ((mPreviousFileSize + bytesCopied) != mTotalSize && mTotalSize != 0
&& !mInterrupt) {
throw new DownloadException(StatusCode.ERROR_DOWNLOAD_INTERRUPT);
}
return bytesCopied;
}
private int copy(InputStream inputStream, RandomAccessFile accessFile)
throws IOException {
if (inputStream == null || accessFile == null) {
return -1;
}
byte[] buffer = new byte[BUFFER_SIZE];
BufferedInputStream bis = new BufferedInputStream(inputStream,
BUFFER_SIZE);
int totalCount = 0, readCount = 0;
try {
accessFile.seek(mPreviousFileSize);
while (!mInterrupt) {
readCount = inputStream.read(buffer, 0, BUFFER_SIZE);
if (readCount == -1) {
break;
}
accessFile.write(buffer, 0, readCount);
totalCount += readCount;
System.out.println(totalCount+"---------------");
}
} finally {
mHttpClient.close();
mHttpClient = null;
accessFile.close();
inputStream.close();
bis.close();
}
return totalCount;
}
public String getUrl() {
return mUrl;
}
public long getDownloadPercent() {
return mDownloadPercent;
}
public long getDownloadSpeed() {
return mDownloadSpeed;
}
public long getDownloadSize() {
return mDownloadSize + mPreviousFileSize;
}
}
下载类这里就不作详细介绍了,可以看代码,需要注意的是设置断点的地方httpGet.addHeader(“Range”, “bytes=” + size+ “-“);和accessFile.seek(size);注意size的大小是已经下载文件的长度-1,貌似原作者这地方好像写错了,如果没有下载就从0开始。其他的类大家可以对照着工程代码来看,主要是处理UI。稍后我会把工程加入github中……