android 下载管理工具类

相信大家在项目开发的过程中会用到下载相关的操作,下面是我在工作中用到的下载逻辑管理类,支持下载和取消下载,进度监听功能,能满足大多数场景需求,希望对大家有帮助

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");
    }
}
相关推荐
2501_915106323 小时前
App 怎么上架 iOS?从准备资料到开心上架(Appuploader)免 Mac 上传的完整实战流程指南
android·macos·ios·小程序·uni-app·iphone·webview
科技峰行者4 小时前
安卓16提前发布能否改写移动生态格局
android
蒲公英少年带我飞5 小时前
Android NDK 编译 protobuf
android
沐怡旸5 小时前
【底层机制】ART虚拟机深度解析:Android运行时的架构革命
android·面试
小禾青青5 小时前
uniapp安卓打包遇到报错:Uncaught SyntaxError: Invalid regular expression: /[\p{L}\p{N}]/
android·uni-app
studyForMokey6 小时前
【Kotlin内联函数】
android·开发语言·kotlin
2501_915921438 小时前
iOS 抓不到包怎么办?工程化排查与替代抓包方案(抓包/HTTPS/Charles代理/tcpdump)
android·ios·小程序·https·uni-app·iphone·tcpdump
诸神黄昏EX9 小时前
Android Init 系列专题【篇六:reboot & shutdown】
android