文件上傳與下載
文件上傳 -- 服務(wù)端
以Tomcat為服務(wù)器暴氏,Android客服端訪問Servlet研儒,經(jīng)Servlet處理邏輯,最終將文件上傳,這里就是簡單模擬該功能灵嫌,就將文件上傳到本機(jī)的D:\\upload
文件夾下辨泳。
還是貼出來服務(wù)端的代碼
package fileupload;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.enterprise.inject.New;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
@WebServlet("/Dservlet")
public class Dservlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. 創(chuàng)建配置工廠
DiskFileItemFactory factory = new DiskFileItemFactory();
// 2. 根據(jù)配置工廠創(chuàng)建解析請(qǐng)求中文件上傳內(nèi)容的解析器
ServletFileUpload upload = new ServletFileUpload(factory);
// 3. 判斷當(dāng)前請(qǐng)求是不是多段提交
if (!upload.isMultipartContent(request)) {
throw new RuntimeException("不是多段提交猎塞!");
}
try {
// 4. 解析request對(duì)象倘屹,將已經(jīng)分割過的內(nèi)容放進(jìn)了List
List<FileItem> list = upload.parseRequest(request);
if (list != null) {
for (FileItem fileItem : list) {
// 判斷當(dāng)前段是普通字段還是文件,這個(gè)方法是判斷普通段
if (fileItem.isFormField()) {
// 獲得name屬性對(duì)應(yīng)的值,這里是username
String fname = fileItem.getFieldName();
// 獲得鍵對(duì)應(yīng)的值
String value = fileItem.getString("utf-8");
System.out.println(fname + "=>"+value );
// 否則就是文件了
} else {
// 獲得文件上傳段中践瓷,文件的流
InputStream in = fileItem.getInputStream();
// 使用用戶上傳的文件名來保存文件的話巫员,文件名可能重復(fù)七扰。
// 所以保存文件之前锐膜,要保證文件名不會(huì)重復(fù)粹排。使用UUID生成隨機(jī)字符串
String fileName = UUID.randomUUID().toString();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("/yyyy/MM/dd/");
String datePath = simpleDateFormat.format(new Date()); // 解析成 /2017/04/15/ 的樣子, 注意這是三個(gè)文件夾
String wholePath = "D:/upload"+datePath;
// 字節(jié)輸出流弄抬,用以保存文件,也不需要后綴名鸯两,因?yàn)槲覀冎皇潜4嬗脩舻臄?shù)據(jù)机错,不需要查看他們的數(shù)據(jù)帘饶。待用戶想下載的時(shí)候颗搂,再加上后綴名
File dir = new File(wholePath);
// mkdirs可以建立多級(jí)目錄。即使所有層級(jí)的目錄都不存在。這些文件夾都會(huì)創(chuàng)建,比如我們事先并沒有創(chuàng)建在D盤創(chuàng)建upload和2017等這些文件夾
// mkdir只能用于父級(jí)目錄已經(jīng)存在的情況下使用,在已存在的父級(jí)目錄下再新建一級(jí)悯周。只能一級(jí)!比如File("D:\\upload\\2017\\04")劳翰。且D:\\upload\\2017是已經(jīng)存在的汉买。父級(jí) 目錄存且只新建一級(jí)。故file.makedir()返回true成功創(chuàng)建啸如。
// 但是File("D:\\upload\\2017\\04\\15")且D:\\upload\\2017存在,但不存在15文件夾模她。因?yàn)楦讣?jí)目錄不存在所以創(chuàng)建失敗返回false
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(wholePath+fileName);
// 將輸入流復(fù)制到輸出流中
IOUtils.copy(in, fos);
fos.close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
文件上傳 -- 客戶端
待上傳的文件放在sdcard的根目錄下旋膳。因?yàn)橐L問網(wǎng)絡(luò)垄懂,讀寫外部存儲(chǔ)电抚。所以要先申請(qǐng)權(quán)限肺然。要注意的是,從Android 6.0開始,讀寫內(nèi)存需要?jiǎng)討B(tài)申請(qǐng)。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
留意一點(diǎn),這里雖然只是申請(qǐng)了寫如外部存儲(chǔ)的權(quán)限,但是這一聲明會(huì)隱式包含READ_EXTERNAL_STORAGE
這一權(quán)限。故上述申請(qǐng)就好了据某。
布局也很簡單,一個(gè)輸入框手動(dòng)填寫路徑,一個(gè)按鈕請(qǐng)求上傳
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.fileopload.MainActivity">
<EditText
android:id="@+id/et_filepath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="請(qǐng)輸入文件路徑"/>
<Button
android:id="@+id/bt_upload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="上傳"/>
</LinearLayout>
MainActivity
package com.example.fileopload;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity {
public static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private EditText editText;
private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
// 申請(qǐng)并獲得權(quán)限
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE} ,1);
}
// 獲取控件
editText = (EditText) findViewById(R.id.et_filepath);
Button btUpload = (Button) findViewById(R.id.bt_upload);
btUpload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fileupload(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "上傳失敽采唷航棱!", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "上傳成功!", Toast.LENGTH_SHORT).show();
}
});
}
});
}
});
}
public void fileupload(Callback callback) {
// 獲得輸入框中的路徑
String path = editText.getText().toString().trim();
File file = new File(path);
OkHttpClient client = new OkHttpClient();
// 上傳文件使用MultipartBody.Builder
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("username", "sunhaiyu") // 提交普通字段
.addFormDataPart("image", file.getName(), RequestBody.create(MEDIA_TYPE_PNG, file)) // 提交圖片,第一個(gè)參數(shù)是鍵(name="第一個(gè)參數(shù)"),第二個(gè)參數(shù)是文件名,第三個(gè)是一個(gè)RequestBody
.build();
// POST請(qǐng)求
Request request = new Request.Builder()
.url("http://10.175.42.160:8080/fileupload/Dservlet")
.post(requestBody)
.build();
client.newCall(request).enqueue(callback);
}
}
上面的代碼很簡單唯灵,唯一要注意的就是需要申請(qǐng)運(yùn)行時(shí)權(quán)限狡蝶。
OK,上傳成功涌乳。去D:\\upload
文件夾下,就能看到我們從手機(jī)上傳的圖片!
多文件同時(shí)下載
先實(shí)現(xiàn)同時(shí)下載幾個(gè)文件。
常見的下載文件有兩種情況。
- 先請(qǐng)求一次文件下載地址敷扫,獲取到文件的大小而芥,在本地創(chuàng)建一個(gè)和待下載文件一樣大小的文件,作為占位歌逢。這樣有個(gè)好處就是當(dāng)磁盤空間不足的時(shí)候巾钉,剛開始下載系統(tǒng)就會(huì)提醒。
- 也是先獲得文件大小秘案,創(chuàng)建空文件(不設(shè)置大信椴浴)從待下載文件處讀取到了多少,就往本地的文件寫入多少阱高,因此占用的空間在不斷增長赚导。
我們要實(shí)現(xiàn)同時(shí)下載多個(gè)文件,首先要知道待下載文件的大小赤惊,這樣才能知道什么時(shí)候才算下載成功了吼旧。也方便進(jìn)度條根據(jù)已下載的大小和總大小的比值顯示準(zhǔn)確的進(jìn)度。
綜上未舟,具體步驟如下
- 請(qǐng)求服務(wù)器獲取文件大小
- 為每個(gè)下載任務(wù)開一個(gè)子線程
- 開啟線程并行下載圈暗,顯示進(jìn)度條
主布局很簡單,準(zhǔn)備同時(shí)下載三個(gè)文件处面,點(diǎn)一個(gè)按鈕就開始一個(gè)任務(wù)厂置。注意:在沒有開始下載的時(shí)候积锅,不要顯示進(jìn)度條囚痴。點(diǎn)擊了下載按鈕才開始下載辐怕。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.filedownload.MainActivity"
android:layout_margin="16dp"
>
<Button
android:id="@+id/bt_download1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="下載1" />
<!--進(jìn)度條開始下載時(shí)才顯示-->
<ProgressBar
android:visibility="invisible"
android:id="@+id/progress1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginTop="10dp"/>
<Button
android:id="@+id/bt_download2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="下載2" />
<ProgressBar
android:visibility="invisible"
android:id="@+id/progress2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginTop="10dp"/>
<Button
android:id="@+id/bt_download3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="下載3" />
<ProgressBar
android:visibility="invisible"
android:id="@+id/progress3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginTop="10dp"/>
</LinearLayout>
MainActivity
package com.example.filedownload;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private String[] urls = {"https://mirrors.tuna.tsinghua.edu.cn/cygwin/x86_64/setup.bz2",
"https://mirrors.tuna.tsinghua.edu.cn/centos/filelist.gz",
"https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda-3.6.0-Linux-x86.sh"};
private Context mContext;
private ProgressBar progressbar1;
private ProgressBar progressbar2;
private ProgressBar progressbar3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
progressbar1 = (ProgressBar) findViewById(R.id.progress1);
progressbar2 = (ProgressBar) findViewById(R.id.progress2);
progressbar3 = (ProgressBar) findViewById(R.id.progress3);
Button btDownload1 = (Button) findViewById(R.id.bt_download1);
Button btDownload2 = (Button) findViewById(R.id.bt_download2);
Button btDownload3 = (Button) findViewById(R.id.bt_download3);
btDownload1.setOnClickListener(this);
btDownload2.setOnClickListener(this);
btDownload3.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bt_download1:
progressbar1.setVisibility(View.VISIBLE);
fileDownload(urls[0], progressbar1);
break;
case R.id.bt_download2:
progressbar2.setVisibility(View.VISIBLE);
fileDownload(urls[1], progressbar2);
break;
case R.id.bt_download3:
progressbar3.setVisibility(View.VISIBLE);
fileDownload(urls[2], progressbar3);
break;
default:
}
}
public void fileDownload(final String url, final ProgressBar progressBar) {
new Thread(new Runnable() {
@Override
public void run() {
InputStream is;
File file = null;
RandomAccessFile savedFile = null;
try {
long fileLength = getFileLength(url);
final String fileName = getFileName(url);
long downLoadLength = 0;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
if (response != null && response.isSuccessful()) {
// 應(yīng)用關(guān)聯(lián)目錄窝稿,無需申請(qǐng)讀寫存儲(chǔ)的運(yùn)行時(shí)權(quán)限
// 位于/sdcard/Android/data/包名/cache
file = new File(getExternalCacheDir() + fileName);
// 隨機(jī)訪問野揪,可通過seek方法定位到文件的任意位置访忿,方便斷點(diǎn)續(xù)傳
savedFile = new RandomAccessFile(file, "rw");
is = response.body().byteStream();
byte[] buffer = new byte[1024];
int len;
int total = 0;
while ((len = is.read(buffer)) != -1) {
savedFile.write(buffer, 0, len);
total += len;
// 注意這里要先乘以100再除,否則java的除法中小數(shù)直接抹去后面的斯稳,我們得到的比值比如0.5直接就變成0海铆,progress也就為0了
int progress = (int) ((total + downLoadLength) * 100 / fileLength);
progressBar.setProgress(progress);
}
// response.body().string()只能調(diào)用一次,再次調(diào)用報(bào)錯(cuò)挣惰。
// 寫完后可以把body關(guān)了
response.body().close();
// 能運(yùn)行到這兒說明下載成功
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "下載成功", Toast.LENGTH_SHORT).show();
}
});
// response為空或者請(qǐng)求的狀態(tài)碼沒有成功
} else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "下載失敗", Toast.LENGTH_SHORT).show();
}
});
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (savedFile != null) {
savedFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
// 獲得文件長度
private long getFileLength(String url) throws IOException{
long contentLength = 0;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
// 有響應(yīng)且不為空
if (response != null && response.isSuccessful()) {
contentLength = response.body().contentLength();
response.body().close();
}
return contentLength;
}
// 得到的是 /xxx.xxx,注意有斜杠
private String getFileName(String url) {
return url.substring(url.lastIndexOf("/"));
}
}
寫到這里就實(shí)現(xiàn)了多文件同時(shí)下載卧斟,去/sdcard/Android/data/com.example.filedownload/cache/
目錄下就能找到剛才下載的文件。上面有些變量和控件沒有用到憎茂,為下面多線程下載單個(gè)文件作準(zhǔn)備珍语。
多線程下載單個(gè)文件
上面的實(shí)現(xiàn)都是一個(gè)線程管一個(gè)下載任務(wù),這樣速度比較慢竖幔“逡遥可以將文件切割成幾部分,同時(shí)由幾個(gè)線程并發(fā)下載拳氢,一定要保證每個(gè)線程負(fù)責(zé)的那一段文件的開始位置和結(jié)束位置精準(zhǔn)無誤募逞。舉個(gè)例子,一個(gè)文件有10M馋评,開三個(gè)線程下載這個(gè)文件放接,平均一段是10 / 3
M,那么第一段是02留特,第二段必須是35纠脾,最后一段稍微長點(diǎn),從6~ length-1磕秤。
同時(shí)還要實(shí)現(xiàn)斷點(diǎn)續(xù)傳乳乌,在讀取流的while循環(huán)里,不斷存入當(dāng)前子線程的已下載字節(jié)數(shù)市咆,使得退出應(yīng)用的時(shí)候存取的剛好是已下載的字節(jié)數(shù)汉操。下次進(jìn)入應(yīng)用的時(shí)候,再從數(shù)據(jù)庫里取出已下載的字節(jié)數(shù)蒙兰,使用RandomAccessFile快速定位到該位置磷瘤。然后再次請(qǐng)求下載的時(shí)候,一定要加上一個(gè)RANGE頭搜变,指定從哪個(gè)位置開始訪問這個(gè)資源采缚。當(dāng)然是從已下載好部分開始。當(dāng)然第一次進(jìn)入應(yīng)用還沒下載過的時(shí)候挠他,數(shù)據(jù)庫肯定是空的扳抽,這時(shí)候肯定取不到已下載字節(jié)數(shù),就需要作具體判斷了。當(dāng)文件下載完成贸呢,將保存斷點(diǎn)的數(shù)據(jù)刪除镰烧,因?yàn)橐呀?jīng)沒用了嘛。
到此文件下載功能完成楞陷≌睿可以拓展一下,增加暫停下載和取消下載的功能固蛾。其中暫停下載就是簡單地不將流寫入结执,而取消下載則需要?jiǎng)h除文件及斷點(diǎn)。
0O揍!!注意不能使用SharedPreferences览芳,當(dāng)刪除斷點(diǎn)時(shí)候有問題斜姥。我嘗試過直接刪除xml文件,文件確實(shí)可以刪除成功沧竟,但是不知為何又會(huì)重新恢復(fù)铸敏,而且保存的還是上次的數(shù)據(jù);也嘗試過editor.clear()后提交悟泵,單線程下可以清空杈笔,在多線程的時(shí)候總是不能清空,還有數(shù)據(jù)糕非。怎么都不行蒙具。試試用數(shù)據(jù)庫來保存斷點(diǎn)。
一一實(shí)現(xiàn)上述功能朽肥。
這次的實(shí)現(xiàn)我們一開始就設(shè)置和待下載文件一樣的長度禁筏。有如下好處:
- 一開始就設(shè)置和待下載文件一樣的長度,可以避免下載結(jié)束后才告知磁盤空間不足衡招。
- 如果不設(shè)置篱昔,seek函數(shù)不斷移動(dòng)到文件末尾,不斷開辟空間始腾。頻繁的I/O操作降低了性能
由于需要用到數(shù)據(jù)庫州刽,準(zhǔn)備一個(gè)Bean和數(shù)據(jù)庫幫助類。
package com.example.filedownload.dao;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class MyDatabaseHelper extends SQLiteOpenHelper {
private Context mContext;
public static final String CREATE_TASK = "create table point ("
+ "_id integer primary key autoincrement,"
+ "task text,"
+ "thread integer,"
+ "position integer);";
public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TASK);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO: 2017/4/20
}
}
bean對(duì)應(yīng)數(shù)據(jù)庫的字段
package com.example.filedownload.bean;
public class Task {
public int _id;
public String task;
public int thread;
public long position;
}
實(shí)現(xiàn)數(shù)據(jù)庫的增刪改查
package com.example.filedownload.dao;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.example.filedownload.bean.Task;
public class TaskDao {
private SQLiteDatabase db;
public TaskDao(Context context, String dbName, int version) {
MyDatabaseHelper databaseHelper = new MyDatabaseHelper(context, dbName, null, version);
db = databaseHelper.getReadableDatabase();
}
// 更新斷點(diǎn)
public void savePoint(Task taskName) {
// update point set position = ? where task = ? and thread = ?;
ContentValues values = new ContentValues();
values.put("position", taskName.position);
db.update("point",values, "task = ? and thread = ?", new String[] {taskName.task, String.valueOf(taskName.thread)});
}
// 每次有一個(gè)線程浪箭,就準(zhǔn)備一個(gè)斷點(diǎn)
public void addPoint(Task taskName) {
// insert into point(task, thread, position) values(?, ?, ?);
ContentValues values = new ContentValues();
values.put("task", taskName.task);
values.put("thread", taskName.thread);
values.put("position", taskName.position);
db.insert("point", null, values);
}
// 下載完成后穗椅,刪除已下載文件的所有斷點(diǎn)
// delete from point where task = ?;
public void delete(String taskName) {
db.delete("point", "task = ?", new String[]{taskName});
}
// 從數(shù)據(jù)庫獲取斷點(diǎn)
public long getLastPoint(String taskName, int threadId) {
// 沒有斷點(diǎn)就返回-1
long lastPoint = -1;
// select position form point where task = ? and thread = ?;
Cursor cursor = db.query("point", new String[] {"position"}, "task = ? and thread = ? ", new String[]{taskName, String.valueOf(threadId)}, null, null, null);
// 條件,游標(biāo)能否定位到下一行奶栖。這里只有一個(gè)唯一結(jié)果用if就行
if (cursor.moveToNext()) {
lastPoint = cursor.getLong(cursor.getColumnIndex("position"));
}
// 關(guān)閉結(jié)果集
cursor.close();
return lastPoint;
}
}
布局簡化匹表,只下載一個(gè)文件门坷。要寫成多文件下載,就現(xiàn)在的所學(xué)寫起來比較麻煩桑孩,所以就簡化了拜鹤。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.filedownload.MainActivity"
android:layout_margin="16dp"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/bt_download"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="下載" />
<Button
android:visibility="invisible"
android:id="@+id/bt_pause"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="暫停" />
<Button
android:visibility="invisible"
android:id="@+id/bt_cancel"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="取消" />
</LinearLayout>
<!--進(jìn)度條開始下載時(shí)才顯示-->
<ProgressBar
android:visibility="invisible"
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginTop="10dp"/>
</LinearLayout>
最后來看MainActivity框冀,一些細(xì)節(jié)注意一下流椒。剛進(jìn)入應(yīng)用,只能看見“下載”的按鈕明也,其余按鈕被隱藏宣虾。當(dāng)用戶點(diǎn)擊下載按鈕后,暫停和取消的按鈕才可見温数。當(dāng)當(dāng)文件下載成功后绣硝,這兩個(gè)按鈕又變成不可見。在下載過程中撑刺,可以點(diǎn)擊暫停以跳過流寫入和斷點(diǎn)存儲(chǔ)的步驟鹉胖;點(diǎn)擊取消,刪除文件及斷點(diǎn)够傍。
下載過程中用到三個(gè)標(biāo)志位isDownloading
甫菠、isPaused
、isCanceled
- 每次點(diǎn)擊下載冕屯,將
isPaused
和isCanceled
置為false寂诱。isDownloading
得分情況,第一次進(jìn)入應(yīng)用安聘,isDownloading沒有初始化痰洒,默認(rèn)false,故其下代碼得到執(zhí)行浴韭,isDownloading變true丘喻。之后若沒有點(diǎn)擊過暫停或者取消按鈕念颈,isDownloading標(biāo)志位并沒有變化泉粉,則不會(huì)重復(fù)執(zhí)行download的代碼。 - 每次點(diǎn)擊暫停舍肠,就把isDownloading置為false搀继,isPaused置為true。
- 每次點(diǎn)擊取消翠语,就把isDownloading置為false叽躯,isCanceled置為true。
package com.example.filedownload;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.example.filedownload.bean.Task;
import com.example.filedownload.dao.TaskDao;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private String fileUrl = "https://mirrors.tuna.tsinghua.edu.cn/mysql/downloads/Win32/Perl-5.00502-mswin32-1.1-x86.zip";
private Context mContext;
// 設(shè)置數(shù)據(jù)庫連接為全局變量肌括。所有線程共用一個(gè)數(shù)據(jù)庫連接点骑。也不會(huì)close掉酣难。
// 所有線程用了就close的話,可能A線程在close的時(shí)候黑滴,B線程又想打開連接進(jìn)行讀寫憨募。
// 不頻繁開關(guān)連接,性能更好袁辈。等到生命周期結(jié)束才自動(dòng)close
private TaskDao taskDao;
private ProgressBar progressbar;
private Button btPause;
private Button btCancel;
private boolean isDownloading;
private boolean isPaused;
private boolean isCanceled;
// 可以改線程數(shù)目菜谣,不要太多。原因你懂的
public static final int THREAD_COUNT = 5;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
// 一進(jìn)應(yīng)用就創(chuàng)建數(shù)據(jù)庫
taskDao = new TaskDao(mContext, "tasks.db", 1);
progressbar = (ProgressBar) findViewById(R.id.progress);
Button btDownload1 = (Button) findViewById(R.id.bt_download);
btPause = (Button) findViewById(R.id.bt_pause);
btCancel = (Button) findViewById(R.id.bt_cancel);
btDownload1.setOnClickListener(this);
btPause.setOnClickListener(this);
btCancel.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bt_download:
btPause.setVisibility(View.VISIBLE);
btCancel.setVisibility(View.VISIBLE);
progressbar.setVisibility(View.VISIBLE);
fileDownload(fileUrl, progressbar, btPause, btCancel);
Toast.makeText(mContext, "正在下載...", Toast.LENGTH_SHORT).show();
break;
case R.id.bt_pause:
pauseDownload();
Toast.makeText(mContext, "下載暫停", Toast.LENGTH_SHORT).show();
break;
case R.id.bt_cancel:
btPause.setVisibility(View.INVISIBLE);
btCancel.setVisibility(View.INVISIBLE);
progressbar.setVisibility(View.INVISIBLE);
canceledDownload(fileUrl);
Toast.makeText(mContext, "下載取消晚缩,刪除文件...", Toast.LENGTH_SHORT).show();
break;
default:
}
}
private class DownloadTask implements Runnable {
private int thread;
private long startIndex;
private long endIndex;
private long lastPosition;
private String url;
public DownloadTask(String url, int thread, long startIndex, long endIndex) {
this.thread = thread;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.url = url;
}
@Override
public void run() {
// 先嘗試讀取斷點(diǎn)尾膊,兩種情況可導(dǎo)致不存在斷點(diǎn)。
// 1. 第一次進(jìn)入應(yīng)用荞彼,還沒開始下載
// 2. 下載完畢冈敛,斷點(diǎn)被刪除。重新下載
// 能讀取到鸣皂,肯定下了一部分但是沒下載完
if (taskDao.getLastPoint(getFileName(url), thread) != -1) { // -1表示找不到鍵對(duì)應(yīng)的值
lastPosition = taskDao.getLastPoint(getFileName(url), thread);
// 如果這部分下載完畢抓谴,直接返回,不再請(qǐng)求網(wǎng)絡(luò)
if (lastPosition == endIndex + 1) {
return;
}
}
// 沒找到就重新下載
else {
lastPosition = startIndex;
}
OkHttpClient client = new OkHttpClient();
// 設(shè)置RANGE頭寞缝,分段文件下載癌压,從上次下載處繼續(xù)
Request request = new Request.Builder().addHeader("RANGE", "bytes=" + lastPosition + "-" + endIndex)
.url(url)
.build();
File file = null;
RandomAccessFile savedFile = null;
try {
Response response = client.newCall(request).execute();
if (response != null && response.isSuccessful()) {
// 應(yīng)用關(guān)聯(lián)目錄,無需申請(qǐng)讀寫存儲(chǔ)的運(yùn)行時(shí)權(quán)限
// 位于/sdcard/Android/data/包名/cache
file = new File(getExternalCacheDir() + "/" + getFileName(url));
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(lastPosition);
// 響應(yīng)成功了準(zhǔn)備斷點(diǎn)
// new 一個(gè)task,初始化task和thread和position
Task threadTask = new Task();
threadTask.task = getFileName(url);
threadTask.thread = thread;
// 上面的兩個(gè)是固定的第租,更新的時(shí)候只更新position
threadTask.position = -1;
// 必須先插入這條新的數(shù)據(jù)措拇,才能在下面對(duì)其update
taskDao.addPoint(threadTask);
InputStream is = response.body().byteStream();
byte[] buffer = new byte[1024 * 1024];
int len;
int total = 0;
while ((len = is.read(buffer)) != -1) {
if (!isPaused && !isCanceled) {
savedFile.write(buffer, 0, len);
total += len;
threadTask.position = total + lastPosition;
// 保存斷點(diǎn)
taskDao.savePoint(threadTask);
}
}
// 寫完后可以把body關(guān)了
response.body().close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (savedFile != null) {
savedFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void fileDownload(final String url, final ProgressBar progressBar, final Button pause, final Button cancel) {
// 每次開始下載,自然要把這兩個(gè)標(biāo)志位置為false
isPaused = false;
isCanceled = false;
// 注意boolean沒有初始化默認(rèn)為false慎宾,第一次進(jìn)入點(diǎn)擊下載肯定會(huì)執(zhí)行丐吓,此后isDownloading為true。
// 之后若沒有點(diǎn)擊暫停取消趟据,標(biāo)志位保持true券犁。多次重復(fù)點(diǎn)擊下載按鈕,標(biāo)志位沒有改變故不會(huì)執(zhí)行
if (!isDownloading) {
new Thread(new Runnable() {
@Override
public void run() {
isDownloading = true;
RandomAccessFile savedFile = null;
try {
final String fileName = getFileName(url);
long fileLength = getFileLength(url);
long partLength = fileLength / THREAD_COUNT;
// 應(yīng)用關(guān)聯(lián)目錄汹碱,無需申請(qǐng)讀寫存儲(chǔ)的運(yùn)行時(shí)權(quán)限, 位于/sdcard/Android/data/包名/cache
File file = new File(getExternalCacheDir() + "/" + fileName);
// 隨機(jī)訪問粘衬,可通過seek方法定位到文件的任意位置,方便斷點(diǎn)續(xù)傳咳促。
savedFile = new RandomAccessFile(file, "rw");
// 一開始就設(shè)置和待下載文件一樣的長度稚新,可以避免下載結(jié)束后才告知磁盤空間不足
// 如果不設(shè)置,seek函數(shù)不斷移動(dòng)到文件末尾跪腹,不斷開辟空間褂删。頻繁的I/O操作降低了性能
savedFile.setLength(fileLength);
// 下面的算法適用于THREAD_COUNT等于任何數(shù)值
for (int thread = 0; thread < THREAD_COUNT; thread++) {
long startIndex = thread * partLength;
long endIndex = (thread + 1) * partLength - 1;
// 如果是最后一段,剩余的全部
if (thread == THREAD_COUNT - 1) {
endIndex = fileLength - 1;
}
// 開啟線程下載
new Thread(new DownloadTask(url, thread, startIndex, endIndex)).start();
}
while (true) {
long totalProgress = 0;
for (int i = 0; i < THREAD_COUNT; i++) {
// 所有段加起來的下載字節(jié)數(shù)冲茸。推導(dǎo)一下屯阀,很簡單
totalProgress += taskDao.getLastPoint(getFileName(url), i) - i * partLength;
}
// 這里有先乘100再除缅帘,否則先除是零點(diǎn)幾,java除法抹去小數(shù)后就是0难衰,再乘100也還是0
int progress = (int) (totalProgress * 100 / fileLength);
progressBar.setProgress(progress);
if (totalProgress == fileLength) {
progressBar.setProgress(100);
// 運(yùn)行到此說明下載成功
taskDao.delete(getFileName(url));
runOnUiThread(new Runnable() {
@Override
public void run() {
pause.setVisibility(View.INVISIBLE);
cancel.setVisibility(View.INVISIBLE);
Toast.makeText(mContext, "下載成功", Toast.LENGTH_SHORT).show();
}
});
break;
}
}
} catch (IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "下載失敗", Toast.LENGTH_SHORT).show();
}
});
e.printStackTrace();
} finally {
try {
if (savedFile != null) {
savedFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
// 獲得文件長度
private long getFileLength(String url) throws IOException {
long contentLength = 0;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
// 有響應(yīng)且不為空
if (response != null && response.isSuccessful()) {
contentLength = response.body().contentLength();
response.body().close();
}
return contentLength;
}
// 得到的是 xxx.xxx,注意不帶斜杠
private String getFileName(String url) {
return url.substring(url.lastIndexOf("/") + 1);
}
// 暫停下載
private void pauseDownload() {
isPaused = true;
isDownloading = false;
}
// 取消下載
private void canceledDownload(String url) {
isCanceled = true;
isDownloading = false;
File file = new File(getExternalCacheDir() + "/" + getFileName(url));
if (file.exists()) {
file.delete();
}
taskDao.delete(getFileName(url));
}
}
最后來看下截圖
點(diǎn)擊下載按鈕
點(diǎn)擊暫停按鈕
點(diǎn)擊取消按鈕
終于折騰完了钦无,代碼太粗糙。當(dāng)然下載功能不能這樣寫盖袭。體驗(yàn)更好的做法是使用后臺(tái)服務(wù)下載文件失暂,這樣我們可以在使用其他應(yīng)用的時(shí)候繼續(xù)保持下載。而且上面的代碼苍凛,異步處理可能有意想不到的異常趣席,最好使用AsyncTask更方便的進(jìn)行異步消息處理。
by @sunhaiyu
2017.5.3