前言:
之前有个需求就是打开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()
返回的将是从 startPosition 到 endPosition 之间的字节大小 ,而不是整个文件的大小。
我这里使用的就是 : 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)。
注意:
- 如果没有设置 Range 请求头,则 getContentLength() 返回的是整个文件的大小。
- 如果你想获取整个文件的大小,可以考虑在不设置 Range 请求头的情况下,单独发起一个请求获取文件大小,然后再进行分块下载。
通过使用这个方法,你可以有效地处理大文件的下载或者断点续传。 conn = (HttpURLConnection) url2.openConnection()
一次 connection 只能调用一次conn.getContentLength()
,否则会抛出异常。