Android 文件下載(斷點、多任務並行下載)

以下代碼是基於百度雲網盤:http://pan.baidu.com/s/1dD1Xo8T 中的demo進行優化及功能添加。
以下代碼實現功能有:多線程下載、多任務並行下載以及下載進度和下載速度的顯示等功能。
實現思路:根據線程數分割待下載文件;利用HttpURLConnection實現各部分文件的下載;利用RandomAccessFile實現下載內容的保存;各線程下載任務信息保存在數據庫,以便暫停和恢復下載。
demo已上傳到github:https://github.com/shichaohui/FileDownloadDemo.git 歡迎下載。
效果圖:
這裏寫圖片描述

主要代碼

DownLoadHelper.java:

package com.example.test;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * 利用數據庫來記錄下載信息
 * 
 * @author [email protected]
 */
public class DownLoadHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "download.db";
    private static final String TB_NAME = "download_info";
    private static final int DOWNLOAD_VERSION = 1;

    public DownLoadHelper(Context context) {
        super(context, DB_NAME, null, DOWNLOAD_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("create table "
                + TB_NAME
                + "(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer, "
                + "start_pos integer, end_pos integer, compelete_size integer,url char)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

}

這個類沒什麼好說的,就是創建數據庫和數據表。

DownlaodSqlTool.java

package com.example.test;

import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

/**
 * 數據庫操作工具類
 * 
 * @author [email protected]
 */
public class DownlaodSqlTool {

    private static DownlaodSqlTool instance = null;
    private DownLoadHelper dbHelper = null;

    private DownlaodSqlTool(Context context) {
        dbHelper = new DownLoadHelper(context);
    }

    private static synchronized void syncInit(Context context) {
        if (instance == null) {
            instance = new DownlaodSqlTool(context);
        }
    }

    public static DownlaodSqlTool getInstance(Context context) {
        if (instance == null) {
            syncInit(context);
        }
        return instance;
    }

    /** 將下載的進度等信息保存到數據庫 */
    public void insertInfos(List<DownloadInfo> infos) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        for (DownloadInfo info : infos) {
            String sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,url) values (?,?,?,?,?)";
            Object[] bindArgs = { info.getThreadId(), info.getStartPos(),
                    info.getEndPos(), info.getCompeleteSize(), info.getUrl() };
            database.execSQL(sql, bindArgs);
        }
    }

    /** 獲取下載的進度等信息 */
    public List<DownloadInfo> getInfos(String urlstr) {
        List<DownloadInfo> list = new ArrayList<DownloadInfo>();
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        String sql = "select thread_id, start_pos, end_pos,compelete_size,url from download_info where url=?";
        Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
        while (cursor.moveToNext()) {
            DownloadInfo info = new DownloadInfo(cursor.getInt(0),
                    cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),
                    cursor.getString(4));
            list.add(info);
        }
        cursor.close();
        return list;
    }

    /** 更新數據庫中的下載信息 */
    public void updataInfos(int threadId, int compeleteSize, String urlstr) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        String sql = "update download_info set compelete_size=? where thread_id=? and url=?";
        Object[] bindArgs = { compeleteSize, threadId, urlstr };
        database.execSQL(sql, bindArgs);
    }

    /** 關閉數據庫 */
    public void closeDb() {
        dbHelper.close();
    }

    /** 刪除數據庫中的數據 */
    public void delete(String url) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        database.delete("download_info", "url=?", new String[] { url });
    }
}

單例模式的數據庫操作類,主要實現數據的增刪改查等操作。

DownloadInfo.java

package com.example.test;

/**
 * 保存每個下載線程下載信息類
 * 
 * @author [email protected]
 */
public class DownloadInfo {

    private int threadId; // 下載線程的id
    private int startPos; // 開始點
    private int endPos; // 結束點
    private int compeleteSize; // 完成度
    private String url; // 下載文件的URL地址

    /**
     * 
     * @param threadId
     *            下載線程的id
     * @param startPos
     *            開始點
     * @param endPos
     *            結束點
     * @param compeleteSize
     *            // 已下載的大小
     * @param url
     *            下載地址
     */
    public DownloadInfo(int threadId, int startPos, int endPos,
            int compeleteSize, String url) {
        this.threadId = threadId;
        this.startPos = startPos;
        this.endPos = endPos;
        this.compeleteSize = compeleteSize;
        this.url = url;
    }

    public DownloadInfo() {
    }

    /** 獲取下載地址 */
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    /** 獲取下載線程的Id */
    public int getThreadId() {
        return threadId;
    }

    public void setThreadId(int threadId) {
        this.threadId = threadId;
    }

    /** 獲取下載的開始位置 */
    public int getStartPos() {
        return startPos;
    }

    public void setStartPos(int startPos) {
        this.startPos = startPos;
    }

    /** 獲取下載的結束位置 */
    public int getEndPos() {
        return endPos;
    }

    public void setEndPos(int endPos) {
        this.endPos = endPos;
    }

    /** 獲取已下載的大小 */
    public int getCompeleteSize() {
        return compeleteSize;
    }

    public void setCompeleteSize(int compeleteSize) {
        this.compeleteSize = compeleteSize;
    }

    @Override
    public String toString() {
        return "DownloadInfo [threadId=" + threadId + ", startPos=" + startPos
                + ", endPos=" + endPos + ", compeleteSize=" + compeleteSize
                + "]";
    }
}

下載實體類,針對於單個下載線程,保存下載線程對應的文件相關信息,比如當前下載線程負責下載的部分是從文件的哪個點開始的(startPos)、哪個點結束的(endPos)以及當前已經下載了多少(compeleteSize)等信息。

DownloadingInfo.java

package com.example.test;

/**
 * 某一任務正在下載時的信息
 * 
 * @author [email protected]
 * 
 */
public class DownloadingInfo {

    private String kbps = "0"; // 每秒下載速度
    private int secondSize = 0; // 一秒鐘累計下載量
    private int fileSize = 0; // 文件大小

    public String getKbps() {
        return kbps;
    }

    public void setKbps(String kbps) {
        this.kbps = kbps;
    }

    public int getSecondSize() {
        return secondSize;
    }

    public void setSecondSize(int secondSize) {
        this.secondSize = secondSize;
    }

    public int getFileSize() {
        return fileSize;
    }

    public void setFileSize(int fileSize) {
        this.fileSize = fileSize;
    }

    @Override
    public String toString() {
        return "DownloadingInfo [kbps=" + kbps + ", secondSize=" + secondSize
                + ", fileSize=" + fileSize + "]";
    }

}

這也是一個下載相關的實體類,針對於一個下載任務(包括多個下載線程)。保存下載任務的下載進度、速度等用於客戶端顯示的數據。

DownloadHttpTool.java

package com.example.test;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;

/**
 * 利用Http協議進行多線程下載具體實現類
 * 
 * @author [email protected]
 */
public class DownloadHttpTool {

    private final int THREAD_COUNT = 2; // 線程數量
    private String urlstr = ""; // URL地址
    private Context mContext = null;
    private List<DownloadInfo> downloadInfos = null; // 保存下載信息的類
    /** 下載文件保存路徑 */
    public static String filePath = ""; // 目錄
    private String fileName = ""; // 文件名
    private String fileNameTmp = ""; // 臨時文件名
    /** 臨時文件名後綴 */
    public static final String FILE_TMP_SUFFIX = ".tmp";
    private int fileSize = 0; // 文件大小
    private DownlaodSqlTool sqlTool = null; // 文件信息保存的數據庫操作類
    private DownloadComplated downloadComplated = null;
    private int totalCompelete = 0;// 所有線程已下載的總數
    private List<DownloadThread> threads = null; // 下載線程
    private Handler handler = null;

    // 利用枚舉表示下載的幾種狀態
    private enum Download_State {
        Downloading, Pause, Ready, Compeleted, Exception;
    }

    private Download_State state = Download_State.Ready; // 當前下載狀態

    /**
     * @param context
     *            上下文對象
     * @param downloadComplated
     */
    public DownloadHttpTool(Context context, Handler handler,
            DownloadComplated downloadComplated) {
        super();
        this.mContext = context;
        this.handler = handler;
        this.downloadComplated = downloadComplated;
        sqlTool = DownlaodSqlTool.getInstance(mContext);
        if ("".equals(filePath)) {
            // TODO 根據有無sdcard設置路徑
            filePath = Environment.getExternalStorageDirectory()
                    .getAbsolutePath() + "/meiriq-download";
        }
        threads = new ArrayList<DownloadThread>();
    }

    /**
     * 開始下載
     * 
     * @param url
     *            下載地址
     */
    public void start(String urlstr) {
        this.urlstr = urlstr;
        String[] ss = urlstr.split("/");
        fileName = ss[ss.length - 1];
        fileNameTmp = fileName + FILE_TMP_SUFFIX;

        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... arg0) {
                // 下載之前首先異步線程調用ready方法做下載的準備工作
                ready();
                Message msg = new Message();
                msg.what = 1;
                msg.arg1 = fileSize;
                msg.obj = DownloadHttpTool.this.urlstr;
                handler.sendMessage(msg);
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                super.onPostExecute(result);
                // 開始下載
                startDownload();
            }
        }.execute();
    }

    /** 在開始下載之前需要調用ready方法進行配置 */
    private void ready() {
        if (new File(filePath + "/" + fileName).exists()) {
            downloadComplated.onComplated(urlstr);
            return;
        }
        totalCompelete = 0;
        downloadInfos = sqlTool.getInfos(urlstr);
        if (downloadInfos.size() == 0) { // 數據庫中沒有相關信息
            initFirst();
        } else {
            File file = new File(filePath + "/" + fileNameTmp);
            if (!file.exists()) {
                sqlTool.delete(urlstr);
                initFirst();
            } else {
                fileSize = downloadInfos.get(downloadInfos.size() - 1)
                        .getEndPos();
                for (DownloadInfo info : downloadInfos) {
                    totalCompelete += info.getCompeleteSize();
                }
            }
        }
    }

    /** 開始下載 */
    private void startDownload() {
        if (downloadInfos != null) {
            if (state == Download_State.Downloading) {
                return;
            }
            state = Download_State.Downloading;
            for (DownloadInfo info : downloadInfos) { // 開啓線程下載
                DownloadThread thread = new DownloadThread(info.getThreadId(),
                        info.getStartPos(), info.getEndPos(),
                        info.getCompeleteSize(), info.getUrl());
                thread.start();
                threads.add(thread);
            }
        }
    }

    /** 暫停當前下載任務 */
    public void pause() {
        state = Download_State.Pause;
    }

    /** 刪除當前下載任務 */
    public void delete() {
        compeleted();
        File file = new File(filePath + "/" + fileNameTmp);
        file.delete();
    }

    /** 完成下載 */
    private void compeleted() {
        state = Download_State.Compeleted;
        sqlTool.delete(urlstr);
        downloadComplated.onComplated(urlstr);
    }

    /** 獲取目標文件大小 */
    public int getFileSize() {
        return fileSize;
    }

    /** 獲取當前下載的大小 */
    public int getTotalCompeleteSize() {
        return totalCompelete;
    }

    /** 第一次下載時進行的初始化 */
    private void initFirst() {
        URL url = null;
        RandomAccessFile accessFile = null;
        HttpURLConnection connection = null;
        try {
            url = new URL(urlstr);
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(5000);
            connection.setRequestMethod("GET");
            fileSize = connection.getContentLength();
            if (fileSize < 0) {
                return;
            }

            File fileParent = new File(filePath);
            if (!fileParent.exists()) {
                fileParent.mkdir();
            }
            File file = new File(fileParent, fileNameTmp);
            if (!file.exists()) {
                file.createNewFile();
            }
            // 隨機訪問文件
            accessFile = new RandomAccessFile(file, "rwd");
            accessFile.setLength(fileSize);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (accessFile != null) {
                try {
                    accessFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
        // 計算每個線程需要下載的大小
        int range = fileSize / THREAD_COUNT;
        // 保存每個線程的下載信息
        downloadInfos = new ArrayList<DownloadInfo>();
        for (int i = 0; i < THREAD_COUNT - 1; i++) {
            DownloadInfo info = new DownloadInfo(i, i * range, (i + 1) * range
                    - 1, 0, urlstr);
            downloadInfos.add(info);
        }
        // 最後一個線程和前面的處理有點不一樣
        DownloadInfo info = new DownloadInfo(THREAD_COUNT - 1,
                (THREAD_COUNT - 1) * range, fileSize - 1, 0, urlstr);
        downloadInfos.add(info);
        // 插入到數據庫
        sqlTool.insertInfos(downloadInfos);
    }

    interface DownloadComplated {

        /**
         * 下載完成回調
         * 
         * @param urlString
         */
        void onComplated(String urlString);

    }

    /** 自定義下載線程 */
    private class DownloadThread extends Thread {

        private int threadId = 0; // 線程Id
        private int startPos = 0; // 在文件中的開始的位置
        private int endPos = 0; // 在文件中的結束的位置
        private int compeleteSize = 0; // 已完成下載的大小
        private String urlstr = ""; // 下載地址

        /**
         * 
         * @param threadId
         *            線程Id
         * @param startPos
         *            在文件中的開始的位置
         * @param endPos
         *            在文件中的結束的位置
         * @param compeleteSize
         *            已完成下載的大小
         * @param urlstr
         *            下載地址
         */
        public DownloadThread(int threadId, int startPos, int endPos,
                int compeleteSize, String urlstr) {
            this.threadId = threadId;
            this.startPos = startPos;
            this.endPos = endPos;
            this.urlstr = urlstr;
            this.compeleteSize = compeleteSize;
        }

        @Override
        public void run() {
            HttpURLConnection connection = null;
            RandomAccessFile randomAccessFile = null;
            InputStream is = null;
            try {
                randomAccessFile = new RandomAccessFile(filePath + "/"
                        + fileNameTmp, "rwd");
                randomAccessFile.seek(startPos + compeleteSize);
                URL url = new URL(urlstr);
                connection = (HttpURLConnection) url.openConnection();
                connection.setConnectTimeout(5000);
                connection.setRequestMethod("GET");
                // 設置請求的數據的範圍
                connection.setRequestProperty("Range", "bytes="
                        + (startPos + compeleteSize) + "-" + endPos);
                is = connection.getInputStream();
                byte[] buffer = new byte[6 * 1024]; // 6K的緩存
                int length = -1;
                while ((length = is.read(buffer)) != -1) {
                    randomAccessFile.write(buffer, 0, length); // 寫緩存數據到文件
                    compeleteSize += length;
                    synchronized (this) { // 加鎖保證已下載的正確性
                        totalCompelete += length;
                        Message msg = new Message();
                        msg.what = 0;
                        msg.arg1 = length;
                        msg.arg2 = totalCompelete;
                        msg.obj = urlstr;
                        handler.sendMessage(msg);
                    }
                    // 非正在下載狀態時跳出循環
                    if (state != Download_State.Downloading) {
                        break;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("異常退出____" + urlstr);
                state = Download_State.Exception;
            } finally {
                // 不管發生了什麼事,都要保存下載信息到數據庫
                sqlTool.updataInfos(threadId, compeleteSize, urlstr);
                if (threads.size() == 1) { // 當前線程是此url對應下載任務唯一一個正在執行的線程
                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (randomAccessFile != null) {
                            randomAccessFile.close();
                        }
                        if (connection != null) {
                            connection.disconnect();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if (state == Download_State.Downloading) { // 此時此線程的下載任務正常完成(沒有被人爲或異常中斷)
                        File file = new File(filePath + "/" + fileNameTmp);
                        file.renameTo(new File(filePath + "/" + fileName));
                    }
                    if (state != Download_State.Pause) {
                        compeleted();
                    }
                }
                threads.remove(this);
            }
        }
    }
}

一個DownloadHttpTool的實例表示一個下載任務,一個下載任務中可以有多個下載線程,可以通過修改常量THREAD_COUNT的方式修改一個下載任務的下載線程數。文件的保存路徑是在sdcard中的meiriq-download文件夾,也可以修改到其他路徑。
此類中在下載開始的時候首先會執行ready()方法獲取文件相關的信息,之後執行startDownload()開啓下載線程執行下載。
下載時使用HttpURLConnection類的setRequestProperty方法指定請求頭字段實現文件的隨機下載(下載從某一個點開始到某一個點結束之際的內容),使用RandomAccessFile實現文件的隨機訪問(可以從某一個點開始寫入數據)。
爲了保證下載速度,寫入數據庫的操作並不是每次寫入文件之後都執行,而是在下載出現異常或者暫停等操作之後才寫入數據庫。所有下載任務全部結束後執行關閉數據流等操作。

DownloadUtil.java

package com.example.test;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import android.content.Context;
import android.os.Handler;
import android.os.Message;

import com.example.test.DownloadHttpTool.DownloadComplated;

/**
 * 將下載方法封裝在此類 提供開始、暫停、刪除以及重置的方法。<br>
 * 通過修改常量{@link DownloadUtil#MAX_COUNT}可改變最大並行下載任務量
 * 
 * @author [email protected]
 */
public class DownloadUtil {

    private static DownloadUtil instance = null;
    private Context context = null;
    private List<String> downloadList = null;
    private Map<String, DownloadHttpTool> downloadMap = null;
    private int currentUrlIndex = -1;
    private final int MAX_COUNT = 2; // 最大並行下載量
    private int currentCount = 0; // 當前並行下載量
    private final String FLAG_FREE = "free"; // 標記downloadMap中空閒的DownloadHttpTool實例
    private OnDownloadListener onDownloadListener = null;

    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            String url = msg.obj.toString();
            if (msg.what == 0) {
                if (onDownloadListener != null) {
                    onDownloadListener
                            .downloadProgress(url, msg.arg2, msg.arg1);
                }
            } else if (msg.what == 1) {
                if (onDownloadListener != null) {
                    onDownloadListener.downloadStart(url, msg.arg1);
                }
            } else if (msg.what == 2) {
                onDownloadListener.downloadEnd(url);
            }
        }

    };

    private DownloadUtil(Context context) {
        this.context = context;
        downloadList = new ArrayList<String>();
        downloadMap = new HashMap<String, DownloadHttpTool>();
    }

    private static synchronized void syncInit(Context context) {
        if (instance == null) {
            instance = new DownloadUtil(context);
        }
    }

    public static DownloadUtil getInstance(Context context) {
        if (instance == null) {
            syncInit(context);
        }
        return instance;
    }

    /**
     * 下載之前的準備工作,並自動開始下載
     * 
     * @param context
     */
    public void prepare(String urlString) {
        downloadList.add(urlString);
        if (currentCount < MAX_COUNT) {
            start();
        } else {
            System.out.println("等待下載____" + urlString);
        }
    }

    /**
     * 開始下載
     */
    private synchronized void start() {
        if (++currentUrlIndex >= downloadList.size()) {
            currentUrlIndex--;
            return;
        }
        currentCount++;
        String urlString = downloadList.get(currentUrlIndex);
        System.out.println("開始下載____" + urlString);
        DownloadHttpTool downloadHttpTool = null;
        if (downloadMap.size() < MAX_COUNT) { // 保證downloadMap.size() <= 2
            downloadHttpTool = new DownloadHttpTool(context, mHandler,
                    downloadComplated);
            if (downloadMap.containsKey(urlString)) {
                downloadMap.remove(urlString);
            }
            downloadMap.put(urlString, downloadHttpTool);
        } else {
            downloadHttpTool = downloadMap.get(FLAG_FREE);
            downloadMap.remove(FLAG_FREE);
            downloadMap.put(urlString, downloadHttpTool);
        }
        downloadHttpTool.start(urlString);
    }

    /** 暫停當前下載任務 */
    public void pause(String urlString) {
        paused(urlString, new Paused() {

            @Override
            public void onPaused(DownloadHttpTool downloadHttpTool) {
                downloadHttpTool.pause();
            }
        });
    }

    /** 暫停所有的下載任務 */
    public void pauseAll() {
        // 如果需要邊遍歷集合邊刪除數據,需要從後向前遍歷,否則會出異常(Caused by:
        // java.util.ConcurrentModificationException)
        String[] keys = new String[downloadMap.size()];
        downloadMap.keySet().toArray(keys);
        for (int i = keys.length - 1; i >= 0; i--) {
            pause(keys[i]);
        }
        instance = null;
    }

    /**
     * 恢復當前下載任務
     * 
     * @param urlString
     *            要恢復下載的文件的地址
     */
    public void resume(String urlString) {
        prepare(urlString);
    }

    /** 恢復所有的下載任務 */
    public void resumeAll() {
        for (Entry<String, DownloadHttpTool> entity : downloadMap.entrySet()) {
            prepare(entity.getKey());
        }
    }

    /** 刪除當前下載任務 */
    public void delete(String urlString) {
        boolean bool = paused(urlString, new Paused() {

            @Override
            public void onPaused(DownloadHttpTool downloadHttpTool) {
                downloadHttpTool.pause();
                downloadHttpTool.delete();
            }
        });
        if (!bool) { // 下載任務不存在,直接刪除臨時文件
            File file = new File(DownloadHttpTool.filePath + "/"
                    + urlString.split("/")[urlString.split("/").length - 1]
                    + DownloadHttpTool.FILE_TMP_SUFFIX);
            System.out.println(file.delete());
        }
    }

    interface Paused {

        void onPaused(DownloadHttpTool downloadHttpTool);

    }

    /**
     * 暫停
     * 
     * @param urlString
     * @param paused
     * @return 下載任務是否存在的標識
     */
    private boolean paused(String urlString, Paused paused) {
        if (downloadMap.containsKey(urlString)) {
            currentCount--;
            DownloadHttpTool downloadHttpTool = downloadMap.get(urlString);
            paused.onPaused(downloadHttpTool);
            if (!downloadMap.containsKey(FLAG_FREE)) { // 保證key == FLAG_FREE的數量
                                                        // = 1
                downloadMap.put(FLAG_FREE, downloadHttpTool);
            }
            downloadMap.remove(urlString);
            start();
            return true;
        }
        return false;
    }

    DownloadComplated downloadComplated = new DownloadComplated() {

        @Override
        public void onComplated(String urlString) {
            System.out.println("下載完成____" + urlString);
            Message msg = new Message();
            msg.what = 2;
            msg.obj = urlString;
            mHandler.sendMessage(msg);
            pause(urlString);
            // 滿足此條件說明全部下載結束
            if (downloadMap.size() == 1 && downloadMap.containsKey(FLAG_FREE)) {
                System.out.println("全部下載結束");
            }
        }
    };

    /** 設置下載監聽 */
    public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
        this.onDownloadListener = onDownloadListener;
    }

    /** 下載回調接口 */
    public interface OnDownloadListener {

        /**
         * 下載開始回調接口
         * 
         * @param url
         * @param fileSize
         *            目標文件大小
         */
        public void downloadStart(String url, int fileSize);

        /**
         * 下載進度回調接口
         * 
         * @param
         * @param downloadedSize
         *            已下載大小
         * @param lenth
         *            本次下載大小
         */
        public void downloadProgress(String url, int downloadedSize, int length);

        /**
         * 下載完成回調
         * 
         * @param url
         */
        public void downloadEnd(String url);

    }

}

一個單例的類封了”開始“、”暫停“、”繼續“、”刪除“等下載任務相關操作方法,管理所有下載任務;利用Handler實現下載進度等信息的更新;常量FLAG_FREE標識空閒下載任務;可通過修改常量MAX_COUNT的值的方式修改最大並行下載任務數。
該類管理下載任務的方式:獲取該類實例後調用prepare(String urlString)方法添加下載任務,如果沒有達到最大並行下載數,則會執行start()開始下載,否則等待其他下載任務下載完成後下載;當一個任務被暫停、刪除或者下載完成後執行start()開始新的下載。集合downloadMap保存所有的下載任務,最多MAX_COUNT個。當一個下載任務完成後downloadMap中對應的下載任務變爲FLAG_FREE以便後來的任務重複使用,如果FLAG_FREE的任務已存在則直接刪除此任務。

MainActivity.java

package com.example.test;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.example.test.DownloadUtil.OnDownloadListener;

public class MainActivity extends FragmentActivity implements OnClickListener {

    private ListView listView = null;
    private List<String> urls = null;
    private DownloadUtil downloadUtil = null;
    private final String TAG_PROGRESS = "_progress";
    private final String TAG_TOTAL = "_total";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (ListView) findViewById(R.id.listview);

        urls = new ArrayList<String>();
        urls.add("http://pc1.gamedog.cn/big/game/dongzuo/102631/shenmiaotw2_yxdog.apk");
        urls.add("http://pc1.gamedog.cn/big/game/yizhi/67450/baoweiluobo_an_yxdog.apk");
        urls.add("http://pc1.gamedog.cn/big/game/yizhi/161623/zhiwudzjs2gqb_an.apk");

        listView.setAdapter(myAdapter);

        downloadUtil = DownloadUtil.getInstance(this);

        downloadUtil.setOnDownloadListener(new OnDownloadListener() {

            String text = "已下載%sM / 共%sM \n佔比%s  \n下載速度%skb/s";
            DecimalFormat decimalFormat = new DecimalFormat("#.##"); // 小數格式化
            Timer timer = null;
            Map<String, DownloadingInfo> downloadingInfos = new HashMap<String, DownloadingInfo>();

            @Override
            public void downloadStart(String url, int fileSize) {
                DownloadingInfo info = new DownloadingInfo();
                info.setFileSize(fileSize);
                downloadingInfos.put(url, info);
                ((ProgressBar) listView.findViewWithTag(url + TAG_PROGRESS))
                        .setMax(fileSize);
            }

            @Override
            public synchronized void downloadProgress(String url,
                    int downloadedSize, int length) {
                DownloadingInfo info = downloadingInfos.get(url);
                if (info != null) {
                    ((ProgressBar) listView.findViewWithTag(url + TAG_PROGRESS))
                            .setProgress(downloadedSize);
                    ((TextView) listView.findViewWithTag(url + TAG_TOTAL)).setText(String.format(
                            text,
                            decimalFormat
                                    .format(downloadedSize / 1024.0 / 1024.0),
                            decimalFormat.format(info.getFileSize() / 1024.0 / 1024.0),
                            (int) (((float) downloadedSize / (float) info
                                    .getFileSize()) * 100) + "%", info
                                    .getKbps()));
                    info.setSecondSize(info.getSecondSize() + length);
                }
                if (timer == null) {
                    timer = new Timer();
                    timer.schedule(new TimerTask() {

                        @Override
                        public void run() {
                            DownloadingInfo info = null;
                            for (Entry<String, DownloadingInfo> entry : downloadingInfos
                                    .entrySet()) {
                                info = entry.getValue();
                                if (info != null) {
                                    info.setKbps(decimalFormat.format(info
                                            .getSecondSize() / 1024.0));
                                    info.setSecondSize(0);
                                }
                            }
                        }
                    }, 0, 1000);
                }
            }

            @Override
            public void downloadEnd(String url) {
                DownloadingInfo info = downloadingInfos.get(url);
                if (info != null) {
                    ((ProgressBar) listView.findViewWithTag(url + TAG_PROGRESS))
                            .setProgress(info.getFileSize());
                    ((TextView) listView.findViewWithTag(url + TAG_TOTAL))
                            .setText(String.format(
                                    text,
                                    decimalFormat.format(info.getFileSize() / 1024.0 / 1024.0),
                                    decimalFormat.format(info.getFileSize() / 1024.0 / 1024.0),
                                    "100%", info.getKbps()));
                    downloadingInfos.remove(url);
                }
            }

        });

    }

    BaseAdapter myAdapter = new BaseAdapter() {

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            Holder holder = null;
            if (convertView == null) {
                convertView = LayoutInflater.from(MainActivity.this).inflate(
                        R.layout.list_item, null);
                holder = new Holder();
                holder.tv_url = (TextView) convertView.findViewById(R.id.url);
                holder.progressBar = (ProgressBar) convertView
                        .findViewById(R.id.progressBar);
                holder.textView_total = (TextView) convertView
                        .findViewById(R.id.textView_total);
                holder.button_start = (Button) convertView
                        .findViewById(R.id.button_start);
                holder.button_pause = (Button) convertView
                        .findViewById(R.id.button_pause);
                holder.button_resume = (Button) convertView
                        .findViewById(R.id.button_resume);
                holder.button_delete = (Button) convertView
                        .findViewById(R.id.button_delete);

                convertView.setTag(holder);

                setClick(holder);

            } else {
                holder = (Holder) convertView.getTag();
            }

            holder.tv_url.setText(urls.get(position));

            holder.progressBar.setTag(urls.get(position) + TAG_PROGRESS);
            holder.textView_total.setTag(urls.get(position) + TAG_TOTAL);
            holder.button_start.setTag(urls.get(position));
            holder.button_pause.setTag(urls.get(position));
            holder.button_resume.setTag(urls.get(position));
            holder.button_delete.setTag(urls.get(position));

            return convertView;
        }

        private void setClick(Holder holder) {
            holder.button_start.setOnClickListener(MainActivity.this);
            holder.button_pause.setOnClickListener(MainActivity.this);
            holder.button_resume.setOnClickListener(MainActivity.this);
            holder.button_delete.setOnClickListener(MainActivity.this);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public Object getItem(int position) {
            return urls.get(position);
        }

        @Override
        public int getCount() {
            return urls.size();
        }

        class Holder {
            TextView tv_url = null;
            ProgressBar progressBar = null;
            TextView textView_total = null;
            Button button_start = null;
            Button button_pause = null;
            Button button_resume = null;
            Button button_delete = null;
        }
    };

    @Override
    public void onClick(View view) {
        String url = view.getTag() == null ? "" : view.getTag().toString();
        switch (view.getId()) {
        case R.id.button_start:
            downloadUtil.prepare(url);
            break;
        case R.id.button_pause:
            downloadUtil.pause(url);
            break;
        case R.id.button_resume:
            downloadUtil.resume(url);
            break;
        case R.id.button_delete:
            downloadUtil.delete(url);
            break;

        default:
            break;
        }
    }

}

使用ListView展示待下載任務列表。使用List<String>集合urls保存下載地址。使用View.setTag(url+TAG_PROGRESS/TAG_TOTAL)的方式標記各個任務對應的View。在顯示數據的時候使用findViewWithTag(url+TAG_PROGRESS/TAG_TOTAL)的方式取得對應的View;使用Timer定時器,每隔1s刷新一次下載速度。
activity_main.xml

<LinearLayout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ListView 
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp" >

    <TextView
        android:id="@+id/url"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/url" />

    <TextView
        android:id="@+id/textView_total"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/progressBar"
        android:layout_margin="10dp"
        android:lines="3"
        android:text="下載進度" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView_total" >

        <Button
            android:id="@+id/button_start"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="開始"
            android:textSize="12sp" />

        <Button
            android:id="@+id/button_pause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="暫停"
            android:textSize="12sp" />

        <Button
            android:id="@+id/button_resume"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="繼續"
            android:textSize="12sp" />

        <Button
            android:id="@+id/button_delete"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="刪除"
            android:textSize="12sp" />
    </LinearLayout>

</RelativeLayout>

最後添加連接網絡和操作sdcard的權限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

此代碼是使用數據庫和RandomAccessFile類實現斷點下載,除此方法外,還可以用臨時文件的方式實現斷點下載,具體方法:同以上代碼使用HttpURLConnection和setRequestProperty方法實現文件的部分下載,然後每一部分保存爲一個臨時文件,待全部下載完成後,將所有相關臨時文件寫入到一個文件,並刪除臨時文件即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章