上一篇主要介绍了多任务断点续传下载的主要思路,这一篇将结合几个主要的类具体的说下是如何实现的。
先看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中……