Android中多任务断点续传下载(二)

上一篇主要介绍了多任务断点续传下载的主要思路,这一篇将结合几个主要的类具体的说下是如何实现的。

先看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中……

github地址