安卓多线程下载文件(二)

安卓多线程下载文件(二)

前言

接着 安卓多线程下载文件(一)的内容,接下来实现文件的下载功能部分;

如果有需要的读者,请先看 安卓多线程下载文件(一)

一、设计下载信息分配任务类

1.下载之前,先通过网络访问 url 来获取被下载的文件的总长度:
伪代码:

  URL url = new URL(downloadUrl);
            connection = (HttpURLConnection) url.openConnection();
       //省略部分代码
       ...
      connection.getContentLength() //获取总长度
      //省略部分代码

2.为了满足 暂停下载的需求,设计了一个是否暂停的标记 hasPaused ,通过暴露set方法来改变标记的值,达到暂停的目的;
同理 取消下载 也是这样设计;

3.为了能及时的更新下载进度,所以使用Handler发送进度 给需要的界面来显示;
之前的设计是每条线程各自计算自己的下载进度,但是发现这样做,最后计算总进度的时候不好计算;
于是换个思路:每条线程只记录自己下载了多少字节,
总的下载进度 = 所有线程已下载的字节数之和 除以 被下载的文件的总字节数;

处于下载状态下,使用Handler ,每过一定的时间(比如1秒钟),去获取所有线程 已经下载的字节 ,加起来 再 除以 总长度 就能算出下载进度了。

4.一般情况下,被下载的文件的地址 url 是唯一的,所以可以根据url 来查询数据库 ,如果数据库中没有对应的线程信息,那么需要从0开始下载,并把必要的线程信息插入数据库;
有信息的话,则根据信息来分配需要几条线程来下载,每条线程负责下载多少字节; 这样就达到了断点续下载的功能;

5.当点击暂停下载按钮后,把暂停标记hasPaused赋值为true,并把相关的信息(比如线程id,已下载的字节数等等)按url和id更新数据库中对应的记录;

其它细节还有很多,就不在一一拆分;直接上菜:

1.1 FromZeroDownloadTask

package com.linkpoon.mixed.downloadutil;

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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;


/***
 * 如果要下载的文件在本地已经存在,
 * 且大小跟源头文件大小相等,
 * 就删掉,
 * 然后重新下载;
 * If the file you want to download already exists locally,
 * And the size is the same as the source file,
 * Just delete it,
 * Then download again;
 * */
public class FromZeroDownloadTask implements Runnable {

    private static final String TAG =  "FromZeroDownloadTask";

    private static final int TYPE_SUCCESS = 0;
    private static final int TYPE_FAILED = 1;
    private static final int TYPE_PAUSED = 2;
    private static final int TYPE_CANCELED = 3;


    private WeakReference<Context> weakReference;

    private ThreadInfoManager dbManager;
    private int threadCount;// 用几条线程下载这个资源?
    private long needDownTotalLength; //下载的文件的总长度
    private Handler handler; //消息传递者 用来更新ui

    private String url; //从哪里下载 ?
    private String fileSavedName; //下载得到的文件的名字
    private String fileSavePath; //下载得到的文件保存到哪里?
    private File myFile = null; // 下载得到的文件

    private boolean hasDownloading = false;// 是否正在下载
    private boolean hasCanceled = false;// 是否已经取消
    private boolean hasPaused = false;//是否已经暂停

    public boolean isHasDownloading() {
        return hasDownloading;
    }

    public void setHasDownloading(boolean hasDownloading) {
        this.hasDownloading = hasDownloading;
    }

    public boolean isHasCanceled() {
        return hasCanceled;
    }

    public void setHasCanceled(boolean hasCanceled) {
        this.hasCanceled = hasCanceled;
    }

    public boolean isHasPaused() {
        return hasPaused;
    }

    public void setHasPaused(boolean hasPaused) {
        this.hasPaused = hasPaused;
    }

    private boolean downloadSuccess = false;// 是否下载成功


    private List<DownloadRunnable> runnableList;


    private final Handler handlerProgress = new Handler();
    private final Runnable runnableProgress = new Runnable() {
        @Override
        public void run() {
            onProgressUpdate(0);//更新进度
            handlerProgress.postDelayed(runnableProgress, 1000);// 每1000毫秒更新一次进度
        }
    };

    private long lastProgress = 0;


    /***
     *
     * @param urlFrom  从哪里下载 ? 资源的网址
     * @param fileSavePath  下载后得到的文件保存到哪里
     * @param fileSavedName  下载后得到的文件叫什么名字
     * @param handler  消息传递者,用来更新UI
     * @param manager  数据库管理者,用来保存断点的,以便暂停后继续下载
     * @param threadCount  用几条线程来下载这个资源
     *
     */
    public FromZeroDownloadTask(Context context, String urlFrom, String fileSavePath, String fileSavedName, Handler handler, ThreadInfoManager manager, int threadCount) {
        this.weakReference = new WeakReference<>(context);
        this.url = urlFrom;
        this.fileSavePath = fileSavePath;
        this.fileSavedName = fileSavedName;
        this.dbManager = manager;
        this.handler = handler;
        this.threadCount = threadCount;
    }


    @Override
    public void run() {
        try {
            LogUtil.i(TAG, "被下载的文件路径path=" + url);
            needDownTotalLength = getTargetFileContentLength(url);  // 得到被下载文件大小
            LogUtil.i(TAG, "获取被下载文件的总长度" + needDownTotalLength);
            if (needDownTotalLength <= 0) {
                // 目标文件不存在 或者大小为0
                onPostExecute(TYPE_FAILED);
                return;
            }

            if (threadCount <= 0) {
                // 使用的线程数量不能少于1条,所以重新指定为1条
                threadCount = 1;
            }
            long perPartSize;
            if (threadCount == 1) {//如果只用一条线程来下载,变成单线程了
                perPartSize = needDownTotalLength;
            } else {
                perPartSize = needDownTotalLength / threadCount + 1;// 有可能出现不能整除的情况,所以+1
            }
            LogUtil.i(TAG, "分给" + threadCount + "条线程,平均每部分" + perPartSize);

            // 从数据库 根据url 获取线程信息
            List<ThreadInfo> threads = dbManager.getThreads(weakReference.get(), url);   // 从数据库 根据url 获取线程信息
            int thSum = threads.size();

            myFile = new File(fileSavePath, fileSavedName);
            if (!myFile.exists()) {
                //本地指定路径下 没有目标文件
                if (thSum > 0) {
                    // 本地没有文件,数据库的记录已经没有意义,需要删掉记录
                    dbManager.deleteData(weakReference.get(), url);
                }
                boolean b = myFile.createNewFile();
                LogUtil.i(TAG, "创建文件结果" + b);
                startFrom0(perPartSize, threads);// 从0开始下载

            }else {
                //本地指定路径下 已经存在 要下载的文件
                LogUtil.i(TAG, "已经存在的文件长度" + myFile.length());
                if (needDownTotalLength == myFile.length()) {
                    // 需要下载的字节数 跟 本地存在的文件字节数 一样长
                    // 接下来,删掉本地文件,重新下
                    boolean b = myFile.delete();
                    if (b) {
                        LogUtil.i(TAG, "删掉本地已经存在的文件 成功!");
                    } else {
                        LogUtil.i(TAG, "删掉本地已经存在的文件 失败!");
                    }
                    if (thSum > 0) {
                        // 删掉 数据库中的线程信息 记录
                        dbManager.deleteData(weakReference.get(), url);
                    }
                    boolean ok = myFile.createNewFile();
                    LogUtil.i(TAG, "创建文件结果" + ok);
                    startFrom0(perPartSize, threads);// 从0开始下载

                }else {
                    //已经下了一部分,但没有下载完成,只需要从上次暂停的位置,接着下载就行
                    LogUtil.i(TAG, "开始寻找暂停位置,接着下");
                    runnableList = new ArrayList<>(thSum);
                    for (ThreadInfo threadInfo : threads) {
                        int threadId = threadInfo.getThreadId();
                        long startPosition = threadInfo.getStartPosition();
                        long endPosition = threadInfo.getEndPosition();
                        long downloadedByte = threadInfo.getDownloadedLength();
                        RandomAccessFile accessFile = new RandomAccessFile(myFile, "rw");
                        DownloadRunnable quietDownloadRunnable = new DownloadRunnable(threadId, url, startPosition, endPosition, downloadedByte, accessFile);
                        runnableList.add(quietDownloadRunnable); //把下载任务保存到集合中
                    }
                    startTaskByThreadPool();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (hasCanceled) {
                delMyFile();// 取消下载
            }
        }

    }


    private void startFrom0(long perPartSize, List<ThreadInfo> threads) throws FileNotFoundException {
        runnableList = new ArrayList<>(threadCount);
        threads.clear();// 先清空记录
        for (int i = 0; i < threadCount; i++) {
            ThreadInfo threadInfo = new ThreadInfo();
            threadInfo.setThreadId(i);
            long startPosition = i * perPartSize;//计算每条线程的下载的开始位置
            threadInfo.setStartPosition(startPosition);
            long endPosition = startPosition + perPartSize;//计算下载的结束位置
            if (i == threadCount - 1) {
                endPosition = needDownTotalLength; //如果是最后一条线程,结束位置 就是 总长度
            }
            threadInfo.setEndPosition(endPosition);
            threadInfo.setUrl(url);
            threadInfo.setDownloadedLength(0);
            threadInfo.setTotalLength(needDownTotalLength);

            threads.add(threadInfo);// 添加一条线程信息记录
            //LogUtil.i(TAG, "ThreadId =" + i + " 开始位置=" + startPosition + " 结束位置=" + endPosition);
            dbManager.insertData(weakReference.get(), threadInfo);  // 把 threadInfo 插入数据库
            RandomAccessFile accessFile = new RandomAccessFile(myFile, "rw");
            DownloadRunnable quietDownloadRunnable = new DownloadRunnable(i, url, startPosition, endPosition, 0, accessFile);
            runnableList.add(quietDownloadRunnable);
        }
        startTaskByThreadPool();
    }

    private void startTaskByThreadPool() {
        if (!runnableList.isEmpty()) {
            LogUtil.i(TAG, "即将新建线程执行任务");
            for (DownloadRunnable r : runnableList) {
                Thread thread = new Thread(r);
                thread.start();// 调用start 来执行,不能直接调用run
                //TODO 这里可以优化,使用线程池来执行任务,而不是新建线程来执行
            }
            setHasDownloading(true);//设置已经下载的标志
            handlerProgress.removeCallbacks(runnableProgress);
            handlerProgress.postDelayed(runnableProgress, 500);// 开始下载后,计算下载进度
        }
    }


    protected void onProgressUpdate(int value) {
        if (downloadSuccess) {
            handlerProgress.removeCallbacks(runnableProgress);// 下载完成了,移除计算进度的任务
            return;
        }
        long progress = calProgress();
        //LogUtil.i(TAG, "下载进度progress=" + progress);
        if (progress >= 99 && checkAllDownloadFinished()) {
            LogUtil.i(TAG, "下载完成!");
            lastProgress = 100;
            dealProgressUpdate(needDownTotalLength, needDownTotalLength, 100); //已经下载完成了,更新进度为100%
            onPostExecute(TYPE_SUCCESS);
        } else {
            lastProgress = progress;
            dealProgressUpdate(getDownloadedByte(), needDownTotalLength, progress); //更新进度
        }

    }


    protected void onPostExecute(int code) {
        switch (code) {
            case TYPE_SUCCESS:
                downloadSuccess = true;
                notifyRealDownloadComplete(); // 下载完成
                setHasDownloading(false);// 下载完成
                handlerProgress.removeCallbacks(runnableProgress);// 下载完成 移除计算进度的runnable
                //handlerProgress.removeCallbacksAndMessages(null);// 下载完成 移除计算进度的runnable和消息
                break;
            case TYPE_FAILED:
                dealTaskFailed();// 下载失败
                setHasDownloading(false);// 下载失败
                handlerProgress.removeCallbacks(runnableProgress);// 下载失败 移除计算进度的runnable
                //handlerProgress.removeCallbacksAndMessages(null);// 下载失败 移除计算进度的runnable和消息
                break;
            case TYPE_PAUSED:
                dealTaskPaused();
                setHasDownloading(false);// 下载暂停
                handlerProgress.removeCallbacks(runnableProgress);// 下载暂停 移除计算进度的runnable
                //handlerProgress.removeCallbacksAndMessages(null);// 下载暂停 移除计算进度的runnable和消息
                break;
            case TYPE_CANCELED:
                dealTaskCanceled();
                setHasDownloading(false);// 下载取消
                handlerProgress.removeCallbacks(runnableProgress);// 下载取消 移除计算进度的runnable
                //handlerProgress.removeCallbacksAndMessages(null);// 下载取消 移除计算进度的runnable和消息
                break;
            default:
                break;
        }
    }

    private void dealTaskCanceled() {
        if (handler == null) {
            return;
        }
        Message msg = new Message();
        msg.what = DownloadConstants.MSG_NORMAL_CANCELED;
        msg.obj = url;
        handler.sendMessage(msg);//发送取消下载的消息
        LogUtil.i(TAG, "dealTaskCanceled 发送取消下载的消息  ");
    }


    private void dealTaskPaused() {
        if (handler == null) {
            return;
        }
        Message msg = new Message();
        msg.what = DownloadConstants.MSG_NORMAL_PAUSE;
        msg.obj = url;
        handler.sendMessage(msg);//发送暂停下载的消息
        LogUtil.i(TAG, "dealTaskPause 发送暂停下载的消息 MSG_NORMAL_PAUSE ");
    }


    private void dealProgressUpdate(long downedByte, long totalByte, long progress) {
        if (handler == null) {
            return;
        }
        Message msg = new Message();
        msg.what = DownloadConstants.MSG_NORMAL_UPDATE_PROGRESS;
        msg.obj = url;
        Bundle bundle = new Bundle();
        bundle.putLong(DownloadConstants.BUNDLE_KEY_DOWNED_BYTE, downedByte);
        bundle.putLong(DownloadConstants.BUNDLE_KEY_TOTAL_BYTE, totalByte);
        bundle.putLong(DownloadConstants.BUNDLE_KEY_PROGRESS, progress);
        bundle.putString(DownloadConstants.BUNDLE_KEY_DOWNLOAD_URL, url);
        msg.setData(bundle);
        handler.sendMessage(msg);//发出进度更新的消息
        // LogUtil.i(TAG, "dealProgressUpdate 发出进度更新的消息");
    }


    /***
     * 处理下载失败的情况
     * 发消息通知ui界面显示下载失败等
     * 从数据库清除 对应的下载
     */
    private void dealTaskFailed() {
        if (handler == null) {
            return;
        }
        Message msg = new Message();
        msg.what = DownloadConstants.MSG_NORMAL_FAILED;
        msg.obj = url;
        handler.sendMessage(msg);//发送下载任务失败的消息
        LogUtil.i(TAG, "dealTaskFailed()  发送下载任务失败的消息 MSG_NORMAL_FAILED");
        if (null != dbManager) {
            // 从数据库清除
            dbManager.deleteData(weakReference.get(), url);  // 从数据库清除
            LogUtil.i(TAG, "dealTaskFailed() dbManager.deleteData 从数据库清除 " + url);
        }
    }


    private void notifyRealDownloadComplete() {
        if (handler == null) {
            return;
        }
        Message msg = new Message();
        msg.what = DownloadConstants.MSG_NORMAL_COMPLETE;
        msg.obj = url;
        Bundle bundle = new Bundle();
        File apkFile = new File(fileSavePath, fileSavedName);
        bundle.putString(DownloadConstants.BUNDLE_KEY_FILED_PATH, apkFile.getAbsolutePath());
        msg.setData(bundle);
        LogUtil.i(TAG, "notifyRealDownloadComplete 发送下载完成的消息");
        handler.sendMessage(msg);//发送下载完成的消息
        if (null != dbManager) {
            // 从数据库清除
            dbManager.deleteData(weakReference.get(), url);  // 从数据库清除
            LogUtil.i(TAG, "notifyRealDownloadComplete() dbManager.deleteData 从数据库清除 " + url);
        }

    }


    /**
     * 获取下载完成的字节数
     */
    public long getDownloadedByte() {
        if (runnableList == null) {
            return 0;
        }
        // 统计多条线程已经下载的总大小
        long sumSize = 0;
        if (!runnableList.isEmpty()) {
            for (DownloadRunnable quietDownloadRunnable : runnableList) {
                if (quietDownloadRunnable != null) {
                    sumSize += quietDownloadRunnable.getDownloadedByte();
                }
            }
        }
        // 返回已经完成的字节数
        return sumSize;
    }

    /**
     * 两个 long 类型的数值 相除
     * 结果会自动取整 导致结果为0
     * 解决:
     * 在做除的操作时,
     * 被除数先乘以1.0
     * 再去除以除数,
     * 这样得到的结果就是小数而不会取整为零。
     */
    private long calProgress() {
        if (needDownTotalLength <= 0) {
            // 除数不能为0
            return -1;
        }
        try {
            return Math.round(((getDownloadedByte() * 1.0) / needDownTotalLength) * 100);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        return -1;
    }


    public boolean checkAllDownloadFinished() {
        if (runnableList != null && !runnableList.isEmpty()) {
            for (DownloadRunnable r : runnableList) {
                if (!r.isDownloadFinish()) {//只要其中一个没完成 那就是没完成;必须全部都完成才算完成
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * 暂停下载
     */
    public void pauseDownload() {
        setHasPaused(true);
        if (runnableList != null && !runnableList.isEmpty()) {
            for (DownloadRunnable r : runnableList) {
                r.pauseDownload();// 需要把下载的线程全部暂停下来,后面在把保存已经下载的字节数,不然会出现已下载的字节数不准的问题,导致恢复下载后,下载得到的文件不完整,安装失败的问题
            }
            onPostExecute(TYPE_PAUSED);// 通知UI更新为暂停状态
            for (DownloadRunnable r : runnableList) {
                ThreadInfo info = new ThreadInfo();
                info.setDownloadedLength(r.getDownloadedByte());
                info.setTotalLength(needDownTotalLength);
                info.setEndPosition(r.getEndPosition());
                info.setStartPosition(r.getStartPosition());
                info.setThreadId(r.getThreadId());
                info.setUrl(r.getRulPath());
                if (null != dbManager) {
                    dbManager.updateData(weakReference.get(), info);// 暂停的时候要更新数据库,把必要的信息记录下来,以便继续下载的时候指定从哪个位置接着下
                }
            }

        }
    }

    /**
     * 取消下载
     */
    public void cancelDownload() {
        setHasCanceled(true);
        if (runnableList != null && !runnableList.isEmpty()) {
            for (DownloadRunnable r : runnableList) {
                r.cancelDownload();
            }
            onPostExecute(TYPE_CANCELED);// 通知UI更新为取消状态
        }
        delMyFile();
    }

    public void delMyFile() {
        try {
            if (myFile != null && myFile.exists()) {
                boolean isDelOk = myFile.delete();
                LogUtil.i(TAG, myFile.getAbsolutePath() + "删除结果" + isDelOk);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 获取下载的目标文件的长度(大小)
     */
    private int getTargetFileContentLength(String downloadUrl) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL(downloadUrl);
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(30000);
            connection.setReadTimeout(30000);
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Accept", "*/*");
            //connection.setRequestProperty("Accept-Language", "zh-CN");
            connection.setRequestProperty("Charset", "UTF-8");
            connection.setRequestProperty("Connection", "Keep-Alive");
            connection.connect();
            if (connection.getResponseCode() == 200) {
                // 请求成功
                return connection.getContentLength();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
        return 0;
    }

}

接下来编写一个类 来从网络上下载文件的功能,
因为多线程下载同一个文件的前提是 服务器支持 范围请求
urlConnection.setRequestProperty(“Range”, “bytes=” + startValue + “-” + endValue);

1.2 DownloadRunnable 类

import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class DownloadRunnable implements Runnable {

    private static final String TAG = Global.TAG_PREFIX + "downloadRunnable";

    private int threadId; //线程的id 只是用来标记是第几条线程
    private String rulPath;// 从哪里下载 从网上的某个网址? 还是服务器的网址?


    // 当前线程的开始下载位置
    private long startPosition;
    // 当前线程的结束下载位置
    private long endPosition;

    private RandomAccessFile accessFile;

    // 定义该线程已下载的字节数
    private long downloadedByte = 0;

    private HttpURLConnection urlConnection = null;
    private InputStream inputStream = null;

    private boolean isCanceled = false;
    private boolean isPaused = false;

    private boolean downloadFinish = false;// 是否下载完成


    public int getThreadId() {
        return threadId;
    }

    public String getRulPath() {
        return rulPath;
    }

    public long getStartPosition() {
        return startPosition;
    }

    public long getEndPosition() {
        return endPosition;
    }

    public RandomAccessFile getAccessFile() {
        return accessFile;
    }

    public boolean isCanceled() {
        return isCanceled;
    }

    public boolean isPaused() {
        return isPaused;
    }


    public DownloadRunnable(int threadId, String rulPath, long startPosition, long endPosition, long downloadedByte, RandomAccessFile randomAccessFile) {
        this.threadId = threadId;
        this.rulPath = rulPath;
        this.startPosition = startPosition;
        this.endPosition = endPosition;
        this.downloadedByte = downloadedByte;
        this.accessFile = randomAccessFile;
        this.downloadFinish = false;
    }

    /**
     * 获取已经下载好的字节数
     */
    public long getDownloadedByte() {
        return this.downloadedByte;
    }


    public boolean isDownloadFinish() {
        return downloadFinish; //获取是否下载完成的标记
    }


    /**
     * 暂停下载
     */
    public void pauseDownload() {
        isPaused = true;
    }

    /**
     * 取消下载
     */
    public void cancelDownload() {
        isCanceled = true;
    }


    @Override
    public void run() {

        try {
            // 开始的字节数= 该线程开始的位置+已经下载的字节数
            long startValue = startPosition + downloadedByte;
            long endValue = endPosition;
            LogUtil.i(TAG, "任务编号" + threadId + ",开始位置startPosition=" + startPosition + ",结束位置endPosition=" + endPosition + ",已下载字节数downloadedByte=" + downloadedByte);
            LogUtil.i(TAG, "任务编号" + threadId + ",真正需要下载的范围:从startValue=" + startValue + "到endValue=" + endValue);
            if (startValue >= endValue) {// 开始的位置 大于等于 结束的位置 ,说明已经下载完成了
                this.downloadFinish = true;
                LogUtil.i(TAG, rulPath + ",任务编号" + threadId + "下载完成");
                return;
            }

            URL url = new URL(rulPath);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setReadTimeout(30 * 1000);// 读取超时时间30秒
            urlConnection.setConnectTimeout(30 * 1000);// 连接超时时间30秒
            String requestMethod = "GET"; //下载文件 是用GET 请求的
            urlConnection.setRequestMethod(requestMethod);
            urlConnection.setDoInput(true);
            urlConnection.setDoOutput(false);

            urlConnection.setRequestProperty("Range", "bytes=" + startValue + "-" + endValue);

            urlConnection.setRequestProperty("Accept", "*/*");// 接受所有类型的文件
            urlConnection.setRequestProperty("Charset", "UTF-8");
            urlConnection.setRequestProperty("Connection", "Keep-Alive");
            //LogUtil.i(TAG, "任务编号" + numId + "开始请求 urlConnection.connect()");

            urlConnection.connect();

            int responseCode = urlConnection.getResponseCode();
            LogUtil.i(TAG, "任务编号" + threadId + "响应码=" + responseCode);
            // Range 请求 返回的响应码 是206
            if (responseCode == 200 || responseCode == 206) { //请求成功
                inputStream = urlConnection.getInputStream();
                // 跳过 startPosition + 已经下载的字节数 个字符,表明该线程只下载自己负责那部分文件
                accessFile.seek(startValue);
                LogUtil.i(TAG, "任务编号" + threadId + "跳过字节数" + startValue);
                byte[] by = new byte[1024];
                int hasRead = 0;
                // 读取网络数据,并写入本地文件
                while ((hasRead = inputStream.read(by)) != -1) {
                    if (isCanceled) {
                        this.downloadFinish = false;
                        return;
                    }
                    if (isPaused) {
                        this.downloadFinish = false;
                        return;
                    }
                    mySleep(25); //必须让线程睡个几毫秒,解决多线程下载,出现线程处于运行状态但进度条不动的问题,也解决CPU占用率高的问题
                    accessFile.write(by, 0, hasRead);
                    // 累计该线程下载的总大小
                    downloadedByte += hasRead;
                }

            }
            accessFile.close();
            this.downloadFinish = true;
            LogUtil.i(TAG, rulPath + ",任务编号" + threadId + "下载完成,downloadedByte=" + downloadedByte);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeRes();
        }

    }

    public void mySleep(long times) {
        try {
            Thread.sleep(times);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void closeRes() {
        try {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (inputStream != null) {
                inputStream.close();
            }
            if (accessFile != null) {
                accessFile.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

1.3 定义消息传递编号

public class DownloadConstants {

    public static final int MSG_NORMAL_INIT = 1;//初始化
    public static final int MSG_NORMAL_SERVICE_BIND = 2;//服务绑定
    public static final int MSG_NORMAL_PREPARE = 3;//准备
    public static final int MSG_NORMAL_WAITING = 4;//等待中
    public static final int MSG_NORMAL_START = 5;//开始
    public static final int MSG_NORMAL_UPDATE_PROGRESS = 6;//进度更新
    public static final int MSG_NORMAL_PAUSE = 7;//暂停
    public static final int MSG_NORMAL_RESUME_RUN = 8;//被暂停后恢复运行
    public static final int MSG_NORMAL_FAILED = 9;//下载失败
    public static final int MSG_NORMAL_COMPLETE = 10;//下载完成
    public static final int MSG_NORMAL_CANCELED = 11;//下载取消

    public static final String BUNDLE_KEY_DOWNED_BYTE = "downed";
    public static final String BUNDLE_KEY_TOTAL_BYTE = "total";
    public static final String BUNDLE_KEY_PROGRESS = "progress";
    public static final String BUNDLE_KEY_DOWNLOAD_URL = "downloadUrl";
    public static final String BUNDLE_KEY_FILE_ID = "fileId";
    public static final String BUNDLE_KEY_THREAD_ID = "threadId";
    public static final String BUNDLE_KEY_FILED_PATH = "filedPath";

}

二、设计下载服务

下载文件一般都是比较耗时的,所以需要开启一个服务在后台慢慢下;

2.1 NewVersionDownLoadService

直接新建一个NewVersionDownLoadService 类继承 Service;

对开始下载,暂停下载,取消下载 三个方法做一些包装;


package com.linkpoon.mixed.service;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;

import androidx.core.app.NotificationCompat;

import com.linkpoon.mixed.R;
import com.linkpoon.mixed.activity.NewVersionDownLoadDetailActivity;
import com.linkpoon.mixed.bean.ServerVersion;
import com.linkpoon.mixed.downloadutil.FromZeroDownloadTask;
import com.linkpoon.mixed.global.Constant;
import com.linkpoon.mixed.manager.ThreadInfoManager;
import com.linkpoon.mixed.util.ToastUtil;


/**
 * 下载服务
 */
public class NewVersionDownLoadService extends Service {

    private final DownloadBinder mBinder = new DownloadBinder();
    private FromZeroDownloadTask downloadTask;
    private String downloadUrl;

    private String fileSavePath;//下载的文件 保存下来时取的路径 ,保存在什么地方?
    private String fileSaveName;//下载的文件 保存下来时取的名字

    private ServerVersion mServerVersion;
    private Handler msgHandler;

    private int currentProgress;

    private boolean hasStartForeground = false;

    public NewVersionDownLoadService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (!hasStartForeground) {
            hasStartForeground = true;
            startForeground(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_preparing_for_download), 0));
        }
//        return super.onStartCommand(intent, flags, startId);
        return Service.START_NOT_STICKY;
    }

    public class DownloadBinder extends Binder {
        public NewVersionDownLoadService getNewVersionDownLoadService() {
            return NewVersionDownLoadService.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }


    public boolean isDownloading() {
        if (downloadTask != null) {
            return downloadTask.isHasDownloading();
        }
        return false;
    }

    public boolean isHasCanceled() {
        if (downloadTask != null) {
            return downloadTask.isHasCanceled();
        }
        return false;
    }

    /**
     * 开始下载
     *
     * @param fileSavePath 下载得到的文件保存在什么地方
     * @param threadCount  用几条线程 来下载 同一个文件
     */
    public void startDownLoad(String fileSavePath, ServerVersion serverVersion, int threadCount, Handler handler) {
        if (this.downloadTask == null) {
            // 任务不存在,新建
            this.downloadUrl = serverVersion.getVersionApkUrl();
            this.fileSaveName = serverVersion.getVersionName();
            this.fileSavePath = fileSavePath;
            this.mServerVersion = serverVersion;
            this.msgHandler = handler;

            ThreadInfoManager dbManager = new ThreadInfoManager();
            this.downloadTask = new FromZeroDownloadTask(this, downloadUrl, fileSavePath, fileSaveName, msgHandler, dbManager, threadCount);
        }

        if (!downloadTask.isHasDownloading()) {
            downloadTask.setHasDownloading(true);
            Thread thread = new Thread(downloadTask);
            thread.setName("thread_new_version_1");
            thread.start();// 开启线程来执行
            startForeground(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_preparing_for_download), 0));
        } else {
            String downloadingApk = getString(R.string.str_downloading);
            ToastUtil.showText(NewVersionDownLoadService.this, downloadingApk);
        }
    }

    /**
     * 暂停下载
     */
    public void pauseDownLoad() {
        if (downloadTask != null) {
            downloadTask.pauseDownload();
            getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_in_the_pause), currentProgress));
            String pauseDownloadApk = getString(R.string.str_suspended_download);
            ToastUtil.showText(NewVersionDownLoadService.this, pauseDownloadApk);
        }
    }

    /**
     * 取消下载
     */
    public void cancelDownLoad() {
        if (downloadTask != null) {
            downloadTask.cancelDownload();
            stopForeground(true);
            getNotificationManager().cancel(Constant.ID_NOTIFICATION_APK_DOWNLOAD);
            String cancelDownloadApk = getString(R.string.str_down_load_apk_canceled);
            ToastUtil.showText(NewVersionDownLoadService.this, cancelDownloadApk);
        }
    }


    public void onProgress(long downloadByte, long totalByte, int progress) {
        currentProgress = progress;
        getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_new_version_apk_downloading), progress));
    }


    public void onSuccess() {
        stopForeground(true);
        getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_down_load_apk_success), -1));
        stopDownLoadServiceDelayed();
        downloadTask = null;
    }


    public void onFailed() {
        stopForeground(true);
        getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_down_load_apk_failed), -1));
        downloadTask = null;
    }


    public void onPaused() {
        getNotificationManager().notify(Constant.ID_NOTIFICATION_APK_DOWNLOAD, getNotification(getString(R.string.str_suspended_download), currentProgress));
    }


    public void onCanceled() {
        getNotificationManager().cancel(Constant.ID_NOTIFICATION_APK_DOWNLOAD);//下载被取消了
    }


    private NotificationManager getNotificationManager() {
        return (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
    }

    private Notification getNotification(String contentText, int mProgress) {

        String channelId = "test_download_id";
        String channelName = "test_new_version";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //安卓8.0及之后版本适配
            final int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
            channel.enableLights(true);//设置提示灯
            channel.setLightColor(Color.RED);//设置提示灯颜色
            channel.setShowBadge(true);//显示logo
            channel.setDescription("");//设置描述
            channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); //设置锁屏可见 VISIBILITY_PUBLIC=可见
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                    .build();
            channel.setSound(null, audioAttributes);//不发出声音,注意这个设置只在第一次安装时生效,如果需要改动后生效,得卸载应用重新安装
            getNotificationManager().createNotificationChannel(channel);
        }

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
        builder.setChannelId(channelId);
        builder.setContentTitle(getString(R.string.str_down_load_new_version_apk)); // 设置标题
        if (mProgress >= 0) {
            builder.setContentText(mProgress + "%");
            builder.setProgress(100, mProgress, false);
        }
        // builder.setSubText(null); // 设置内容下面一小行的文字
        // builder.setTicker(""); // 设置收到通知时在顶部显示的文字信息
        builder.setContentText(contentText);
        builder.setWhen(System.currentTimeMillis()); // 设置通知时间,一般设置的是收到通知时的System.currentTimeMillis()
        builder.setSmallIcon(R.drawable.ic_stat_name); // 设置小图标,在接收到通知的时候顶部也会显示这个小图标
        builder.setOnlyAlertOnce(true);//只提醒一次
        builder.setSound(null);//不发出提示音
        builder.setOngoing(true);
        // 设置为true,表示它为一个正在进行的通知。他们通常是用来表示
        // 一个后台任务,用户积极参与(如播放音乐)或以某种方式正在等待,
        // 因此占用设备(如一个文件下载,同步操作,主动网络连接)
        builder.setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.drawable.vector_drawable_ic_download_112c68)); // 设置大图标
        builder.setAutoCancel(false); // 用户点击Notification点击面板后是否让通知取消(默认不取消)
        builder.setPriority(NotificationCompat.PRIORITY_DEFAULT);// 设置优先级
        Intent intent = new Intent(this, NewVersionDownLoadDetailActivity.class);
        if (fileSavePath != null) {
            intent.putExtra(NewVersionDownLoadDetailActivity.INTENT_KEY_PATH, fileSavePath);
        }
        if (mServerVersion != null) {
            intent.putExtra(NewVersionDownLoadDetailActivity.INTENT_KEY_VERSION, mServerVersion);
        }
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 1216, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(pendingIntent);
        Notification notification = builder.build();

        return notification;
    }


    /**
     * 延迟关闭服务
     */
    private void stopDownLoadServiceDelayed() {
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                stopSelf();
            }
        }, 10000);
    }

}

三、注册服务

在清单文件AndroidManifest.xml 中注册NewVersionDownLoadService;

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    
<application
        android:name=".app.App"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:largeHeap="true"
        android:networkSecurityConfig="@xml/network_security_config"
        android:requestLegacyExternalStorage="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:replace="android:allowBackup"
        tools:targetApi="n">

        <!-- tools:replace="android:allowBackup,android:icon" -->

        <activity
            android:name=".activity.MainActivity"
            android:configChanges="keyboardHidden|orientation"
            android:exported="true"
            android:launchMode="standard"
            android:theme="@style/SplashTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

  
        <service
            android:name=".service.NewVersionDownLoadService"
            android:exported="false" />
            
</application>

</manifest>

剩下的请接着看 安卓多线程下载文件(三)

四、参考文献

java多线程下载文件(断点下载、进度展示、网速展示)

【Java多线程】如何使用Java多线程下载网络文件 断点续传

java 多线程下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值