Android 断点续传显示进度的坑

前言:

之前有个需求就是打开APP的时候,判断当前版本是不是最新的,如果不是最新的就后台静默下载,下载完成后弹框让用户选择是否安装。

需要支持断点续传,比如你下载了20%,退出APP进程,然后再次打开的时候从20%的位置继续下载而不是从头开始下载。

代码很简单,我贴出来

dart 复制代码
package com.example.android_apk_install;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;

import androidx.core.content.FileProvider;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class ApkInstallHelper {

    private static final String TAG = "ApkInstallHelper";
    private final static String URL_PATH = "https://xxx/app/apk/";
    private final Context context;
    private OnDownloadListener onDownloadListener;
    private final static String SP_NAME = "DIM_APK_DOWNLOAD";
    private final static String SP_KEY = "DOWNLOAD_BYTES_SO_FAR";
    private final SharedPreferences sharedPreferences;

    public ApkInstallHelper(Context context) {
        this.context = context;
        this.sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
    }

    public void setOnDownloadListener(OnDownloadListener listener) {
        this.onDownloadListener = listener;
    }


    public void downloadByContinue(File apkDestFile, String apkName) {
        new Thread(() -> {
            long startPosition = 0;
            long endPosition = 0;
            String urlStr = URL_PATH + apkName;
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(20_000);
                conn.setRequestMethod("GET");
                //获取下载文件的总大小
                endPosition = conn.getContentLength();

                // 下载的临时文件存在的话,才会断点下载
                File tempFile = new File(apkDestFile, apkName + ".temp");
                if (tempFile.exists()) {
                    startPosition = sharedPreferences.getLong(SP_KEY, 0L);
                }

                DownLoadTask2 downLoadTask2 = new DownLoadTask2(urlStr, apkName, startPosition, endPosition, apkDestFile);
                Thread thread = new Thread(downLoadTask2);
                thread.setName("appDownloadThread");
                thread.start();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }).start();
    }

    class DownLoadTask2 implements Runnable {
        private final String url;
        private final String fileName;
        private final long startPosition;
        private final long endPosition;
        private final File destFile;

        public DownLoadTask2(String url, String fileName, long startPos, long endPos, File destFile) {
            this.url = url;
            this.fileName = fileName;
            this.startPosition = startPos;
            this.endPosition = endPos;
            this.destFile = destFile;
        }

        @Override
        public void run() {
            Log.e("DownLoadTask2", Thread.currentThread().getName() + " , startPosition >>> " + startPosition + " , endPosition = " + endPosition);
            // 下载临时文件名,下载完成后重新命名
            File tempFile = new File(destFile, fileName + ".temp");
            HttpURLConnection conn;
            try {
                Handler mainHandler = new Handler(Looper.getMainLooper());
                long start = System.currentTimeMillis();
                URL url2 = new URL(url);
                conn = (HttpURLConnection) url2.openConnection();
                conn.setConnectTimeout(5_000);
                conn.setRequestMethod("GET");

                //设置当前线程下载的起点,终点
                conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
                //使用java中的RandomAccessFile 对文件进行随机读写操作
                int responseCode = conn.getResponseCode();
                long fileSize = conn.getContentLength();
                if (206 == responseCode) {
                    RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "rw");
                    //设置开始写文件的位置
                    randomAccessFile.seek(startPosition);
                    int len;
                    byte[] buffer = new byte[1024];
                    InputStream is = conn.getInputStream();
                    long total = startPosition;
                    try {
                        while ((len = is.read(buffer)) != -1) {
                            randomAccessFile.write(buffer, 0, len);
//                            Log.e("DownLoadTask2", Thread.currentThread().getName() +
//                                    " , total >>> " + total + ", len = " + len +" , fileSize = " +fileSize +" , endPosition = " + endPosition);
                            total += len;
                            sharedPreferences.edit().putLong(SP_KEY, total).apply();
                            // 为什么这里是除以 endPosition 而不是 fileSize 呢? 因为如果是断点下载,假设文件大小 70M ,第一次下载了20M,APP杀掉进程,再次打开app会断点下载,从20M开始,那么
                            // 此时的 fileSize = 50M 了,那么下载之后进度就会大于 100%
                            String progress = total * 100 / endPosition + "%";
                            Log.e("DownLoadTask2", Thread.currentThread().getName() + " , progress >>> " + progress);
                            if (onDownloadListener != null) {
                                mainHandler.post(() -> onDownloadListener.downloadProgress(progress));
                            }
                        }
                    } catch (Exception exception) {
                        exception.printStackTrace();
                        Log.d(" Error:", exception.getMessage());
                    } finally {
                        is.close();
                        randomAccessFile.close();
                    }
                } else {
                    Log.e(TAG, "--------------下载出错-------------responseCode = " + responseCode + " , url = " + url);
                    new Handler(Looper.getMainLooper()).post(() -> {
                        if (onDownloadListener != null)
                            onDownloadListener.downloadSuccess(false);
                    });
                    return;
                }
                long time = System.currentTimeMillis() - start;
                Log.e("DownLoadTask2", Thread.currentThread().getName() + " time = " + time);

                Log.e(TAG, "--------------下载完成-------------");
                // 下载成功后,重新命名为正式名称,以此判断是否下载完成,
                boolean res = tempFile.renameTo(new File(destFile, fileName));
                if (!res) {
                    Log.e(TAG, tempFile.getAbsolutePath() + " , 重命名失败");
                    new Handler(Looper.getMainLooper()).post(() -> {
                        if (onDownloadListener != null)
                            onDownloadListener.downloadSuccess(false);
                    });
                    return;
                }
                // 下载完成后删除sp中的下载大小
                sharedPreferences.edit().clear().apply();
                new Handler(Looper.getMainLooper()).post(() -> {
                    if (onDownloadListener != null)
                        onDownloadListener.downloadSuccess(true);
                });
            } catch (IOException e) {
                Log.d(" Error:", e.getMessage());
            }
        }
    }

    


    public interface OnDownloadListener {
        void downloadSuccess(boolean success);
        void downloadProgress(String progress);
    }
}

我出错误的地方在哪里呢?就是在计算下载进度的时候,开始写的是 String progress = total * 100 / fileSize+ "%"; 结果发现,下载部分后,退出 app,再次打开 app 的时候 进度超过 100%了,简直不忍直视啊,幸亏我自测严谨啊,哈哈,然后debug 了一下,发现 fileSize 不是文件的原始大小了,而是减去已下载的部分了,找到问题原因就好解决了,代码改成如下方式:

dart 复制代码
String progress = total * 100 / endPosition + "%";

endPosition 表示的是下载文件的原始大小。

最终,问题圆满解决。

当你在使用 HttpURLConnection 设置请求头 "Range" 来请求文件的部分内容时,httpURLConnection.getContentLength() 返回的将是从 startPositionendPosition 之间的字节大小 ,而不是整个文件的大小

我这里使用的就是 : conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);

详细说明:

  • Range 请求:Range 请求头允许你指定文件中你想获取的字节范围。例如,"Range": "bytes=500-999" 将请求从文件中第500到999字节的内容。
  • Content-Length 返回值:当你设置了 Range 请求头后,HttpURLConnection 的 getContentLength() 方法会返回该请求范围内的数据大小,也就是从 startPosition 到 endPosition 的字节数,而不是整个文件的大小。

举个例子:

如果文件大小为1000字节,你设置了 "Range": "bytes=500-999",那么 getContentLength() 将返回500(即999-500+1)

注意:

  1. 如果没有设置 Range 请求头,则 getContentLength() 返回的是整个文件的大小。
  2. 如果你想获取整个文件的大小,可以考虑在不设置 Range 请求头的情况下,单独发起一个请求获取文件大小,然后再进行分块下载。
    通过使用这个方法,你可以有效地处理大文件的下载或者断点续传。
  3. conn = (HttpURLConnection) url2.openConnection() 一次 connection 只能调用一次 conn.getContentLength(),否则会抛出异常。
相关推荐
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck1 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei1 小时前
java的类加载机制的学习
java·学习
Yaml43 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~3 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616883 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7893 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java4 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~4 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
程序媛小果4 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot