com.liulishuo.filedownloader.services.FileDownloadRunnable.java Source code

Java tutorial

Introduction

Here is the source code for com.liulishuo.filedownloader.services.FileDownloadRunnable.java

Source

/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader.services;

import android.os.Process;
import android.text.TextUtils;

import com.liulishuo.filedownloader.event.DownloadTransferEvent;
import com.liulishuo.filedownloader.model.FileDownloadModel;
import com.liulishuo.filedownloader.model.FileDownloadStatus;
import com.liulishuo.filedownloader.model.FileDownloadTransferModel;
import com.liulishuo.filedownloader.util.FileDownloadLog;
import com.liulishuo.filedownloader.util.FileDownloadUtils;
import com.squareup.okhttp.CacheControl;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.SocketTimeoutException;

/**
 * Created by Jacksgong on 9/24/15.
 */
class FileDownloadRunnable implements Runnable {

    private static final int BUFFER_SIZE = 1024 * 4;
    private final FileDownloadTransferModel downloadTransfer;

    private final String url;
    private final String path;

    private final IFileDownloadDBHelper helper;

    private long maxNotifyBytes;

    private int maxNotifyCounts = 0;

    //tmp
    private boolean isContinueDownloadAvailable;

    // etag
    private String etag;

    private FileDownloadModel downloadModel;

    public int getId() {
        return downloadModel.getId();
    }

    private volatile boolean isRunning = false;
    private volatile boolean isPending = false;

    private final OkHttpClient client;
    private final int autoRetryTimes;

    public FileDownloadRunnable(final OkHttpClient client, final FileDownloadModel model,
            final IFileDownloadDBHelper helper, final int autoRetryTimes) {
        isPending = true;
        isRunning = false;

        this.client = client;
        this.helper = helper;

        this.url = model.getUrl();
        this.path = model.getPath();

        downloadTransfer = new FileDownloadTransferModel();

        downloadTransfer.setDownloadId(model.getId());
        downloadTransfer.setStatus(model.getStatus());
        downloadTransfer.setSoFarBytes(model.getSoFar());
        downloadTransfer.setTotalBytes(model.getTotal());

        maxNotifyCounts = model.getCallbackProgressTimes();
        maxNotifyCounts = maxNotifyCounts <= 0 ? 0 : maxNotifyCounts;

        this.isContinueDownloadAvailable = false;

        this.etag = model.getETag();
        this.downloadModel = model;

        this.autoRetryTimes = autoRetryTimes;
    }

    public boolean isExist() {
        return isPending || isRunning;
    }

    @Override
    public void run() {
        isPending = false;
        isRunning = true;
        int retryingTimes = 0;
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        FileDownloadModel model = this.downloadModel;

        if (model == null) {
            FileDownloadLog.e(this, "start runnable but model == null?? %s", getId());

            this.downloadModel = helper.find(getId());

            if (this.downloadModel == null) {
                FileDownloadLog.e(this, "start runnable but downloadMode == null?? %s", getId());
                return;
            }

            model = this.downloadModel;
        }

        if (model.getStatus() != FileDownloadStatus.pending) {
            FileDownloadLog.e(this, "start runnable but status err %s", model.getStatus());
            // ??urlpath(????) ??
            onError(new RuntimeException(String.format("start runnable but status err %s", model.getStatus())));

            return;
        }

        // 
        do {

            long soFar = 0;
            try {

                if (model.isCanceled()) {
                    FileDownloadLog.d(this, "already canceled %d %d", model.getId(), model.getStatus());
                    break;
                }

                FileDownloadLog.d(FileDownloadRunnable.class, "start download %s %s", getId(), model.getUrl());

                checkIsContinueAvailable();

                Request.Builder headerBuilder = new Request.Builder().url(url);
                addHeader(headerBuilder);
                headerBuilder.tag(this.getId());
                // ?cache?REST?
                headerBuilder.cacheControl(CacheControl.FORCE_NETWORK);

                Call call = client.newCall(headerBuilder.get().build());

                Response response = call.execute();

                final boolean isSucceedStart = response.code() == 200;
                final boolean isSucceedContinue = response.code() == 206 && isContinueDownloadAvailable;

                if (isSucceedStart || isSucceedContinue) {
                    long total = downloadTransfer.getTotalBytes();
                    if (isSucceedStart || total == 0) {
                        total = response.body().contentLength();
                    }

                    if (isSucceedContinue) {
                        soFar = downloadTransfer.getSoFarBytes();
                        FileDownloadLog.d(this, "add range %d %d", downloadTransfer.getSoFarBytes(),
                                downloadTransfer.getTotalBytes());
                    }

                    InputStream inputStream = null;
                    RandomAccessFile accessFile = getRandomAccessFile(isSucceedContinue);
                    try {
                        inputStream = response.body().byteStream();
                        byte[] buff = new byte[BUFFER_SIZE];
                        maxNotifyBytes = maxNotifyCounts <= 0 ? -1 : total / maxNotifyCounts;

                        updateHeader(response);
                        onConnected(isSucceedContinue, soFar, total);

                        do {
                            int byteCount = inputStream.read(buff);
                            if (byteCount == -1) {
                                break;
                            }

                            accessFile.write(buff, 0, byteCount);

                            //write buff
                            soFar += byteCount;
                            if (accessFile.length() < soFar) {
                                // ??
                                throw new RuntimeException(
                                        String.format("file be changed by others when downloading %d %d",
                                                accessFile.length(), soFar));
                            } else {
                                onProcess(soFar, total);
                            }

                            if (isCancelled()) {
                                onPause();
                                return;
                            }

                        } while (true);

                        if (soFar == total) {
                            onComplete(total);

                            // ?
                            break;
                        } else {
                            throw new RuntimeException(
                                    String.format("sofar[%d] not equal total[%d]", soFar, total));
                        }
                    } finally {
                        if (inputStream != null) {
                            inputStream.close();
                        }

                        if (accessFile != null) {
                            accessFile.close();
                        }
                    }

                } else {
                    throw new RuntimeException(String.format("response code error: %d", response.code()));
                }

            } catch (Throwable ex) {
                // TODO ???????
                if (autoRetryTimes > retryingTimes++) {
                    // retry
                    onRetry(ex, retryingTimes, soFar);
                    continue;
                } else {
                    // error
                    onError(ex);
                    break;
                }
            } finally {
                isRunning = false;
            }

        } while (true);

    }

    private void addHeader(Request.Builder builder) {
        if (isContinueDownloadAvailable) {
            builder.addHeader("If-Match", this.etag);
            builder.addHeader("Range", String.format("bytes=%d-", downloadTransfer.getSoFarBytes()));
        }
    }

    private void updateHeader(Response response) {
        if (response == null) {
            throw new RuntimeException("response is null when updateHeader");
        }

        boolean needRefresh = false;
        final String oldEtag = this.etag;
        final String newEtag = response.header("Etag");

        FileDownloadLog.w(this, "etag find by header %s", newEtag);

        if (oldEtag == null && newEtag != null) {
            needRefresh = true;
        } else if (oldEtag != null && newEtag != null && !oldEtag.equals(newEtag)) {
            needRefresh = true;
        }

        if (needRefresh) {
            this.etag = newEtag;
            helper.updateHeader(downloadTransfer.getDownloadId(), newEtag);
        }

    }

    private final DownloadTransferEvent event = new DownloadTransferEvent(null);

    private void onConnected(final boolean isContinue, final long soFar, final long total) {
        downloadTransfer.setSoFarBytes(soFar);
        downloadTransfer.setTotalBytes(total);
        downloadTransfer.setEtag(this.etag);
        downloadTransfer.setIsContinue(isContinue);
        downloadTransfer.setStatus(FileDownloadStatus.connected);

        helper.update(downloadTransfer.getDownloadId(), FileDownloadStatus.connected, soFar, total);

        FileDownloadProcessEventPool.getImpl().asyncPublishInNewThread(event.setTransfer(downloadTransfer.copy()));
    }

    private long lastNotifiedSoFar = 0;

    private void onProcess(final long soFar, final long total) {
        if (soFar != total) {
            downloadTransfer.setSoFarBytes(soFar);
            downloadTransfer.setTotalBytes(total);
            downloadTransfer.setStatus(FileDownloadStatus.progress);

            helper.update(downloadTransfer.getDownloadId(), FileDownloadStatus.progress, soFar, total);
        }

        if (maxNotifyBytes < 0 || soFar - lastNotifiedSoFar < maxNotifyBytes) {
            return;
        }

        lastNotifiedSoFar = soFar;
        FileDownloadLog.d(this, "On progress %d %d %d", downloadTransfer.getDownloadId(), soFar, total);

        FileDownloadProcessEventPool.getImpl().asyncPublishInNewThread(event.setTransfer(downloadTransfer));

    }

    private void onRetry(Throwable ex, final int retryTimes, final long soFarBytes) {
        FileDownloadLog.e(this, ex, "On retry %d %s %d %d", downloadTransfer.getDownloadId(), ex.getMessage(),
                retryTimes, autoRetryTimes);

        ex = exFiltrate(ex);
        downloadTransfer.setStatus(FileDownloadStatus.retry);
        downloadTransfer.setThrowable(ex);
        downloadTransfer.setRetryingTimes(retryTimes);
        downloadTransfer.setSoFarBytes(soFarBytes);
        // TODO ???

        helper.updateRetry(downloadTransfer.getDownloadId(), ex.getMessage(), retryTimes);

        FileDownloadProcessEventPool.getImpl()
                .asyncPublishInNewThread(new DownloadTransferEvent(downloadTransfer.copy()// because we must make sure retry status no change by downloadTransfer reference
        ));
    }

    private void onError(Throwable ex) {
        FileDownloadLog.e(this, ex, "On error %d %s", downloadTransfer.getDownloadId(), ex.getMessage());

        ex = exFiltrate(ex);
        downloadTransfer.setStatus(FileDownloadStatus.error);
        downloadTransfer.setThrowable(ex);

        helper.updateError(downloadTransfer.getDownloadId(), ex.getMessage());

        FileDownloadProcessEventPool.getImpl().asyncPublishInNewThread(event.setTransfer(downloadTransfer));
    }

    private void onComplete(final long total) {
        FileDownloadLog.d(this, "On completed %d %d", downloadTransfer.getDownloadId(), total);
        downloadTransfer.setStatus(FileDownloadStatus.completed);

        helper.updateComplete(downloadTransfer.getDownloadId(), total);

        FileDownloadProcessEventPool.getImpl().asyncPublishInNewThread(event.setTransfer(downloadTransfer));
    }

    private void onPause() {
        this.isRunning = false;
        FileDownloadLog.d(this, "On paused %d %d %d", downloadTransfer.getDownloadId(),
                downloadTransfer.getSoFarBytes(), downloadTransfer.getTotalBytes());
        downloadTransfer.setStatus(FileDownloadStatus.paused);

        helper.updatePause(downloadTransfer.getDownloadId());

        // ???pause???
        //        FileEventPool.getImpl().asyncPublishInNewThread(new FileDownloadTransferEvent(downloadTransfer));
    }

    public void onResume() {
        FileDownloadLog.d(this, "On resume %d", downloadTransfer.getDownloadId());
        downloadTransfer.setStatus(FileDownloadStatus.pending);

        this.isPending = true;

        helper.updatePending(downloadTransfer.getDownloadId());

        FileDownloadProcessEventPool.getImpl().asyncPublishInNewThread(event.setTransfer(downloadTransfer));
    }

    private boolean isCancelled() {
        return this.downloadModel.isCanceled();
    }

    // ----------------------------------
    private RandomAccessFile getRandomAccessFile(final boolean append) throws Throwable {
        if (TextUtils.isEmpty(path)) {
            throw new RuntimeException("found invalid internal destination path, empty");
        }

        if (!FileDownloadUtils.isFilenameValid(path)) {
            throw new RuntimeException(String.format("found invalid internal destination filename %s", path));
        }

        File file = new File(path);

        if (file.exists() && file.isDirectory()) {
            throw new RuntimeException(
                    String.format("found invalid internal destination path[%s], & path is directory[%B]", path,
                            file.isDirectory()));
        }
        if (!file.exists()) {
            if (!file.createNewFile()) {
                throw new IOException(String.format("create new file error  %s", file.getAbsolutePath()));
            }
        }

        RandomAccessFile outFd = new RandomAccessFile(file, "rw");
        if (append) {
            outFd.seek(downloadTransfer.getSoFarBytes());
        }
        return outFd;
        //        return new FileOutputStream(file, append);
    }

    private void checkIsContinueAvailable() {
        File file = new File(path);
        if (file.exists()) {
            final long fileLength = file.length();
            if (fileLength >= downloadTransfer.getSoFarBytes() && this.etag != null
                    && fileLength < downloadTransfer.getTotalBytes()) {
                // fileLength >= total bytes ??
                FileDownloadLog.d(this, "adjust sofar old[%d] new[%d]", downloadTransfer.getSoFarBytes(),
                        fileLength);

                this.isContinueDownloadAvailable = true;
            } else {
                final boolean result = file.delete();
                FileDownloadLog.d(this, "delete file for dirty file %B, fileLength[%d], sofar[%d] total[%d] etag",
                        result, fileLength, downloadTransfer.getSoFarBytes(), downloadTransfer.getTotalBytes());
            }
        }
    }

    private Throwable exFiltrate(Throwable ex) {
        if (TextUtils.isEmpty(ex.getMessage())) {
            if (ex instanceof SocketTimeoutException) {
                ex = new RuntimeException(ex.getClass().getSimpleName(), ex);
            }
        }

        return ex;
    }
}