com.vincestyling.netroid.request.FileDownloadRequest.java Source code

Java tutorial

Introduction

Here is the source code for com.vincestyling.netroid.request.FileDownloadRequest.java

Source

/*
 * Copyright (C) 2015 Vince Styling
 *
 * 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.vincestyling.netroid.request;

import android.text.TextUtils;
import com.vincestyling.netroid.*;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;

/**
 * Its purpose is provide a big file download impmenetation, suport continuous transmission
 * on the breakpoint download if server-side enable 'Content-Range' Header.
 * for example:
 * execute a request and submit header like this : Range=bytes=1000- (1000 means the begin point of the file).
 * response return a header like this Content-Range=bytes 1000-1895834/1895835, that's continuous transmission,
 * also return Accept-Ranges=bytes tell us the server-side supported range transmission.
 * <p/>
 * This request will stay longer in the thread which dependent your download file size,
 * that will fill up your thread poll as soon as possible if you launch many request,
 * if all threads is busy, the high priority request such as {@link StringRequest}
 * might waiting long time, so don't use it alone.
 * we highly recommend you to use it with the {@link com.vincestyling.netroid.toolbox.FileDownloader},
 * FileDownloader maintain a download task queue, let's set the maximum parallel request count, the rest will await.
 * <p/>
 * By the way, this request priority was {@link Priority#LOW}, higher request will jump ahead it.
 */
public class FileDownloadRequest extends Request<Void> {
    private File mStoreFile;
    private File mTemporaryFile;

    public FileDownloadRequest(String storeFilePath, String url) {
        this(new File(storeFilePath), url);
    }

    public FileDownloadRequest(File storeFile, String url) {
        super(url, null);
        mStoreFile = storeFile;
        mTemporaryFile = new File(storeFile + ".tmp");

        // Turn the retries frequency greater.
        setRetryPolicy(new DefaultRetryPolicy(DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 200,
                DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
    }

    /**
     * Init or reset the Range header, ensure the begin position always be the temporary file size.
     */
    @Override
    public void prepare() {
        // Note: if the request header "Range" greater than the actual length that server-size have,
        // the response header "Content-Range" will return "bytes */[actual length]", that's wrong.
        addHeader("Range", "bytes=" + mTemporaryFile.length() + "-");

        //      Suppress the HttpStack accept gzip encoding, avoid the progress calculate wrong problem.
        //      addHeader("Accept-Encoding", "identity");
    }

    /**
     * Ignore the response content, just rename the TemporaryFile to StoreFile.
     */
    @Override
    protected Response<Void> parseNetworkResponse(NetworkResponse response) {
        if (!isCanceled()) {
            if (mTemporaryFile.canRead() && mTemporaryFile.length() > 0) {
                if (mTemporaryFile.renameTo(mStoreFile)) {
                    return Response.success(null, response);
                } else {
                    return Response.error(new NetroidError("Can't rename the download temporary file!"));
                }
            } else {
                return Response.error(new NetroidError("Download temporary file was invalid!"));
            }
        }
        return Response.error(new NetroidError("Request was Canceled!"));
    }

    /**
     * In this method, we got the Content-Length, with the TemporaryFile length,
     * we can calculate the actually size of the whole file, if TemporaryFile not exists,
     * we'll take the store file length then compare to actually size, and if equals,
     * we consider this download was already done.
     * We used {@link RandomAccessFile} to continue download, when download success,
     * the TemporaryFile will be rename to StoreFile.
     */
    @Override
    public byte[] handleResponse(HttpResponse response, Delivery delivery) throws IOException, ServerError {
        // Content-Length might be negative when use HttpURLConnection because it default header Accept-Encoding is gzip,
        // we can force set the Accept-Encoding as identity in prepare() method to slove this problem but also disable gzip response.
        HttpEntity entity = response.getEntity();
        long fileSize = entity.getContentLength();
        if (fileSize <= 0) {
            NetroidLog.d("Response doesn't present Content-Length!");
        }

        long downloadedSize = mTemporaryFile.length();
        boolean isSupportRange = HttpUtils.isSupportRange(response);
        if (isSupportRange) {
            fileSize += downloadedSize;

            // Verify the Content-Range Header, to ensure temporary file is part of the whole file.
            // Sometime, temporary file length add response content-length might greater than actual file length,
            // in this situation, we consider the temporary file is invalid, then throw an exception.
            String realRangeValue = HttpUtils.getHeader(response, "Content-Range");
            // response Content-Range may be null when "Range=bytes=0-"
            if (!TextUtils.isEmpty(realRangeValue)) {
                String assumeRangeValue = "bytes " + downloadedSize + "-" + (fileSize - 1);
                if (TextUtils.indexOf(realRangeValue, assumeRangeValue) == -1) {
                    throw new IllegalStateException("The Content-Range Header is invalid Assume[" + assumeRangeValue
                            + "] vs Real[" + realRangeValue + "], " + "please remove the temporary file ["
                            + mTemporaryFile + "].");
                }
            }
        }

        // Compare the store file size(after download successes have) to server-side Content-Length.
        // temporary file will rename to store file after download success, so we compare the
        // Content-Length to ensure this request already download or not.
        if (fileSize > 0 && mStoreFile.length() == fileSize) {
            // Rename the store file to temporary file, mock the download success. ^_^
            mStoreFile.renameTo(mTemporaryFile);

            // Deliver download progress.
            delivery.postDownloadProgress(this, fileSize, fileSize);

            return null;
        }

        RandomAccessFile tmpFileRaf = new RandomAccessFile(mTemporaryFile, "rw");

        // If server-side support range download, we seek to last point of the temporary file.
        if (isSupportRange) {
            tmpFileRaf.seek(downloadedSize);
        } else {
            // If not, truncate the temporary file then start download from beginning.
            tmpFileRaf.setLength(0);
            downloadedSize = 0;
        }

        InputStream in = null;
        try {
            in = entity.getContent();
            // Determine the response gzip encoding, support for HttpClientStack download.
            if (HttpUtils.isGzipContent(response) && !(in instanceof GZIPInputStream)) {
                in = new GZIPInputStream(in);
            }
            byte[] buffer = new byte[6 * 1024]; // 6K buffer
            int offset;

            while ((offset = in.read(buffer)) != -1) {
                tmpFileRaf.write(buffer, 0, offset);

                downloadedSize += offset;
                delivery.postDownloadProgress(this, fileSize, downloadedSize);

                if (isCanceled()) {
                    delivery.postCancel(this);
                    break;
                }
            }
        } finally {
            try {
                // Close the InputStream
                if (in != null)
                    in.close();
            } catch (Exception e) {
                NetroidLog.v("Error occured when calling InputStream.close");
            }

            try {
                // release the resources by "consuming the content".
                entity.consumeContent();
            } catch (Exception e) {
                // This can happen if there was an exception above that left the entity in
                // an invalid state.
                NetroidLog.v("Error occured when calling consumingContent");
            }
            tmpFileRaf.close();
        }

        return null;
    }

    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

    /**
     * Never use cache in this case.
     */
    @Override
    public void setCacheExpireTime(TimeUnit timeUnit, int amount) {
    }
}