Android入门第57天-使用OKHttp多线程制作像迅雷一样的断点续传功能

news2024/12/27 13:01:02

简介

今天我们将继续使用OkHttp组件并制作一个基于多线程的可断点续传的下载器来结束Android OkHttp组件的所有知识内容。在这一课里我们会在上一次课程的基础上增加SQLite的使用以便于我们的App可以暂存下载时的实时进度,每次下载开始都会判断是覆盖式还是续传式下载。同时由于Android自带的进度条太丑了,我们对它稍稍进行了一些美化。可以说今天这篇教程也是一篇阶段性的功能整合实验。

下面开始进入课程。

课程目标

  1. 使用SQLite进行下载时的进度信息的暂存;

  1. 自定义ProgressBar的样式;

断点下载的原理

如果你认真的在看完了上篇教程后并且脱离我的Sample代码自己动手实现了一个多线程下载器的话那么今天这篇教程对于你来说会变得相当的简单。

因为所谓的断点下载就是把每一条线程当前在下载的信息存入一个SQLite的表内。而断点下载就是通过暂存的信息去改变RandomAcessFile在写入时的seek。

当然这里面还伴随着一些小技巧,我们需要我们的APP的“STOP”动作可以打断正在下载的进度,打断后如果再次点击了“DOWNLOAD”按钮,此时各子线程做的任务为“续传”,续传的进度是否完成了呢这也需要子线程和主线程间进行状态通信。

需要知道每个子线程运行是否已经结束了

这边并不是需要知道每个子线程的返回、中间态。我们只是需要知道每一个子线程是否运行完了。

在平时开发中我们经常会面临这样的一种情况。比如说我们外部需要长时间的等待?或者也有开发搞了一个全局的栈去计算、也有用future接口的。很多时候往往为了取一个状态,开发创造了一堆的“轮子”,导致了整个项目代码过于复杂以及不好调试。因此这些手法都不是很优雅。今天笔者给各位推荐一种更为优雅的写法,以便于在外部判断每一个子线程是否都运行完毕了。

使用状态反转来不断check子线程状态

其实它的核心思路是:

  1. 在外部有一个无限while 循环,while(notFinish);

  1. 循环入口上手就把循环终止, notFinish=false;

  1. 接着依次检查每一个子线程内的一个状态值-finish,这个值在每个子线程内任务结束后会设为true。只要这个值在外部被检测到不为true,那么把外部循环的状态再改为notFinish=true,以使得外部循环不断运行直到所有子线程检测下来都确为finish,此时外部的while循环跳出;

每个子线程下载的实时信息存储

我们设计了一个这样的表结构用来存储下载的实时信息。

  • 每次下载进程开始时,先根据下载URL去该表中查出所有的下载信息。比如说我们开启了3个线程,那么对于同一个URL:/test.zip可以根据download_path查出3条数据。把3条数据的download_length相加拼在一起,如果<当前远程文件size说明上次下载没有完成,那么继续下载。否则新建一个空文件并把这个空文件的长度设定为远程资源文件的长度;

  • 每个子线程在下载时不断根据download_path update这张表里的数据把当前的实时进度写进去;

  • 下载完后根据download_path清空这个表里的数据;

Http Get请求如何支持断点续传

Request.addHeader("Range", "bytes=" + startPos + "-" + endPos)

假设线程编号从1开始,开了3个子线程,共有1-3个线程,线程编号为1-3,此处的startPos和endPos的计算公式如下:

  • startPos=每个线程分页下载文件大小*线程编号+上一次下载进度,如果线程为1号线程那么startPos=上一次的下载进度;

  • endPos=每个线程分页下载文件大小*当前线程编号-1,-1代表“不计算文件末尾结束符”;

int startPos = block * (threadId - 1) + downLength;//开始位置
int endPos = block * threadId - 1;//结束位置

自定义Android里的ProgressBar的样式

第一步:

res\values\colors.xml文件中加入一个ProgressBar的底色theme_progressbar

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="theme_progressbar">#D0E3F7</color>
</resources>

这是一个很浅很淡的蓝色。

第二步:

res\drawable\下,新建一个progressbar_color.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- 背景  gradient是渐变,corners定义的是圆角 -->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="3dp"/>

            <solid android:color="@color/theme_progressbar" />
        </shape>
    </item>
    <!-- 进度条 -->
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="3dp"/>
                <solid android:color="#FF51AAE6" />
            </shape>
        </clip>
    </item>

</layer-list>

第三步:

在activity_main.xml文件里定义progressbar时引用这个progressbar_color.xml文件。

 <ProgressBar
        android:id="@+id/progressBarDownload"
        style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:max="100"
        android:progressDrawable="@drawable/progressbar_color"
        android:visibility="visible" />

以上内容都准备好了,我们就可以进入全代码了。

全代码

项目结构

前端

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
    <Button
        android:id="@+id/buttonDownload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="download"
        android:layout_marginRight="10dp"
        android:textSize="20sp" />

    <Button
        android:id="@+id/buttonStop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="stop"
        android:textSize="20sp" />
    </LinearLayout>
    <ProgressBar
        android:id="@+id/progressBarDownload"
        style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:max="100"
        android:progressDrawable="@drawable/progressbar_color"
        android:visibility="visible" />
</LinearLayout>

后端

DbOpeerateHelper.java

package org.mk.android.demo.http;

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

public class DbOperateHelper extends SQLiteOpenHelper {
    private static final String TAG = "DemoContinueDownload";
    private static final String DB_NAME = "dw_manager.db";
    private static final String DB_TABLE = "dw_infor";
    private static final int DB_VERSION = 1;
    public DbOperateHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    private static final String DB_CREATE =

            "CREATE TABLE dw_infor ("
                    +"dw_id    INTEGER PRIMARY KEY AUTOINCREMENT,"
                    +"download_path    VARCHAR,"
                    +"thread_id    INTEGER,"
                    +"download_length    INTEGER);";

    @Override
    public void onCreate(SQLiteDatabase db) {
        Log.i(TAG, ">>>>>>execute create table->" + DB_CREATE);
        db.execSQL(DB_CREATE);
        Log.i(TAG, ">>>>>>db init successfully");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int _oldVersion, int _newVersion) {
        //db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
        //onCreate(_db);
        db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
        onCreate(db);

    }
}

DBService.java

package org.mk.android.demo.http;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class DBService {
    private static final String TAG = "DemoContinueDownload";
    private DbOperateHelper dbHelper;
    private static final String DB_NAME = "dw_manager.db";
    private static final String DB_TABLE = "dw_infor";
    private static final int DB_VERSION = 1;

    public DBService(Context ctx){
        dbHelper=new DbOperateHelper(ctx,DB_NAME,null,DB_VERSION);
    }
    /**
     * 获得指定URI的每条线程已经下载的文件长度
     * @param downloadPath
     * @return
     * */
    public List<DWManagerInfor> getData(String downloadPath)
    {
        //获得可读数据库句柄,通常内部实现返回的其实都是可写的数据库句柄
        //根据下载的路径查询所有现场的下载数据,返回的Cursor指向第一条记录之前
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = db.rawQuery("select thread_id, download_length from dw_infor where download_path=?",
                new String[]{downloadPath});
        List<DWManagerInfor> data=new ArrayList<DWManagerInfor>();
        try {
            //从第一条记录开始遍历Cursor对象
            //cursor.moveToFirst();
            while (cursor.moveToNext()) {
                DWManagerInfor dwInfor =new DWManagerInfor();
                dwInfor.setThreadId(cursor.getInt(cursor.getColumnIndexOrThrow("thread_id")));
                dwInfor.setDownloadLength(cursor.getInt(cursor.getColumnIndexOrThrow("download_length")));
                data.add(dwInfor);
            }
        }catch(Exception e){
            Log.e(TAG,">>>>>>getData from db error: "+e.getMessage(),e);
        }finally{
            try {
                cursor.close();//关闭cursor,释放资源;
            }catch(Exception e){}
            try {
                db.close();
            }catch(Exception e){}
        }
        return data;
    }

    /**
     * 保存每条线程已经下载的文件长度
     */

    public void save(String downloadPath, Map<Integer,Integer> map)
    {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.beginTransaction();
        try{

            //使用增强for循环遍历数据集合
            for(Map.Entry<Integer, Integer> entry : map.entrySet())
            {

                db.execSQL("insert into dw_infor(download_path, thread_id, download_length) values(?,?,?)",
                        new Object[]{downloadPath, entry.getKey(),entry.getValue()});
            }
            //设置一个事务成功的标志,如果成功就提交事务,如果没调用该方法的话那么事务回滚
            //就是上面的数据库操作撤销
            db.setTransactionSuccessful();
        }catch(Exception e){
            Log.e(TAG,">>>>>>save download infor into db error: "+e.getMessage(),e);
        }finally{
            //结束一个事务
            db.endTransaction();
            try{
                db.close();
            }catch(Exception e){}
        }
    }

    public int updateItem(DWManagerInfor dwInfor)throws Exception{
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        try{
            ContentValues newValues = new ContentValues();
            newValues.put("download_length",dwInfor.getDownloadLength());
            newValues.put("thread_id",dwInfor.getThreadId());
            newValues.put("download_path",dwInfor.getDownloadPath());
            return db.update(DB_TABLE,newValues,"thread_id='"+dwInfor.getThreadId()+"' and download_path='"+dwInfor.getDownloadPath()+"'",null);
        }catch(Exception e){
            Log.e(TAG,"update item error: "+e.getMessage(),e);
            throw new Exception("update item error: "+e.getMessage(),e);
        }finally{
            try{
                db.close();
            }catch(Exception e){}
        }
    }
    public void delete(String path)
    {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        try {
            String deleteSql = "delete from dw_infor where download_path=?";
            db.execSQL(deleteSql, new Object[]{path});
        }catch(Exception e){
            Log.e(TAG,">>>>>>delete from path->"+path+" error: "+e.getMessage(),e);
        }finally{
            try{
                db.close();
            }catch(Exception e){}
        }
    }
    public long addItem(DWManagerInfor dwInfor)throws Exception{
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        try{
            ContentValues newValues = new ContentValues();
            newValues.put("download_path", dwInfor.getDownloadPath());
            newValues.put("thread_id", dwInfor.getThreadId());
            newValues.put("download_length", dwInfor.getDownloadLength());
            Log.i(TAG, "addItem successfully");
            return db.insert(DB_TABLE, null, newValues);
        }catch(Exception e){
            Log.e(TAG,">>>>>>addItem into db error: "+e.getMessage(),e);
            throw new Exception(">>>>>>addItem into db error: "+e.getMessage(),e);
        }finally{
            try{
                db.close();
            }catch(Exception e){}
        }
    }
}

DownloadProgressListener.java

package org.mk.android.demo.http;

public interface DownloadProgressListener {
    public void onDownloadSize(int size);
}

DWManagerInfor.java

package org.mk.android.demo.http;

import java.io.Serializable;

public class DWManagerInfor implements Serializable {
    private int dwId=0;
    private int threadId=0;


    public int getDwId() {
        return dwId;
    }

    public void setDwId(int dwId) {
        this.dwId = dwId;
    }

    public int getThreadId() {
        return threadId;
    }

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

    public int getDownloadLength() {
        return downloadLength;
    }

    public void setDownloadLength(int downloadLength) {
        this.downloadLength = downloadLength;
    }

    public String getDownloadPath() {
        return downloadPath;
    }

    public void setDownloadPath(String downloadPath) {
        this.downloadPath = downloadPath;
    }

    private int downloadLength=0;
    private String downloadPath="";

}

DownloadService.java

这是一个主要的用于启动多线程下载和操作断点信息的类,在这个类内会分出3个子线程,每个子线程内又把这个类的this传入在子线程内进行回调、写下载时的实时信息入库、传递子线程状态,因此它是一个核心类。

package org.mk.android.demo.http;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import androidx.annotation.NonNull;

import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.sql.Array;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class DownloadService {
    private static final String TAG = "DemoContinueDownload";
    private File saveFile;
    private int downloadedSize = 0;               //已下载的文件长度
    private Context context = null;
    private int threadCount = 3;
    private int fileSize = 0;
    private int block = 0;
    private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();  //缓存个条线程的下载的长度
    //private DBAdapter dbAdapter = null;
    private DBService dbService=null;
    private DownloadThread[] threads;        //根据线程数设置下载的线程池
    private boolean exited = false;
    private String downloadUrl = "";

    public DownloadService(Context context, String downloadUrl) {
        this.context = context;
        //dbAdapter = new DBAdapter(context);
        dbService=new DBService(context);
        this.threads = new DownloadThread[threadCount];
        this.downloadUrl = downloadUrl;
    }

    public int getFileSize() {
        return this.fileSize;
    }

    /**
     * 退出下载
     */
    public void exit() {
        Log.i(TAG, ">>>>>>触发了exited");
        this.exited = true;    //将退出的标志设置为true;
    }

    public boolean getExited() {
        return this.exited;
    }

    /**
     * 累计已下载的大小
     * 使用同步锁来解决并发的访问问题
     */
    protected synchronized void append(int size) {
        //把实时下载的长度加入到总的下载长度中
        downloadedSize += size;
    }

    /**
     * 更新指定线程最后下载的位置
     *
     * @param threadId 线程id
     * @param pos      最后下载的位置
     */
    protected synchronized void update(int threadId, int pos) {
        try {
            this.data.put(threadId, pos);
            //dbAdapter.open();
            DWManagerInfor dwInfor = new DWManagerInfor();
            dwInfor.setDownloadPath(this.downloadUrl);
            dwInfor.setThreadId(threadId);
            dwInfor.setDownloadLength(pos);
            //dbAdapter.updateItem(dwInfor);
            dbService.updateItem(dwInfor);
        } catch (Exception e) {
            Log.e(TAG, ">>>>>>update error: " + e.getMessage(), e);
        }
        //把指定线程id的线程赋予最新的下载长度,以前的值会被覆盖掉
        this.data.put(threadId, pos);
        //更新数据库中制定线程的下载长度

    }

    private String generateFile(long fileLength, boolean generateFile) throws Exception {
        String end = downloadUrl.substring(downloadUrl.lastIndexOf("."));
        URL url = new URL(downloadUrl);
        //String downloadFilePath = "Cache_" + System.currentTimeMillis() + end;
        String urlFileName = FilenameUtils.getName(url.getPath());
        RandomAccessFile file = null;
        try {
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                String fileName = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + urlFileName;
                Log.i(TAG, ">>>>>>需要操作的文件名为->" + fileName);
                Log.i(TAG,">>>>>>downloadedSize->"+downloadedSize+"  fileLength->"+fileLength);
                if (generateFile) {
                    if(downloadedSize==0||downloadedSize>=fileLength) {
                        Log.i(TAG,">>>>>>新建文件并设定长度->"+fileLength);
                        file = new RandomAccessFile(fileName, "rwd");
                        file.setLength(fileLength);
                        file.close();
                    }else{
                        Log.i(TAG,">>>>>>文件存在,返回文件名进行续传");
                    }
                }
                return fileName;
            } else {
                throw new Exception("SD卡不可读写");
            }
        } catch (Exception e) {
            throw new Exception("GenerateTempFile error: " + e.getMessage(), e);
        } finally {
            try {
                file.close();
            } catch (Exception e) {
            }
        }

    }

    public int getRemainDownloadLen(int threadCount, long fileLength) {
        int block = 0;
        try {
            block = (int) fileLength % threadCount == 0 ? (int) fileLength / threadCount :
                    (int) fileLength / threadCount + 1;
        } catch (Exception e) {
            Log.e(TAG, ">>>>>>getRemainDownloadLen error: " + e.getMessage(), e);
        }
        return block;
    }

    public void download(boolean generateFile, DownloadProgressListener downloadProgressListener) throws Exception {
        try {
            fileSize = getDownloadFileSize(downloadUrl);
            //把所有的DB内已经存在的size放入全局的data中,以作缓存
            List<DWManagerInfor> dwInforList = new ArrayList<DWManagerInfor>();
            dwInforList = dbService.getData(downloadUrl);
            Log.i(TAG, ">>>>>>in download method the dwInforList size->" + dwInforList.size());
            if (dwInforList.size() > 0) {
                for (DWManagerInfor dwInfor : dwInforList) {
                    downloadedSize += dwInfor.getDownloadLength();
                    this.data.put(dwInfor.getThreadId(), dwInfor.getDownloadLength());
                }
            } else {
                for (int i = 0; i < threadCount; i++) {
                    this.data.put(i + 1, 0);
                }
            }
            this.block = getRemainDownloadLen(3, fileSize);
            Log.i(TAG,">>>>>>downloadSize->"+downloadedSize);
            String saveFileName = generateFile(this.fileSize, generateFile);//生成一个Random空文件并把文件长度设置好
            Log.i(TAG, ">>>>>>开始生成线程进行分: " + this.threadCount + " 条线程并行下载...每条线程的block->" + this.block);
            Log.i(TAG, ">>>>>>全局data size->" + data.size());
            for (int i = 0; i < this.threads.length; i++) {//开启线程进行下载
                int downLength = 0;
                if (data.size() > 0) {
                    downLength = this.data.get(i + 1);
                }
                Log.i(TAG, ">>>>>>开启前发觉当前下载进度为->" + downLength);
                //通过特定的线程id获取该线程已经下载的数据长度
                //判断线程是否已经完成下载,否则继续下载
                if (downLength < this.block && this.downloadedSize < this.fileSize) {
                    //初始化特定id的线程
                    //this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1),
                    // i+1);
                    this.threads[i] = new DownloadThread(this, downloadUrl, saveFileName, this.block,
                            this.data.get(i + 1), i + 1);
                    //设置线程优先级,Thread.NORM_PRIORITY = 5;
                    //Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,数值越大优先级越高
                    this.threads[i].setPriority(7);
                    this.threads[i].start();    //启动线程
                } else {
                    Log.i(TAG, "当前线程不用下载,因为当前线程己下载长度downLength->" + downLength + " block->" + this.block);
                    this.threads[i] = null;   //表明线程已完成下载任务
                }
            }
            //dbAdapter.open();
            dbService.delete(downloadUrl);
            dbService.save(downloadUrl, this.data);
            //把下载的实时数据写入数据库中
            boolean notFinish = true;
            //下载未完成
            while (notFinish) {
                // 循环判断所有线程是否完成下载
                Thread.sleep(300);
                notFinish = false;
                for (int i = 0; i < threadCount; i++) {
                    if (this.threads[i] != null && !this.threads[i].isFinish()) {
                        //如果发现线程未完成下载
                        notFinish = true;
                        //设置标志为下载没有完成,以便于外层while循环不断check;
                    }
                }
                if (downloadProgressListener != null) {
                    downloadProgressListener.onDownloadSize(this.downloadedSize);
                }
                //通知目前已经下载完成的数据长度
            }
            if (downloadedSize == this.fileSize) {
                  dbService.delete(downloadUrl);
            }
        } catch (Exception e) {
            Log.e(TAG, ">>>>>>download error: " + e.getMessage(), e);
            throw new Exception(">>>>>>download error: " + e.getMessage(), e);
        }

    }

    public int getDownloadFileSize(String downloadUrl) throws Exception {
        int size = -1;
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)//设置连接超时时间
                .readTimeout(10, TimeUnit.SECONDS).build();//设置读取超时时间
        Request request = new Request.Builder().url(downloadUrl)//请求接口,如果需要传参拼接到接口后面
                .build(); //创建Request对象
        Response response = null;
        try {
            Call call = client.newCall(request);
            response = call.execute();
            if (200 == response.code()) {
                Log.d(TAG, ">>>>>>response.code()==" + response.code());
                Log.d(TAG, ">>>>>>response.message()==" + response.message());
                try {
                    size = (int) response.body().contentLength();
                    Log.d(TAG, ">>>>>>file length->" + size);
                    //fileSizeListener.onHttpResponse((int) size);
                } catch (Exception e) {
                    Log.e(TAG, ">>>>>>get remote file size error: " + e.getMessage(), e);
                }
            }
        } catch (Exception e) {
            Log.e(TAG, ">>>>>>open connection to path->" + downloadUrl + "\nerror: " + e.getMessage(), e);
            throw new Exception(">>>>>>getDownloadFileSize from->" + downloadUrl + "\nerror: " + e.getMessage(), e);
        } finally {
            try {
                response.close();
            } catch (Exception e) {
            }
        }
        return size;
    }

}

DownloadThread.java

这个类就是每一个子线程的实现了。在这个类里每一个子线程会启动OkHttp并使用http-header: Range去做断点下载。

值得注意的是,如果你的http-header带着Rnage去做请求,你得到的response code不是200还是206即:partial content。

package org.mk.android.demo.http;

import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class DownloadThread extends Thread {
    private static final String TAG = "DemoContinueDownload";
    private String downloadUrl;              //下载的URL
    private int block;                //每条线程下载的大小
    private int threadId = 1;            //初始化线程id设置
    private int downLength;             //该线程已下载的数据长度
    private boolean finish = false;         //该线程是否完成下载的标志
    private DownloadService downloader;
    private String saveFileName = "";

    //private FileDownloadered downloader;      //文件下载器
    public DownloadThread(DownloadService downloader, String downloadUrl, String saveFileName, int block,
            int downLength, int threadId) {
        this.downloader = downloader;
        this.downloadUrl = downloadUrl;
        this.saveFileName = saveFileName;
        this.block = block;
        this.downLength = downLength;
        this.threadId = threadId;
    }

    @Override
    public void run() {
        Log.i(TAG, ">>>>>>downloadLength->" + downLength + " block->" + block);
        if (downLength < block) {
            int startPos = block * (threadId - 1) + downLength;//开始位置
            int endPos = block * threadId - 1;//结束位置
            OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)//设置连接超时时间
                    .readTimeout(10, TimeUnit.SECONDS)//设置读取超时时间
                    .build();
            Request request = new Request.Builder().get().url(downloadUrl)//请求接口,如果需要传参拼接到接口后面
                    .addHeader("Referer", downloadUrl)
                    .addHeader("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, " +
                            "application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, " +
                            "application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, " +
                            "application/vnd.ms-powerpoint, application/msword, */*")
                    .addHeader("connection", "keep-alive")
                    .addHeader("Range", "bytes=" + startPos + "-" + endPos)
                    .addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET " +
                            "CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET " +
                            "CLR 3.5.30729)")
                    .build(); //创建Request对象
            //Log.i(TAG, ">>>>>>线程" + threadId + "开始下载...Range: bytes=" + startPos + "-" + endPos);
            Call call = client.newCall(request);
            //异步请求
            call.enqueue(new Callback() {
                //失败的请求
                @Override
                public void onFailure(@NonNull Call call, @NonNull IOException e) {
                    Log.e(TAG, ">>>>>>下载进程加载->" + downloadUrl + " error:" + e.getMessage(), e);
                    finish = true;
                }

                //结束的回调
                @Override
                public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                    Log.i(TAG, ">>>>>>连接->" + downloadUrl + " 己经连接,进入下载...");
                    InputStream is = null;
                    Log.i(TAG, ">>>>>>当前:response code->" + response.code());
                    RandomAccessFile threadFile = null;
                    try {
                        if (response.code() == 200 || response.code() == 206) {
                            Log.i(TAG, ">>>>>>response.code()==" + response.code());
                            //Log.i(TAG, ">>>>>>response.message()==" + response.message());
                            is = response.body().byteStream();
                            byte[] buffer = new byte[1024];
                            int offset = 0;
                            int length = 0;
                            threadFile = new RandomAccessFile(saveFileName, "rwd");
                            threadFile.seek(startPos);
                            while (!downloader.getExited() && (offset = is.read(buffer, 0, 1024)) != -1) {
                                //Log.i(TAG,">>>>>>offset write->"+offset);
                                threadFile.write(buffer, 0, offset);
                                downLength += offset;
                                downloader.update(threadId, downLength);
                                downloader.append(offset);
                            }
                            //Log.i(TAG,"current offset->"+offset);
                            //Log.i(TAG, ">>>>>>线程" + threadId  + "已下载完成");
                            finish = true;
                            threadFile.close();
                        }
                    } catch (Exception e) {
                        downLength = -1;               //设置该线程已经下载的长度为-1
                        Log.e(TAG, ">>>>>>线程:" + threadId + " 下载出错: " + e.getMessage(), e);
                        finish = true;
                    } finally {
                        try {
                            threadFile.close();
                            ;
                        } catch (Exception e) {
                        }
                        try {
                            is.close();
                            ;
                        } catch (Exception e) {
                        }
                    }

                }
            });
        }
    }

    /**
     * 下载是否完成
     *
     * @return
     */
    public boolean isFinish() {
        return finish;
    }

    /**
     * 已经下载的内容大小
     *
     * @return 如果返回值为-1,代表下载失败
     */
    public long getDownLength() {
        return downLength;
    }
}

MainActivity.java

package org.mk.android.demo.http;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private SQLiteDatabase db;
    private Context context;
    //private DBAdapter dbAdapter;
    private DBService dbService;
    private Button buttonDownload;
    private Button buttonStop;
    private DownloadTask downloadTask;
    private ProgressBar progressBarDownload;
    private static final String TAG = "DemoContinueDownload";
    //private static final String DOWNLOAD_URL = "http://www.jszjenergy.cn/data/upload/image/20191231/1577758425809614.jpg";
    private static final String DOWNLOAD_URL = "https://7-zip.org/a/7z2201.exe";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = getApplicationContext();
        //dbAdapter = new DBAdapter(context);
        dbService=new DBService(context);
        progressBarDownload = (ProgressBar) findViewById(R.id.progressBarDownload);
        buttonDownload = (Button) findViewById(R.id.buttonDownload);
        buttonStop = (Button) findViewById(R.id.buttonStop);
        //progressBarDownload.setVisibility(View.GONE);
        buttonDownload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                        Log.i(TAG, ">>>>>>version.SDK->" + Build.VERSION.SDK_INT);
                        if (!Environment.isExternalStorageManager()) {
                            Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                            startActivity(intent);
                            return;
                        }
                    }
                    downloadTask = new DownloadTask();
                    downloadTask.start();
                } catch (Exception e) {
                    Log.e(TAG, ">>>>>>downloadTest error: " + e.getMessage(), e);
                }
            }
        });
        buttonStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (downloadTask != null) {
                    downloadTask.exit();
                }
            }
        });
    }

    private Handler downloadHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            Log.i(TAG, ">>>>>>receive handler Message msg.what is: " + msg.what);
            switch (msg.what) {
                case 101:
                    progressBarDownload.setVisibility(View.VISIBLE);
                    //progressBarDownload.setProgress();
                    int inputNum = msg.getData().getInt("pgValue");
                    progressBarDownload.setProgress(inputNum);
                    if (inputNum >= 100) {
                        Toast.makeText(context, "下载完成", Toast.LENGTH_LONG).show();
                    }
                    break;
            }
            return false;
        }
    });

    private class DownloadTask extends Thread {
        private DownloadService loader;

        /**
         * 退出下载
         */
        public void exit() {
            if (loader != null) {
                loader.exit();
            }
        }

        @Override
        public void run() {
            try {
                loader = new DownloadService(context, DOWNLOAD_URL);
                //dbAdapter = new DBAdapter(context);
                //dbAdapter.open();
                //dbAdapter.delete(DOWNLOAD_URL);
                loader.download(true, new DownloadProgressListener() {
                    @Override
                    public void onDownloadSize(int size) {
                        int fileSize=loader.getFileSize();
                        Log.i(TAG, ">>>>>>下载中,当前尺寸: " + size+" totalSize->"+fileSize);
                        float progress = ((float) size / (float) fileSize) * 100;
                        int pgValue = (int) progress;
                        Message msg = new Message();
                        msg.what = 101;
                        Bundle bundle = new Bundle();
                        bundle.putInt("pgValue", pgValue);
                        msg.setData(bundle);
                        downloadHandler.sendMessage(msg);
                    }
                });
            } catch (Exception e) {
                Log.e(TAG, ">>>>>>downloadTest error: " + e.getMessage(), e);
            } finally {
                //dbAdapter.close();
            }
        }
    }
}

为了正确运行上述内容你需要在gradle的build文件内加入OkHttp和commons-io的依赖包。

 implementation 'com.squareup.okhttp3:okhttp:3.10.0'
 implementation group: 'commons-io', name: 'commons-io', version: '2.6'

运行后的效果

当你无论如何stop再download再stop或者下载完后多次再download,那么当文件被成功下载后,会在Android的资源列表里此处显示下载的资源。

它位于data\media\0下。

为了验证你下载的正确性,你可以把这个资源右键->另存出去。然后双击这个安装程序,如果它可以正确安装那么说明你的断点下载是正确了。

结束今天的课程,不妨自己动一下手试试看吧。

附、AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <!-- 在SDCard中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
    <!-- 往SDCard写入数据权限 -->
    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
    <!--外部存储的写权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--外部存储的读权限-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:networkSecurityConfig="@xml/network_config"
        android:requestLegacyExternalStorage="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.DemoContinueDownloadProcess"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/146861.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(3)go-micro微服务项目搭建

文章目录一 微服务项目介绍二 go-micro安装1.拉取micro镜像2.生成项目目录三 项目搭建使用DDD模式开发项目&#xff1a;四 最后一 微服务项目介绍 账户功能是每一个系统都绕不开的一部分&#xff0c;所以本次搭建的微服务项目就是账户微服务项目&#xff0c;其中向外暴露的功能…

【C语言航路】第十站:指针进阶(一)

目录 一、字符指针 二、指针数组 三、数组指针 1.数组指针的定义 2.数组名和&数组名 3.数组指针的使用 四、数组参数、指针参数 1.一维数组传参 2.二维数组传参 3.一级指针传参 4.二级指针传参 五、函数指针 总结 一、字符指针 我们知道指针有一种类型叫做字符…

Vue3新特性

文章目录一 新特性之组合API1.1 ref&reactive1.2 methods1.3 props和context1.4 完整代码&效果演示二 Vue3新特性之生命周期函数(在setup中)三 父子级组件间数据传递3.1 Provide&Inject四 Fragment&#xff08;碎片&#xff09;一 新特性之组合API 1.1 ref&re…

XMLHttpRequest和Referer

XMLHttpRequest 对象简介 1999年&#xff0c;微软公司发布 IE 浏览器5.0版&#xff0c;第一次引入新功能&#xff1a;允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意&#xff0c;直到2004年 Gmail 发布和2005年 Google Map 发布&#xff0c;才引起广…

承蒙时光不弃,做个好人!

落幕 2022年博客之星终于要在今晚2023年1月7日24点整落下帷幕&#xff0c;从上个月28号开始&#xff0c;仿佛经历了一场噩梦&#xff0c;本是抱着随便玩玩的心态报了名&#xff0c;没成想&#xff0c;刚开始自投五星之后竟然显示10几名&#xff0c;那是不是我稍加努力就进前十…

狂揽两千星,速度百倍提升,高性能 Python 编译器 Codon 火了

前言 众所周知&#xff0c;Python 是一门简单易学、具有强大功能的编程语言&#xff0c;在各种用户使用统计榜单中总是名列前茅。相应地&#xff0c;围绕 Python&#xff0c;研究者开发了各种便捷工具&#xff0c;以更好的服务于这门语言。 编译器充当着高级语言与机器之间的…

TensorFlow笔记之单神经元完成多分类任务

文章目录前言一、逻辑回归1.二分类问题2.多分类问题二、数据集调用三、TensorFlow1.x1.定义模型2.训练模型3.结果可视化四、TensorFlow2.x1.定义模型2.训练模型3.结果可视化总结前言 记录分别在TensorFlow1.x与TensorFlow2.x中使用单神经元完成MNIST手写数字识别的过程。 一、…

Linux出现ping: www.baidu.com: 未知的名称或服务解决方法

文章目录解决对象方法先找到网关在Windows下进行VMnet8的配置ping成功Linux出现ping: www.baidu.com: 未知的名称或服务解决方法 解决对象 本文的方法用于各位大佬已经用过以下方法仍然无法ping成功 Linux防火墙已关闭和Windows防火墙已经关闭已经配置好 vim /etc/sysconfig/…

手撕C语言理论知识(上)粗略讲解C语言的部分入门知识

目录 C语言的一些基础知识 操作符简介 Scanf的%[ ] 语句&#xff08;分支、循环、goto&#xff09; 函数 C语言的一些基础知识 主函数 - 程序的入口 - main函数有且仅有一个。char - short - int - long - long long - float - double%d - 十进制整型 %u - 无符号整型 %…

【博学谷学习记录超强总结,用心分享|产品经理基础总结和感悟15】

互联网产品设计背后的心理学02&#xff1a;你就是会被其他人的行为所影响一、前言二、实验设计及结果分析三、实验原理四、实验方法总结五、产品设计中的应用六、结束语前文回顾&#xff1a;让人们做出决定并不是信息本身&#xff0c;而是这些信息呈现的背景或情景。我们这个信…

Spring Cloud Alibaba Dubbo(服务远程调用)

一、软件环境 &#xff08;1&#xff09;自己部署服务器 所有软件及服务器自己进行管理提供&#xff0c;可以直接在项目中添加Spring Cloud依赖。推荐 <dependencyManagement> <dependencies> <dependency> <groupId>com.a…

liunx centos9中安装flask并在pycharm中使用图文攻略

liunx centos9中安装flask并在pycharm中使用图文攻略1.首先在liunx的终端中输入2.安装好flask之后就在pycharm创建新的项目处添加flask项目3.点击绿色三角箭头开始运行flask项目4. 然后登录ip地址就出现Hllo world就代表flask环境搭建完成需要注意事项1.首先在liunx的终端中输入…

ngx_thread_pool_init()

ngx_thread_pool_cycle()函数的主要工作是从待处理的任务队列中获取一个任务&#xff0c;然后调用任务对象的handler()函数处理任务&#xff0c;完成后把任务放置到完成队列中&#xff0c;并通过ngx_notify()通知主线程 手写线程池与性能分析 - 知乎 pthread_cond_wait函数的原…

【5G RRC】5G系统消息介绍

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

一键绕过ID锁激活,为什么很多人都会失败?绕ID这一篇就够了

最近阳了所以暂时断更&#xff0c;你们也要注意身体&#xff0c;最好不要阳 现在绕ID的方法已经非常完善&#xff0c;一个小白选手只要有设备就可以正常绕过ID&#xff0c;总的来说绕ID分为两个步骤&#xff1a;第一步是手机的越狱&#xff0c;这里只能是用checkra1n越狱&…

数据在内存中存储☞(超详解)

目录 一.数据类型大家族 1.了解类型的意义 2.数据类型大家族的分类 二.详解☞数据储存之整形 1.储存方式 &#xff08;1&#xff09;.原码反码补码的概念 &#xff08;2&#xff09;.原码反码补码出现的原因&#xff1a; 计算机中只有加法器没有减法器&#xff0c;所有只…

SemanticKITTI: A Dataset for Semantic Scene Understanding of LiDAR Sequences

Paper name SemanticKITTI: A Dataset for Semantic Scene Understanding of LiDAR Sequences Paper Reading Note URL: https://arxiv.org/pdf/1904.01416.pdf TL;DR 2019 ICCV 论文&#xff0c;提出了一个大规模的真实场景 LiDAR 点云标注数据集 SemanticKITTI&#xff…

数字信号处理第六次试验:数字信号处理在双音多频拨号系统中的应用

数字信号处理第六次试验&#xff1a;数字信号处理在双音多频拨号系统中的应用前言一、实验目的二、实验原理和方法1.关于双音多频拨号系统2.电话中的双音多频&#xff08;DTMF&#xff09;信号的产生与检测3.检测DTMF信号的DFT参数选择4.DTMF信号的产生与识别仿真实验三、实验内…

菜鼠的保研总结

1.个人基本情况 本科学校&#xff1a;山东某双非 本科专业&#xff1a;网络工程 成绩排名&#xff1a;1/46 英语成绩&#xff1a;四级529&#xff0c;六级502 科研竞赛&#xff1a;美国大学生数学建模比赛特等奖提名、全国英语翻译比赛三等奖、山东省蓝桥杯java大学生B组三等奖…

C++基础:KMP

让我们先看一个问题&#xff1a;给定一个字符串 S&#xff0c;以及一个模式串 P&#xff0c;所有字符串中只包含大小写英文字母以及阿拉伯数字。模式串 P 在字符串 S 中多次作为子串出现。求出模式串 P 在字符串 S 中所有出现的位置的起始下标。输入格式第一行输入整数 N&#…