android——文件斷點續(xù)傳下載(一)

GIF.gif

文件的斷點續(xù)傳下載在項目中或多或少的會碰到,要實習(xí)該效果,先看看會涉及到哪些東西:
1、如果要顯示下載進度的話,就要自定義下載進度效果,自定義view是回涉及到的
2、需要將下載的文件存儲在sd卡中,android6.0后,對于sd卡的讀寫權(quán)限做了更改,需要動態(tài)去申請該權(quán)限,涉及到權(quán)限的適配
3、下載或暫停時需要將下載的進度存儲在數(shù)據(jù)庫中,下次繼承下載時先要要從數(shù)據(jù)庫中獲取上次下載的進度,從上次的下載進度繼續(xù)下載,涉及到數(shù)據(jù)庫的增刪改查
4、activity和service之間的數(shù)據(jù)傳遞和通信,會涉及到BroadcastReceiver廣播、Handler等使用
5、如果需要在通知欄顯示下載進度等信息,還需要做好android8.0 通知狀態(tài)欄的適配,該效果沒有實現(xiàn)這一點,就沒有做這方面的適配

自定義進度條、android6.0權(quán)限適配、數(shù)據(jù)庫的增刪改查封裝在這里就不多說了,可以參考下面的播客:
android酷炫下載進度條
關(guān)于android6.0的權(quán)限申請
Android數(shù)據(jù)庫面向?qū)ο笾?、刪、改、查

整體思路:
在用戶點擊開始下載時,會去做權(quán)限的申請,如果用戶賦予權(quán)限會開啟一個Serivce,整個下載都是在Service中進行,在Service的onStartCommand()方法中會去開啟一個線程通過網(wǎng)絡(luò)請求獲取到文件的大小,然后會在本地創(chuàng)建一個和文件大小的文件夾,用于文件的存儲,再通過Handler消息通知去開啟一個線程進行文件的下載任務(wù),在開始下載任務(wù)后會先在數(shù)據(jù)庫中查詢,如果沒有任何下載信息會實例化一個新的下載對象,如果有就根據(jù)之前的下載進度進行下載,在下載時會通過BroadcastReceiver將下載進度傳遞給activity去更新下載進度條,如果用戶點擊了暫停會將當(dāng)前的下載進度存入數(shù)據(jù)庫中,便于下次下載時獲取上次的下載進度,下載完畢后會刪除數(shù)據(jù)庫中的下載記錄,就根據(jù)上面的思路用代碼來實現(xiàn)吧。

用戶點擊開始下載時會調(diào)用btnStart()點擊事件的方法,申請權(quán)限,也會將下載信息的實例對象傳遞給Service;

public void btnStart(View view) {
        //權(quán)限申請
        PermissionHelper.with(MainActivity.this).
                requestPermission(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE}).
                requestCode(WRITE_EXTERNAL_REQUEST_DODE).
                request();

    }
    @PermissionSuccess(requestCode =WRITE_EXTERNAL_REQUEST_DODE)
    private void doSuccess(){
        //通過intent傳遞參數(shù)給service
        Intent intent = new Intent(this, DownLoadService.class);
        intent.setAction(DownLoadService.ACTION_START);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);
    }
    @PermissionFail(requestCode =WRITE_EXTERNAL_REQUEST_DODE)
    private void doFail(){

    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PermissionHelper.requestPermissionsResult(this,requestCode,permissions,grantResults);
    }

如果用戶點擊了暫停會調(diào)用btnStop()點擊事件方法,同樣的也會將下載信息實例對象傳遞給Service;

public void btnStop(View view) {
        //通過intent傳遞參數(shù)給service
        Intent intent = new Intent(this, DownLoadService.class);
        intent.setAction(DownLoadService.ACTION_STOP);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);
    }

開啟Service后,在Service中的onStartCommand()方法中獲取對應(yīng)的下載信息實例對象,如果是開始下載就去創(chuàng)建一個線程通過網(wǎng)絡(luò)請求獲取文件的大小,如果是暫停下載的話,就調(diào)用DownLoadTask下載任務(wù)類中的isPause,將其改為true;

@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            return super.onStartCommand(intent, flags, startId);
        }
        //獲取activity傳遞過來的參數(shù)
        if (ACTION_START.equals(intent.getAction())) {
            FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
            Log.e(TAG, ACTION_START);
            //開啟線程
            InitThread thread = new InitThread(fileInfo);
            thread.start();
        } else if (ACTION_STOP.equals(intent.getAction())) {
            FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
            Log.e(TAG, ACTION_STOP);
            if (mTask != null) {
                //暫停下載
                mTask.isPause = true;
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

在InitThread這個線程中去獲取文件的大小,同時創(chuàng)建一個和文件大小的文件用于存儲下載好的文件,再通過handler消息通知去下載該文件;

class InitThread extends Thread {
        private FileInfo mFileInfo;

        public InitThread(FileInfo fileInfo) {
            this.mFileInfo = fileInfo;
        }

        @Override
        public void run() {
            super.run();
            HttpURLConnection conn = null;
            RandomAccessFile faf = null;
            int length = -1;
            try {
                //連接網(wǎng)絡(luò)文件,
                URL url = new URL(mFileInfo.url);
                conn = (HttpURLConnection) url.openConnection();
                //設(shè)置鏈接超時
                conn.setConnectTimeout(6000);
                //設(shè)置請求方式
                conn.setRequestMethod("GET");
                if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                    //獲得文件的大小
                    length = conn.getContentLength();
                }
                if (length < 0) {
                    return;
                }
                Log.e(TAG, length + "");
                File dir = new File(DOWNLOAD_PATH);
                if (!dir.exists()) {
                    dir.mkdir();
                }
                //在本地創(chuàng)建文件
                File file = new File(dir, mFileInfo.fileName);
                faf = new RandomAccessFile(file, "rwd");
                //設(shè)置文件的長度
                faf.setLength(length);
                mFileInfo.length = length;
                //通過handler發(fā)送消息進行文件的下載
                Message message = mHandler.obtainMessage(MSG_INIT, mFileInfo);
                mHandler.sendMessage(message);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (conn != null) {
                        conn.disconnect();
                    }
                    if (faf != null) {
                        faf.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

這個時候就可以在Handler的handlerMessage()回調(diào)方法中根據(jù)msg.what去實例化一個DownLoadTask對象并調(diào)用downLoad()方法進行下載文件;

Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MSG_INIT:
                    FileInfo fileInfo = (FileInfo) msg.obj;
                    Log.e(TAG, MSG_INIT + "");
                    //啟動下載任務(wù)
                    mTask = new DownLoadTask(DownLoadService.this, fileInfo);
                    mTask.downLoad();
                    break;
            }
        }
    };

當(dāng)然了,在DownLoadTask的構(gòu)造方法中會先去創(chuàng)建數(shù)據(jù)庫實例;

public DownLoadTask(Context context, FileInfo fileInfo) {
        mContext = context;
        mFileInfo = fileInfo;
        //創(chuàng)建數(shù)據(jù)庫實例
        dao = new ThreadDAOImpl(context);
    }

這里就沒有對數(shù)據(jù)庫的創(chuàng)建、增、刪、改、查進行封裝了,如果想封裝可以參考上面的博客,不過實現(xiàn)思路還是差不多的,還是繼承自系統(tǒng)的SQLiteOpenHelper類,在onCreate()方法和onUpgrade()方法中進行數(shù)據(jù)庫的創(chuàng)建和更新;

public class DBHelper extends SQLiteOpenHelper {
    private static volatile DBHelper dbHelper;
    //創(chuàng)建表語法
    private static final String SQL_CREATE = "create table " + DBConstant.TABLE_NAME + "(_id integer primary key autoincrement," +
            DBConstant.THREAD_ID + " integer," + DBConstant.URL + " text," + DBConstant.THREAD_START + " integer," + DBConstant.THREAD_END + " integer," + DBConstant.FINISHED + " integer)";
    //刪除表語法
    private static final String SQL_DROP = "drop table if exists " + DBConstant.TABLE_NAME;

    public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    private DBHelper(Context context) {
        super(context, DBConstant.DB_NAME, null, DBConstant.VERSION);
    }

    public static DBHelper getInstance(Context context) {
        if (dbHelper == null) {
            synchronized (DBHelper.class) {
                if (dbHelper == null) {
                    dbHelper = new DBHelper(context.getApplicationContext());
                }
            }
        }
        return dbHelper;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //先刪除
        db.execSQL(SQL_DROP);
        //再創(chuàng)建
        db.execSQL(SQL_CREATE);
    }
}

在對數(shù)據(jù)庫進行增刪改查操作是還是先定義一個接口,該接口提供了增刪改查的方法,具體的在實現(xiàn)類中進行邏輯處理;

public interface ThreadDAO {
    /**
     * 插入線程信息
     *
     * @param threadInfo
     */
    void insertThread(ThreadInfo threadInfo);

    /**
     * 刪除信息
     *
     * @param url
     * @param threadId
     */
    void deleteThread(String url, int threadId);

    /**
     * 更新信息
     *
     * @param url
     * @param threadId
     */
    void updateThread(String url, int threadId, int finished);

    /**
     * 查詢信息
     *
     * @param url
     * @return
     */
    List<ThreadInfo> queryThread(String url);

    /**
     * 判斷線程信息是否存在
     *
     * @param url
     * @param threadId
     * @return
     */
    boolean isExists(String url, int threadId);
}
public class ThreadDAOImpl implements ThreadDAO {
    private DBHelper dbHelper = null;

    public ThreadDAOImpl(Context context) {
        dbHelper = DBHelper.getInstance(context);
    }

    @Override
    public void insertThread(ThreadInfo threadInfo) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.execSQL("insert into " + DBConstant.TABLE_NAME + "(thread_id,url,thread_start,thread_end,finished) values(?,?,?,?,?)",
                new Object[]{threadInfo.id, threadInfo.url, threadInfo.thread_start, threadInfo.thread_end, threadInfo.finished});
        db.close();
    }

    @Override
    public void deleteThread(String url, int threadId) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.execSQL("delete from " + DBConstant.TABLE_NAME + " where url = ? and thread_id = ?",
                new Object[]{url, threadId});
        db.close();
    }

    @Override
    public void updateThread(String url, int threadId, int finished) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.execSQL("update " + DBConstant.TABLE_NAME + " set finished = ? where url = ? and thread_id = ?",
                new Object[]{finished, url, threadId});
        db.close();
    }

    @Override
    public List<ThreadInfo> queryThread(String url) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from " + DBConstant.TABLE_NAME + " where url = ?", new String[]{url});
        List<ThreadInfo> list = new ArrayList<>();
        while (cursor.moveToNext()) {
            ThreadInfo threadInfo = new ThreadInfo();
            threadInfo.id = cursor.getInt(cursor.getColumnIndex("thread_id"));
            threadInfo.url = cursor.getString(cursor.getColumnIndex("url"));
            threadInfo.thread_start = cursor.getInt(cursor.getColumnIndex("thread_start"));
            threadInfo.thread_end = cursor.getInt(cursor.getColumnIndex("thread_end"));
            threadInfo.finished = cursor.getInt(cursor.getColumnIndex("finished"));
            list.add(threadInfo);
        }
        cursor.close();
        db.close();
        return list;
    }

    @Override
    public boolean isExists(String url, int threadId) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from " + DBConstant.TABLE_NAME + " where url = ? and thread_id = ?", new String[]{url, threadId + ""});
        boolean isExits = cursor.moveToNext();
        cursor.close();
        db.close();
        return isExits;
    }
}

數(shù)據(jù)庫實例創(chuàng)建好后,就先去數(shù)據(jù)庫中查詢是否有下載記錄,如果有就接著上次的記錄下載,如果沒有就創(chuàng)建一個新的下載記錄,再創(chuàng)建一個線程用于文件的下載;

/**
     * 開始文件的下載
     */
    public void downLoad() {
        //讀取數(shù)據(jù)庫的線程信息
        List<ThreadInfo> threadInfos = dao.queryThread(mFileInfo.url);
        ThreadInfo threadInfo = null;
        if (threadInfos.size() == 0) {
            //如果數(shù)據(jù)庫中沒有就根據(jù)下載文件信息創(chuàng)建一個新的下載信息實例對象
            threadInfo = new ThreadInfo();
            threadInfo.id = 0;
            threadInfo.url = mFileInfo.url;
            threadInfo.thread_start = 0;
            threadInfo.thread_end = mFileInfo.length;
            threadInfo.finished = 0;
        } else {
            //獲取數(shù)據(jù)庫返回的下載信息實例對象
            threadInfo = threadInfos.get(0);
        }
        //創(chuàng)建子線程下載
        DownLoadThread downLoadThread = new DownLoadThread(threadInfo);
        downLoadThread.start();
    }

在DownLoadThread線程中就可以進行文件的下載、暫停后下載記錄的更新、利用廣播進行下載進度的傳遞和更新等;

/**
     * 下載線程
     */
    class DownLoadThread extends Thread {
        private ThreadInfo mThreadInfo;

        public DownLoadThread(ThreadInfo threadInfo) {
            mThreadInfo = threadInfo;
        }

        @Override
        public void run() {
            super.run();
            //向數(shù)據(jù)庫中插入一條信息
            if (!dao.isExists(mThreadInfo.url, mThreadInfo.id)) {
                //插入一條新的下載記錄信息
                dao.insertThread(mThreadInfo);
            }
            HttpURLConnection conn = null;
            RandomAccessFile raf = null;
            InputStream input = null;
            try {
                URL url = new URL(mThreadInfo.url);
                conn = (HttpURLConnection) url.openConnection();
                conn.setReadTimeout(6000);
                conn.setRequestMethod("GET");
                //設(shè)置下載位置
                int start = mThreadInfo.thread_start + mThreadInfo.finished;
                conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.thread_end);
                //設(shè)置一個文件寫入位置
                File file = new File(DownLoadService.DOWNLOAD_PATH, mFileInfo.fileName);
                raf = new RandomAccessFile(file, "rwd");
                //設(shè)置文件寫入位置
                raf.seek(start);
                Intent intent = new Intent(DownLoadService.ACTION_UPDATE);
                mFinished += mThreadInfo.finished;
                //開始下載了
                if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                    //讀取數(shù)據(jù)
                    input = conn.getInputStream();
                    byte[] buffer = new byte[1024 * 4];
                    int len = -1;
                    while ((len = input.read(buffer)) != -1) {
                        //寫入文件
                        raf.write(buffer, 0, len);
                        //下載進度發(fā)送廣播給activity
                        mFinished += len;
                        Log.e("DownLoadService",mFinished+"");
                        intent.putExtra("finished", mFinished * 100 / mFileInfo.length);
                        mContext.sendBroadcast(intent);
                        //下載暫停是要把進度保存在數(shù)據(jù)庫中
                        if (isPause) {
                            //暫停時更新數(shù)據(jù)庫中的下載信息
                            dao.updateThread(mThreadInfo.url, mThreadInfo.id, mFinished);
                            return;
                        }
                    }
                    //刪除線程信息
                    dao.deleteThread(mThreadInfo.url, mThreadInfo.id);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (conn != null) {
                        conn.disconnect();
                    }
                    if (raf != null) {
                        raf.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

下載進度通過BroadcastReceiver傳遞后,需要在activity中注冊一個廣播,在BroadcastReceiver的onReceive()方法中進行進度條的更新,需要注意在activity的onDestory方法中將注冊的廣播注銷掉;

//注冊廣播接收器
IntentFilter filter = new IntentFilter();
filter.addAction(DownLoadService.ACTION_UPDATE);
registerReceiver(mBroadcastReceiver, filter);

廣播的注冊是在activity的onCreate方法中注冊的;

/**
     * 更新ui的廣播接收器
     */
    BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction() == DownLoadService.ACTION_UPDATE) {
                int finished = intent.getIntExtra("finished", 0);
                //設(shè)置下載進度
                progressBar.setProgress(finished);
                int progress = progressBar.getProgress();
                if (progress == progressBar.getMax()) {
                    Toast.makeText(MainActivity.this, "下載完畢", Toast.LENGTH_LONG).show();
                }
            }
        }
    };
@Override
    protected void onDestroy() {
        super.onDestroy();
        //注銷廣播
        unregisterReceiver(mBroadcastReceiver);
    }

這樣一個簡單的文件斷點續(xù)傳就實現(xiàn)了。

源碼地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、簡歷準備 1、個人技能 (1)自定義控件、UI設(shè)計、常用動畫特效 自定義控件 ①為什么要自定義控件? Andr...
    lucas777閱讀 5,388評論 2 54
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,650評論 1 32
  • 2.1 Activity 2.1.1 Activity的生命周期全面分析 典型情況下的生命周期:在用戶參與的情況下...
    AndroidMaster閱讀 3,279評論 0 8
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 7,328評論 0 17
  • 本文出自 Eddy Wiki ,轉(zhuǎn)載請注明出處:http://eddy.wiki/interview-androi...
    eddy_wiki閱讀 3,378評論 0 20

友情鏈接更多精彩內(nèi)容