App Network Framework Warfare III: Download Progress and Breakpoint Resume

Posted by clartsonly on Sun, 19 May 2019 20:20:20 +0200

App Network Request Actual Warfare III: Download Progress and Breakpoint Renewal

Boss, salvage.

Original formula, no illustration diao

Needs analysis and problem solving steps

It appears that the rxjava and retrofit combination cannot return synchronously as follows:

Response<BaseResponse<ResEntity1.DataBean>> tokenRes = call.execute();

(I really don't know how to do this with rxjava, the woody guys know it.)

Asynchronous callbacks do not provide both progress and pause or cancel downloads.So you have to think of another way.Provides a code downloaded by okhttpi (quoted from the first line of code by Guo Lin):

try
{
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
            .addHeader("RANGE", "bytes=" + downloadedLength + "-")
            .url(downloadUrl)
            .build();
    Response response = client.newCall(request).execute();
    if (response != null)
    {
        Log.e("DownLoadTask", "(DownLoadTask.java:78)" + response.body().contentLength());
        is = response.body().byteStream();
        savedFile = new RandomAccessFile(file, "rw");
        savedFile.seek(downloadedLength);
        byte[] bytes = new byte[1024];
        int total = 0;
        int len;
        while ((len = is.read(bytes)) != -1)
        {
            if (isCanceled)
            {
                return TYPE_CANCELED;
            } else if (isPaused)
            {
                return TYPE_PAUSE;
            } else
            {
                total += len;
                savedFile.write(bytes, 0, len);
                int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                publishProgress(progress);
            }
        }
    }
    response.body().close();
    return TYPE_SUCCESS;
} catch (IOException e)
{
    e.printStackTrace();
} finally
{
    try
    {
        if (is != null)
        {
            is.close();
        }
        if (savedFile != null)
        {
            savedFile.close();
        }
        if (isCanceled && file != null)
        {
            file.delete();
        }
    } catch (IOException e)
    {
        e.printStackTrace();
    }
}

In fact, there are two issues involved here:

  • Download Progress
  • Breakpoint Continuation

Download Progress

The key is to customize the ResponseBody, notice the source method inside, where we can face the source in advance and feel like the idea of the okhttp interceptor.

First, define a download interface:

/**
 * <pre>
 *     Author: Xiao Kun
 *     Time: 2018/04/20
 *     Description:
 *     Version: 1.0
 * </pre>
 */
public interface DownLoadListener
{
    void onProgress(int progress, boolean downSuc, boolean downFailed);
}

The three parameters are the download progress, whether the download is completed, whether the download failed.

Next, look at the custom ReponseBody code as follows:

@Override
public BufferedSource source()
{
    if (bufferedSource == null)
    {
        bufferedSource = Okio.buffer(source(mResponseBody.source()));
    }
    return bufferedSource;
}

private Source source(Source source)
{
    return new ForwardingSource(source)
    {
        @Override
        public long read(Buffer sink, long byteCount) throws IOException
        {
            long bytesRead = super.read(sink, byteCount);
            totalBytesRead += bytesRead != -1 ? bytesRead : 0;
            if (bytesRead == -1)
            {
                //Download complete
                sHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        mListener.onProgress(100, true, false);
                    }
                });
            } else
            {
                //Downloading, update progress
                sHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        int progress = (int) (totalBytesRead * 100 / contentLength);
                        mListener.onProgress(progress, false, false);
                    }
                });
            }
            saveToFile(sink);
            return bytesRead;
        }
    };
}

//write file
private void saveToFile(Buffer buffer)
    {
        InputStream inputStream = buffer.inputStream();
        RandomAccessFile saveFile = null;
        try
        {
            saveFile = new RandomAccessFile(file, "rw");
            saveFile.seek(getDownloadedLength());
            byte[] bytes = new byte[1024];
            int len;
            while ((len = inputStream.read(bytes)) != -1)
            {
                saveFile.write(bytes, 0, len);
            }
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        } finally
        {
            try
            {
                if (inputStream != null)
                {
                    inputStream.close();
                }
                if (saveFile != null)
                {
                    saveFile.close();
                }
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

It's that ForwardingSource class, and it really feels like an interceptor in okhttp, that's to intercept that source and then do something undescribable, hey hey.RandomAccessFile is used here for subsequent breakpoint renewal functionality.It is also a constructor that can use two parameters of FileOutputStream, as follows

//if <code>true</code>, then bytes will be written to the end of the file rather than the //beginning
public FileOutputStream(String name, boolean append)
    throws FileNotFoundException
{
    this(name != null ? new File(name) : null, append);
}

The second argument, true, is also writable.Overall, download progress is relatively simple.

Breakpoint Continuation

In fact, the key is that Made words are not clear.Look at the picture:

There is one attribute in the Http request header as follows:

request = request.newBuilder()
        .header("RANGE", "bytes=" + downloadedLength + "-")
        .build();

The purpose of this property is to change the request domain, for example, if I want to start downloading at the 500th byte.So how do you dynamically modify it, yes, or use the Interceptor interceptor:

//Download File Interceptor
static Interceptor downloadInterceptor = new Interceptor()
{
    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Request request = chain.request();
        //Record the length of the written file
        long downloadedLength = DownloadManager.dSp.getLong(downloadEntity.getFileName(), 0);

        Response proceed = chain.proceed(request);
        //Total length of saved source file (critical)
        DownloadManager.dSp.edit().putLong(downloadEntity.getFileName() + "content_length", proceed.body().contentLength()).commit();
        request = request.newBuilder()
                .header("RANGE", "bytes=" + downloadedLength + "-")
                .build();
        Response response = chain.proceed(request);
        response = response.newBuilder()
                .body(new ProgressResponseBody(response.body(), downloadEntity))
                .build();
        return response;
    }
};

All progress = (length of written file + length of downloading) / total length of source file, I will not paste the code in the project.Another important thing to note is that I suspend and cancel downloads, using Disposable in rxjava, and I wrote a download management class specifically, as follows:

/**
 * Created by Xiao Kun on 2018/4/22.
 *
 * @author Xiao Kun
 * @date 2018/4/22
 */

public class DownloadManager
{
    public static SharedPreferences dSp;

    /**
     * Initialize DownloadManager
     *
     * @param context
     */
    public static void initDownManager(Context context)
    {
        dSp = context.getSharedPreferences("download_file", Context.MODE_PRIVATE);
    }

    /**
     * Pause Download
     *
     * @param disposable Controlling the switch on rxjava
     * @param fileName   Downloaded file name, must contain suffix
     */
    public static void pauseDownload(Disposable disposable, String fileName)
    {
        if (disposable == null || TextUtils.isEmpty(fileName))
        {
            return;
        }
        if (!disposable.isDisposed())
        {
            disposable.dispose();
        }
        if (dSp == null)
        {
            throw new NullPointerException("Must be initialized first DownloadManager");
        }
        File file = initFile(fileName);
        if (file.exists() && dSp != null)
        {
            dSp.edit().putLong(file.getName(), file.length()).commit();
        }
    }

    /**
     * Cancel Download
     *
     * @param disposable Controlling the switch on rxjava
     * @param fileName   Downloaded file name, must contain suffix
     */
    public static void cancelDownload(Disposable disposable, String fileName)
    {
        if (disposable == null || TextUtils.isEmpty(fileName))
        {
            return;
        }
        if (!disposable.isDisposed())
        {
            disposable.dispose();
        }
        File file = initFile(fileName);
        if (file.exists() && dSp != null)
        {
            dSp.edit().putLong(file.getName(), 0).commit();
        }
        //Cancel download, last step remember to delete downloaded files
        if (file.exists())
        {
            file.delete();
        }
    }

    public static File initFile(String fileName)
    {
        String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
        File file = new File(directory + File.separator + fileName);
        return file;
    }
}

Note that I deleted the written file when I canceled the download. No problem, old fellow.Whole, clear and white, Deyun Society has a brand!
Finally, it's fairly easy for us to download in the project, as the code says:

//Test downloaded files
    private void downloadFile()
    {
        fileName = "httpTest.apk";
        downloadEntity = new DownloadEntity(loadListener, fileName);
        ApiService apiService = RetrofitHelper.createService(ApiService.class,
                RetrofitHelper.getDownloadRetrofit(downloadEntity));

        Observable<ResponseBody> observable = apiService.downLoadFile(url)
                .subscribeOn(Schedulers.io());
        observable.subscribe(new DownLoadObserver()
        {
            @Override
            public void onSubscribe(Disposable d)
            {
                disposable = d;
            }
        });
    }

Then pause:

//Pause Download
DownloadManager.pauseDownload(disposable, fileName);

Then cancel the download:

//Cancel Download
DownloadManager.cancelDownload(disposable, fileName);

github link: Demo

Above.

Topics: OkHttp network Retrofit Java