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");
    }
}
相关推荐
dalancon1 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon1 小时前
VSYNC 信号完整流程2
android
dalancon1 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013842 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android3 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才3 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶4 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙4 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github
qq_283720055 小时前
MySQL技巧(四): EXPLAIN 关键参数详细解释
android·adb
没有了遇见6 小时前
Android 架构之网络框架多域名配置<三>
android