Android文件上傳與下載

文件上傳與下載

文件上傳 -- 服務(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)度。

綜上未舟,具體步驟如下

  1. 請(qǐng)求服務(wù)器獲取文件大小
  2. 為每個(gè)下載任務(wù)開一個(gè)子線程
  3. 開啟線程并行下載圈暗,顯示進(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 / 3M,那么第一段是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)。

0O揍!!注意不能使用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甫菠、isPausedisCanceled

  • 每次點(diǎn)擊下載冕屯,將isPausedisCanceled置為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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末醇蝴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子想罕,更是在濱河造成了極大的恐慌悠栓,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件按价,死亡現(xiàn)場離奇詭異惭适,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)楼镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門癞志,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人框产,你說我怎么就攤上這事凄杯。” “怎么了秉宿?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵戒突,是天一觀的道長。 經(jīng)常有香客問我描睦,道長膊存,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任忱叭,我火速辦了婚禮隔崎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘韵丑。我一直安慰自己爵卒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布埂息。 她就那樣靜靜地躺著技潘,像睡著了一般遥巴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上享幽,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天铲掐,我揣著相機(jī)與錄音,去河邊找鬼值桩。 笑死摆霉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的奔坟。 我是一名探鬼主播携栋,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼咳秉!你這毒婦竟也來了婉支?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤澜建,失蹤者是張志新(化名)和其女友劉穎向挖,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炕舵,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡何之,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咽筋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溶推。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖奸攻,靈堂內(nèi)的尸體忽然破棺而出蒜危,到底是詐尸還是另有隱情,我是刑警寧澤舞箍,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布舰褪,位于F島的核電站,受9級(jí)特大地震影響疏橄,放射性物質(zhì)發(fā)生泄漏占拍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一捎迫、第九天 我趴在偏房一處隱蔽的房頂上張望晃酒。 院中可真熱鬧,春花似錦窄绒、人聲如沸贝次。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛔翅。三九已至敲茄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間山析,已是汗流浹背堰燎。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笋轨,地道東北人秆剪。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像爵政,于是被迫代替她去往敵國和親仅讽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,512評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫钾挟、插件洁灵、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,029評(píng)論 4 62
  • 《等愛的狐貍》 如果可以, 想變成一只等愛的狐貍等龙, 孤單靜躺在臨海的礁石地处渣, 云霞為彩手做筆。 如果可以蛛砰, 想變成...
    漫不經(jīng)閱讀 222評(píng)論 1 4
  • 1. 對(duì)于減肥人群琅翻,建議先進(jìn)行無氧運(yùn)動(dòng)再進(jìn)行有氧運(yùn)動(dòng)位仁,燃脂效率大大提高,減肥事半功倍方椎。有氧運(yùn)動(dòng)包括跑步聂抢、騎車、游泳...
    何舒卉閱讀 287評(píng)論 0 2