Android入門之利用OKHttp實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能
簡介
今天我們將繼續(xù)使用OkHttp組件并制作一個基于多線程的可斷點(diǎn)續(xù)傳的下載器來結(jié)束Android OkHttp組件的所有知識內(nèi)容。在這一課里我們會在上一次課程的基礎(chǔ)上增加SQLite的使用以便于我們的App可以暫存下載時的實(shí)時進(jìn)度,每次下載開始都會判斷是覆蓋式還是續(xù)傳式下載。同時由于Android自帶的進(jìn)度條太丑了,我們對它稍稍進(jìn)行了一些美化??梢哉f今天這篇教程也是一篇階段性的功能整合實(shí)驗(yàn)。
下面開始進(jìn)入課程。
課程目標(biāo)
1.使用SQLite進(jìn)行下載時的進(jìn)度信息的暫存;
2.自定義ProgressBar的樣式;
斷點(diǎn)下載的原理
如果你認(rèn)真的在看完了上篇教程后并且脫離我的Sample代碼自己動手實(shí)現(xiàn)了一個多線程下載器的話那么今天這篇教程對于你來說會變得相當(dāng)?shù)暮唵巍?/p>
因?yàn)樗^的斷點(diǎn)下載就是把每一條線程當(dāng)前在下載的信息存入一個SQLite的表內(nèi)。而斷點(diǎn)下載就是通過暫存的信息去改變RandomAcessFile在寫入時的seek。
當(dāng)然這里面還伴隨著一些小技巧,我們需要我們的APP的“STOP”動作可以打斷正在下載的進(jìn)度,打斷后如果再次點(diǎn)擊了“DOWNLOAD”按鈕,此時各子線程做的任務(wù)為“續(xù)傳”,續(xù)傳的進(jìn)度是否完成了呢這也需要子線程和主線程間進(jìn)行狀態(tài)通信。
需要知道每個子線程運(yùn)行是否已經(jīng)結(jié)束了
這邊并不是需要知道每個子線程的返回、中間態(tài)。我們只是需要知道每一個子線程是否運(yùn)行完了。
在平時開發(fā)中我們經(jīng)常會面臨這樣的一種情況。比如說我們外部需要長時間的等待?或者也有開發(fā)搞了一個全局的棧去計(jì)算、也有用future接口的。很多時候往往為了取一個狀態(tài),開發(fā)創(chuàng)造了一堆的“輪子”,導(dǎo)致了整個項(xiàng)目代碼過于復(fù)雜以及不好調(diào)試。因此這些手法都不是很優(yōu)雅。今天筆者給各位推薦一種更為優(yōu)雅的寫法,以便于在外部判斷每一個子線程是否都運(yùn)行完畢了。
使用狀態(tài)反轉(zhuǎn)來不斷check子線程狀態(tài)
其實(shí)它的核心思路是:
- 在外部有一個無限while 循環(huán),while(notFinish);
- 循環(huán)入口上手就把循環(huán)終止, notFinish=false;
- 接著依次檢查每一個子線程內(nèi)的一個狀態(tài)值-finish,這個值在每個子線程內(nèi)任務(wù)結(jié)束后會設(shè)為true。只要這個值在外部被檢測到不為true,那么把外部循環(huán)的狀態(tài)再改為notFinish=true,以使得外部循環(huán)不斷運(yùn)行直到所有子線程檢測下來都確為finish,此時外部的while循環(huán)跳出;
每個子線程下載的實(shí)時信息存儲
我們設(shè)計(jì)了一個這樣的表結(jié)構(gòu)用來存儲下載的實(shí)時信息。
- 每次下載進(jìn)程開始時,先根據(jù)下載URL去該表中查出所有的下載信息。比如說我們開啟了3個線程,那么對于同一個URL:/test.zip可以根據(jù)download_path查出3條數(shù)據(jù)。把3條數(shù)據(jù)的download_length相加拼在一起,如果<當(dāng)前遠(yuǎn)程文件size說明上次下載沒有完成,那么繼續(xù)下載。否則新建一個空文件并把這個空文件的長度設(shè)定為遠(yuǎn)程資源文件的長度;
- 每個子線程在下載時不斷根據(jù)download_path update這張表里的數(shù)據(jù)把當(dāng)前的實(shí)時進(jìn)度寫進(jìn)去;
- 下載完后根據(jù)download_path清空這個表里的數(shù)據(jù);
Http Get請求如何支持?jǐn)帱c(diǎn)續(xù)傳
Request.addHeader("Range", "bytes=" + startPos + "-" + endPos)
假設(shè)線程編號從1開始,開了3個子線程,共有1-3個線程,線程編號為1-3,此處的startPos和endPos的計(jì)算公式如下:
- startPos=每個線程分頁下載文件大小*線程編號+上一次下載進(jìn)度,如果線程為1號線程那么startPos=上一次的下載進(jìn)度;
- endPos=每個線程分頁下載文件大小*當(dāng)前線程編號-1,-1代表“不計(jì)算文件末尾結(jié)束符”;
int startPos = block * (threadId - 1) + downLength;//開始位置 int endPos = block * threadId - 1;//結(jié)束位置
自定義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>
這是一個很淺很淡的藍(lán)色。
第二步:
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> <!-- 進(jìn)度條 --> <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" />
以上內(nèi)容都準(zhǔn)備好了,我們就可以進(jìn)入全代碼了。
項(xiàng)目結(jié)構(gòu)
前端代碼
<?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的每條線程已經(jīng)下載的文件長度 * @param downloadPath * @return * */ public List<DWManagerInfor> getData(String downloadPath) { //獲得可讀數(shù)據(jù)庫句柄,通常內(nèi)部實(shí)現(xiàn)返回的其實(shí)都是可寫的數(shù)據(jù)庫句柄 //根據(jù)下載的路徑查詢所有現(xiàn)場的下載數(shù)據(jù),返回的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();//關(guān)閉cursor,釋放資源; }catch(Exception e){} try { db.close(); }catch(Exception e){} } return data; } /** * 保存每條線程已經(jīng)下載的文件長度 */ public void save(String downloadPath, Map<Integer,Integer> map) { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.beginTransaction(); try{ //使用增強(qiáng)for循環(huán)遍歷數(shù)據(jù)集合 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()}); } //設(shè)置一個事務(wù)成功的標(biāo)志,如果成功就提交事務(wù),如果沒調(diào)用該方法的話那么事務(wù)回滾 //就是上面的數(shù)據(jù)庫操作撤銷 db.setTransactionSuccessful(); }catch(Exception e){ Log.e(TAG,">>>>>>save download infor into db error: "+e.getMessage(),e); }finally{ //結(jié)束一個事務(wù) 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
這是一個主要的用于啟動多線程下載和操作斷點(diǎn)信息的類,在這個類內(nèi)會分出3個子線程,每個子線程內(nèi)又把這個類的this傳入在子線程內(nèi)進(jìn)行回調(diào)、寫下載時的實(shí)時信息入庫、傳遞子線程狀態(tài),因此它是一個核心類。
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>(); //緩存?zhèn)€條線程的下載的長度 //private DBAdapter dbAdapter = null; private DBService dbService=null; private DownloadThread[] threads; //根據(jù)線程數(shù)設(shè)置下載的線程池 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, ">>>>>>觸發(fā)了exited"); this.exited = true; //將退出的標(biāo)志設(shè)置為true; } public boolean getExited() { return this.exited; } /** * 累計(jì)已下載的大小 * 使用同步鎖來解決并發(fā)的訪問問題 */ protected synchronized void append(int size) { //把實(shí)時下載的長度加入到總的下載長度中 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); //更新數(shù)據(jù)庫中制定線程的下載長度 } 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,">>>>>>新建文件并設(shè)定長度->"+fileLength); file = new RandomAccessFile(fileName, "rwd"); file.setLength(fileLength); file.close(); }else{ Log.i(TAG,">>>>>>文件存在,返回文件名進(jìn)行續(xù)傳"); } } 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內(nèi)已經(jīng)存在的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空文件并把文件長度設(shè)置好 Log.i(TAG, ">>>>>>開始生成線程進(jìn)行分: " + this.threadCount + " 條線程并行下載...每條線程的block->" + this.block); Log.i(TAG, ">>>>>>全局data size->" + data.size()); for (int i = 0; i < this.threads.length; i++) {//開啟線程進(jìn)行下載 int downLength = 0; if (data.size() > 0) { downLength = this.data.get(i + 1); } Log.i(TAG, ">>>>>>開啟前發(fā)覺當(dāng)前下載進(jìn)度為->" + downLength); //通過特定的線程id獲取該線程已經(jīng)下載的數(shù)據(jù)長度 //判斷線程是否已經(jīng)完成下載,否則繼續(xù)下載 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); //設(shè)置線程優(yōu)先級,Thread.NORM_PRIORITY = 5; //Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,數(shù)值越大優(yōu)先級越高 this.threads[i].setPriority(7); this.threads[i].start(); //啟動線程 } else { Log.i(TAG, "當(dāng)前線程不用下載,因?yàn)楫?dāng)前線程己下載長度downLength->" + downLength + " block->" + this.block); this.threads[i] = null; //表明線程已完成下載任務(wù) } } //dbAdapter.open(); dbService.delete(downloadUrl); dbService.save(downloadUrl, this.data); //把下載的實(shí)時數(shù)據(jù)寫入數(shù)據(jù)庫中 boolean notFinish = true; //下載未完成 while (notFinish) { // 循環(huán)判斷所有線程是否完成下載 Thread.sleep(300); notFinish = false; for (int i = 0; i < threadCount; i++) { if (this.threads[i] != null && !this.threads[i].isFinish()) { //如果發(fā)現(xiàn)線程未完成下載 notFinish = true; //設(shè)置標(biāo)志為下載沒有完成,以便于外層while循環(huán)不斷check; } } if (downloadProgressListener != null) { downloadProgressListener.onDownloadSize(this.downloadedSize); } //通知目前已經(jīng)下載完成的數(shù)據(jù)長度 } 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)//設(shè)置連接超時時間 .readTimeout(10, TimeUnit.SECONDS).build();//設(shè)置讀取超時時間 Request request = new Request.Builder().url(downloadUrl)//請求接口,如果需要傳參拼接到接口后面 .build(); //創(chuàng)建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
這個類就是每一個子線程的實(shí)現(xiàn)了。在這個類里每一個子線程會啟動OkHttp并使用http-header: Range去做斷點(diǎn)下載。
值得注意的是,如果你的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設(shè)置 private int downLength; //該線程已下載的數(shù)據(jù)長度 private boolean finish = false; //該線程是否完成下載的標(biāo)志 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;//結(jié)束位置 OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)//設(shè)置連接超時時間 .readTimeout(10, TimeUnit.SECONDS)//設(shè)置讀取超時時間 .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(); //創(chuàng)建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, ">>>>>>下載進(jìn)程加載->" + downloadUrl + " error:" + e.getMessage(), e); finish = true; } //結(jié)束的回調(diào) @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { Log.i(TAG, ">>>>>>連接->" + downloadUrl + " 己經(jīng)連接,進(jìn)入下載..."); InputStream is = null; Log.i(TAG, ">>>>>>當(dāng)前: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; //設(shè)置該線程已經(jīng)下載的長度為-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; } /** * 已經(jīng)下載的內(nèi)容大小 * * @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, ">>>>>>下載中,當(dāng)前尺寸: " + 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(); } } } }
為了正確運(yùn)行上述內(nèi)容你需要在gradle的build文件內(nèi)加入OkHttp和commons-io的依賴包。
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation group: 'commons-io', name: 'commons-io', version: '2.6'
運(yùn)行后的效果
當(dāng)你無論如何stop再download再stop或者下載完后多次再download,那么當(dāng)文件被成功下載后,會在Android的資源列表里此處顯示下載的資源。
它位于data\media\0下。
為了驗(yàn)證你下載的正確性,你可以把這個資源右鍵->另存出去。然后雙擊這個安裝程序,如果它可以正確安裝那么說明你的斷點(diǎn)下載是正確了。
結(jié)束今天的課程,不妨自己動一下手試試看吧。
附、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中創(chuàng)建與刪除文件權(quán)限 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" tools:ignore="ProtectedPermissions" /> <!-- 往SDCard寫入數(shù)據(jù)權(quán)限 --> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!--外部存儲的寫權(quán)限--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!--外部存儲的讀權(quán)限--> <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>
以上就是Android入門之利用OKHttp實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能的詳細(xì)內(nèi)容,更多關(guān)于Android OKHttp斷點(diǎn)續(xù)傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android程序打開和對輸入法的操作(打開/關(guān)閉)
整理了一下Android下對輸入法的操作:打開輸入法窗口、關(guān)閉出入法窗口、如果輸入法打開則關(guān)閉,如果沒打開則打開、獲取輸入法打開的狀態(tài)2013-05-05在Android中 獲取正在運(yùn)行的Service 實(shí)例
本篇文章小編為大家介紹,在Android中 獲取正在運(yùn)行的Service 實(shí)例。需要的朋友參考下2013-04-04android判斷一個Activity是否處于棧頂?shù)膶?shí)例
下面小編就為大家分享一篇android判斷一個Activity是否處于棧頂?shù)膶?shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03Android 中ScrollView與ListView沖突問題的解決辦法
這篇文章主要介紹了Android 中ScrollView與ListView沖突問題的解決辦法的相關(guān)資料,希望通過本文能幫助到大家,讓大家掌握解決問題的辦法,需要的朋友可以參考下2017-10-10