相信大家在项目开发的过程中会用到下载相关的操作,下面是我在工作中用到的下载逻辑管理类,支持下载和取消下载,进度监听功能,能满足大多数场景需求,希望对大家有帮助
java
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 描述: 下载管理器
* 创建者: IT 乐手
* 日期: 2025/3/27
*/
public class DownloadManager {
private static final String TAG = "DownloadManager";
private DownloadManager() {
}
private static class SingletonHolder {
private static final DownloadManager INSTANCE = new DownloadManager();
}
public static DownloadManager getInstance() {
return SingletonHolder.INSTANCE;
}
public MutableLiveData<String> unzipModelLiveData = new MutableLiveData<>();
// 添加当前任务跟踪
private final Map<String, DownloadTask> currentTasks = new ConcurrentHashMap<>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
public DownloadTask downloadFile(String url, String version, String key, DownloadCallback callback) throws Exception {
// 检查是否已有相同任务正在执行
String taskKey = generateTaskKey(url, key);
if (currentTasks.containsKey(taskKey)) {
DownloadTask existingTask = currentTasks.get(taskKey);
if (existingTask != null && !existingTask.isCanceled) {
AtotoLogger.e(TAG, "downloadFile: task for " + key + " is already in progress" );
return existingTask;
}
}
File cacheDir = new File(AppGlobalUtils.getApplication().getExternalFilesDir(null), "model");
if (!cacheDir.exists()) {
cacheDir.mkdir();
}
String modelDirName = VoskCachedManager.getInstance().getModelByLang(key);
if (modelDirName == null || modelDirName.isEmpty()) {
throw new Exception(key + " is not supported");
}
File destination = new File(cacheDir, modelDirName + ".zip");
AtotoLogger.d(TAG, "downloadFile: " + url + " to " + cacheDir.getAbsolutePath() + ", version: " + version + ", destination: " + destination.getAbsolutePath());
DownloadTask task = downloadFile(url,version, destination, callback);
currentTasks.put(taskKey, task);
return task;
}
private static @NonNull Map<String, String> buildHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", AccountUseCase.getToken());
headers.put("deviceSeries", DeviceUtil.getPlatform());
return headers;
}
public DownloadTask downloadFile(String url, String version, File destination, DownloadCallback callback) {
DownloadTask task = new DownloadTask(url, destination);
OkHttpClient client = new OkHttpClient();
Request.Builder requestBuilder = new Request.Builder().url(task.url);
buildRequestHeader(requestBuilder, buildHeaders());
Call call = client.newCall(requestBuilder.build());
task.setCall(call);
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
AtotoLogger.e(TAG, "downloadFile error: " + e);
removeTask(task);
mHandler.post(() -> {
if (task.isCanceled) {
if (callback != null) {
callback.onCancel(task.url);
}
} else {
if (callback != null) {
callback.onError(task.url, e.getMessage());
}
}
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
// 在开始处理响应前检查取消状态
if (task.isCanceled) {
removeTask(task);
response.close();
AtotoLogger.d(TAG, "downloadFile canceled (before processing): " + task.url);
mHandler.post(() -> {
if (callback != null) {
callback.onCancel(task.url);
}
});
return;
}
if (!response.isSuccessful()) {
removeTask(task);
AtotoLogger.e(TAG, "downloadFile error: " + response.message());
mHandler.post(() -> {
if (callback != null) {
callback.onError(task.url, "Failed to download file: " + response.message());
}
});
return;
}
// 已存在文件,直接删除
if (destination.exists()) {
destination.delete();
}
InputStream inputStream = null;
FileOutputStream outputStream = null;
boolean downloadSuccess = false;
boolean shouldUnzip = false;
try {
assert response.body() != null;
inputStream = response.body().byteStream();
outputStream = new FileOutputStream(destination);
byte[] buffer = new byte[2048];
long totalBytesRead = 0;
long fileSize = response.body().contentLength();
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1 && !task.isCanceled) {
totalBytesRead += bytesRead;
outputStream.write(buffer, 0, bytesRead);
long finalTotalBytesRead = totalBytesRead;
mHandler.post(() -> {
if (callback != null) {
callback.onProgress(task.url, finalTotalBytesRead, fileSize);
}
});
}
if (task.isCanceled) {
AtotoLogger.d(TAG, "downloadFile canceled: " + task.url);
mHandler.post(() -> {
if (callback != null) {
callback.onCancel(task.url);
}
});
}
else {
downloadSuccess = true;
shouldUnzip = true;
mHandler.post(() -> {
if (callback != null) {
callback.onSuccess(task.url, destination.getAbsolutePath());
}
});
}
} catch (Exception e) {
downloadSuccess = false;
AtotoLogger.e(TAG, "downloadFile error: " + e);
if (task.isCanceled) {
mHandler.post(() -> {
if (callback != null) {
callback.onCancel(task.url);
}
});
} else {
mHandler.post(() -> {
if (callback != null) {
callback.onError(task.url, e.getMessage());
}
});
}
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
// 关闭流失败
AtotoLogger.e(TAG, e);
}
// 清理文件:如果取消或失败,删除文件
if ((task.isCanceled || !downloadSuccess) && destination.exists()) {
boolean result = destination.delete();
AtotoLogger.d(TAG, "delete file: " + destination.getAbsolutePath() + " result: " + result);
}
// 只有下载成功且未被取消时才解压
if (shouldUnzip && !task.isCanceled) {
// 解压zip文件
startUnZip(destination, version, callback);
}
removeTask(task);
}
}
});
return task;
}
/**
* 移除任务
*/
private void removeTask(DownloadTask task) {
currentTasks.values().removeIf(t -> t.url.equals(task.url));
}
private void buildRequestHeader(okhttp3.Request.Builder builder, Map<String, String> heads) {
if (null == heads || heads.isEmpty()) {
return;
}
for (Map.Entry<String, String> entry : heads.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
}
/**
* 解压并删除zip文件
* @param destination
*/
private void startUnZip(File destination, String version, DownloadCallback callback) {
if (destination.exists() && destination.getAbsolutePath().endsWith(".zip")) {
Thread thread = new Thread(()-> {
try {
FileUnzip.unzipFile(destination, destination.getParentFile(), getFileNameWithoutExtension(destination.getName()));
} catch (Exception e) {
AtotoLogger.e(TAG, e);
}
// 写文件到version.txt
File modelDir = new File(destination.getParentFile(), getFileNameWithoutExtension(destination.getName()));
if (modelDir.isDirectory()) {
File versionFile = new File(modelDir, "version.txt");
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(versionFile);
outputStream.write(version.getBytes());
outputStream.flush();
unzipModelLiveData.postValue(destination.getName());
mHandler.post(()-> {
callback.onUnzipFinish(destination.getName());
});
} catch (IOException e) {
AtotoLogger.e(TAG, e);
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
AtotoLogger.e(TAG, e);
}
}
}
} else {
AtotoLogger.d(TAG, "modelDir: " + modelDir.getAbsolutePath() + " is not a directory");
}
// 删除zip文件
if (destination.exists()) {
boolean result = destination.delete();
AtotoLogger.d(TAG, "delete file: " + destination.getAbsolutePath() + " result: " + result);
}
});
thread.start();
}
}
private String getFileNameWithoutExtension(String fileName) {
// 查找最后一个点的位置
int lastDotIndex = fileName.lastIndexOf(".");
// 如果存在点,并且点不是第一个字符,去掉后缀
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
fileName = fileName.substring(0, lastDotIndex);
}
return fileName;
}
/**
* 生成任务唯一标识
*/
private String generateTaskKey(String url, String key) {
return url + "_" + key;
}
public static class DownloadTask {
private volatile boolean isCanceled = false;
private String url;
private File destination;
private Call call;
public DownloadTask(String url, File destination) {
this.url = url;
this.destination = destination;
}
public synchronized void cancel() {
if (this.call != null) {
this.call.cancel();
}
isCanceled = true;
}
public synchronized boolean isCanceled() {
return isCanceled;
}
public void setCall(Call call) {
this.call = call;
}
}
public interface DownloadCallback {
void onProgress(String url,long currentSize, long totalSize);
void onSuccess(String url,String filePath);
void onUnzipFinish(String unzipFilePath);
void onError(String url,String error);
void onCancel(String url);
}
}
依赖的解压工具
java
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* 描述:
* 创建者: IT乐手
* 日期: 2025/7/23
*/
public class FileUnzip {
private static final String TAG = "FileUnzip";
/**
* 解压文件
* @param zipFile 压缩包
* @param targetDirectory 解压的目录
* @param destDirName 解压后的文件夹名称
* @throws IOException
*/
public static void unzipFile(File zipFile, File targetDirectory, String destDirName) throws IOException {
long start = System.currentTimeMillis();
int level = 0;
File oldDir = null;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry zipEntry;
while ((zipEntry = zis.getNextEntry()) != null) {
File newFile = new File(targetDirectory, zipEntry.getName());
if (zipEntry.isDirectory()) {
if (level == 0) {
oldDir = newFile;
}
newFile.mkdirs();
++level;
} else {
newFile.getParentFile().mkdirs();
AtotoLogger.d("unzip file " + newFile.getAbsolutePath());
try (FileOutputStream fos = new FileOutputStream(newFile)) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
zis.closeEntry();
}
}
}
if (oldDir != null && oldDir.isDirectory() && oldDir.exists()) {
File newDir = new File(oldDir.getParentFile(), destDirName);
AtotoLogger.d("Change oldDir " + oldDir.getAbsolutePath() + " to " + newDir.getAbsolutePath());
try {
Files.move(oldDir, newDir);
AtotoLogger.d("Change success !");
} catch (Exception e) {
AtotoLogger.printException(TAG, e);
}
}
AtotoLogger.d(TAG, "unzipFile: " + zipFile.getAbsolutePath() + " to " + targetDirectory.getAbsolutePath() + " cost: " + (System.currentTimeMillis() - start) + "ms");
}
}