简介
今天我们将继续使用OkHttp组件并制作一个基于多线程的可断点续传的下载器来结束Android OkHttp组件的所有知识内容。在这一课里我们会在上一次课程的基础上增加SQLite的使用以便于我们的App可以暂存下载时的实时进度,每次下载开始都会判断是覆盖式还是续传式下载。同时由于Android自带的进度条太丑了,我们对它稍稍进行了一些美化。可以说今天这篇教程也是一篇阶段性的功能整合实验。
下面开始进入课程。
课程目标
使用SQLite进行下载时的进度信息的暂存;
自定义ProgressBar的样式;
断点下载的原理
如果你认真的在看完了上篇教程后并且脱离我的Sample代码自己动手实现了一个多线程下载器的话那么今天这篇教程对于你来说会变得相当的简单。
因为所谓的断点下载就是把每一条线程当前在下载的信息存入一个SQLite的表内。而断点下载就是通过暂存的信息去改变RandomAcessFile在写入时的seek。
当然这里面还伴随着一些小技巧,我们需要我们的APP的“STOP”动作可以打断正在下载的进度,打断后如果再次点击了“DOWNLOAD”按钮,此时各子线程做的任务为“续传”,续传的进度是否完成了呢这也需要子线程和主线程间进行状态通信。
需要知道每个子线程运行是否已经结束了
这边并不是需要知道每个子线程的返回、中间态。我们只是需要知道每一个子线程是否运行完了。
在平时开发中我们经常会面临这样的一种情况。比如说我们外部需要长时间的等待?或者也有开发搞了一个全局的栈去计算、也有用future接口的。很多时候往往为了取一个状态,开发创造了一堆的“轮子”,导致了整个项目代码过于复杂以及不好调试。因此这些手法都不是很优雅。今天笔者给各位推荐一种更为优雅的写法,以便于在外部判断每一个子线程是否都运行完毕了。
使用状态反转来不断check子线程状态
其实它的核心思路是:
在外部有一个无限while 循环,while(notFinish);
循环入口上手就把循环终止, notFinish=false;
接着依次检查每一个子线程内的一个状态值-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>