当前位置: 首页 > article >正文

Android 文件带进度的下载功能实现与封装

网络框架

现在基本都是okhttp3+rotrofit同时你可以加入rxjava3,今天就讲一下这几个结合实现简单的下载功能
先定义接口,下面两个区别就是一个可以断点续传而已

    /**
     * 大文件官方建议用 @Streaming 来进行注解,不然会出现IO异常,小文件可以忽略不注入
     *
     * @param fileUrl 文件路径地址
     * @return 观察者
     */
    @Streaming
    @GET
    Observable<ResponseBody> downloadFile(@Url String fileUrl);
    /**
     * 大文件官方建议用 @Streaming 来进行注解,不然会出现IO异常,小文件可以忽略不注入
     *
     * @param fileUrl 文件路径地址
     * @return 观察者
     */
    @Streaming
    @GET
    Observable<ResponseBody> downloadFile(@Header("Range") String range, @Url String fileUrl);

创建retrofit对象

和普通接口差不多

 private static final int TIME_OUT_SECOND = 120;
    private static Retrofit builder;
    private static final Interceptor headerInterceptor = chain -> {
        Request originalRequest = chain.request();
        Request.Builder requestBuilder = originalRequest.newBuilder()
                .addHeader("Accept-Encoding", "gzip")
                .method(originalRequest.method(), originalRequest.body());
        Request request = requestBuilder.build();
        return chain.proceed(request);
    };

    private static Retrofit getDownloadRetrofit() {

        OkHttpClient.Builder mBuilder = new OkHttpClient.Builder()
                .connectTimeout(TIME_OUT_SECOND, TimeUnit.SECONDS)
                .readTimeout(TIME_OUT_SECOND, TimeUnit.SECONDS)
                .writeTimeout(TIME_OUT_SECOND, TimeUnit.SECONDS)
                .addInterceptor(headerInterceptor);
        if (builder == null) {
            builder = new Retrofit.Builder()
                    .baseUrl(PropertiesUtil.getInstance().loadConfig(BaseApplication.getInstance()).getBaseUrl())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
                    .client(mBuilder.build())
                    .build();
        } else {
            builder = builder.newBuilder()
                    .client(mBuilder.build())
                    .build();
        }
        return builder;
    }

编写rxjava3代码

    public static Observable<File> enqueue(String url,String saveBasePath) {
        File tempFile = CommonUtil.getTempFile(url, saveBasePath);
        return getDownloadRetrofit(interceptor)
                .create(BaseApiService.class)
                .downloadFile("bytes=" + tempFile.length() + "-", url)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

完成初步代码

其实写到这里基本就已经完成了90%了因为如果你不要进度的话,你这样可以直接拿到ResponseBody然后就是读写文件了
tempFile为本地保存的文件名,可以直接读取url

        try (BufferedSource source = responseBody().source()) {
            long totalByte = responseBody().contentLength();
            long downloadByte = 0;
            if (!tempFile.getParentFile().exists()) {
                boolean mkdir = tempFile.getParentFile().mkdirs();
            }

            byte[] buffer = new byte[1024 * 4];
            RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "rwd");
            long tempFileLen = tempFile.length();
            randomAccessFile.seek(tempFileLen);
            while (true) {
                int len = responseBody().byteStream().read(buffer);
                if (len == -1) {
                    break;
                }

                randomAccessFile.write(buffer, 0, len);
            }
            randomAccessFile.close();


        } catch (IOException e) {
        }

完善进度功能

进度也就是根据已下载的文件大小/文件总大小,至于文件总大小后台可以返回也可以读responseBody().contentLength(),已下载的大小总和 所以我们只需要修改下上面的方法就可以得到进度

try (BufferedSource source = responseBody().source()) {
            long totalByte = responseBody().contentLength();
            long downloadByte = 0;
            if (!tempFile.getParentFile().exists()) {
                boolean mkdir = tempFile.getParentFile().mkdirs();
            }

            byte[] buffer = new byte[1024 * 4];
            RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "rwd");
            long tempFileLen = tempFile.length();
            randomAccessFile.seek(tempFileLen);
            while (true) {
                int len = responseBody().byteStream().read(buffer);
                if (len == -1) {
                    break;
                }

                randomAccessFile.write(buffer, 0, len);
                downloadByte += len;
                int progress = (int) ((downloadByte * 100) / totalByte);
            }
            randomAccessFile.close();
        } catch (IOException e) {
        }

这样进度就有了现在我们可以加上监听或者回调或者直接用通知发送出去,监听回调就不说了,我们介绍下通知

进度通知

编写一个通知工具类,这个不用解释吧,记得创建渠道

public class DownloadNotificationUtil extends ContextWrapper {

    private final NotificationManager mManager;
    private NotificationCompat.Builder mBuilder;

    public DownloadNotificationUtil(Context context) {
        super(context);
        mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    }

    /**
     * 显示通知栏
     *
     * @param id 通知消息id
     */
    public void showNotification(int id) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel();
        }
        mBuilder = new NotificationCompat.Builder(this, ConstantsHelper.DOWNLOAD_CHANNEL_ID);
        mBuilder.setTicker("开始下载");
        mBuilder.setOngoing(true);
        mBuilder.setContentTitle("开始下载");
        mBuilder.setProgress(100, 0, false);
        mBuilder.setContentText(0 + "%");
        mBuilder.setSmallIcon(AppManager.getAppManager().getAppIcon(this));
//        mBuilder.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.ic_launcher));
        mManager.notify(id, mBuilder.build());
    }

    @TargetApi(Build.VERSION_CODES.O)
    public void createNotificationChannel() {
        NotificationChannel channel = new NotificationChannel(ConstantsHelper.DOWNLOAD_CHANNEL_ID,
                ConstantsHelper.DOWNLOAD_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        channel.enableVibration(false);
        channel.enableLights(true);
        channel.setSound(null, null);
        if (notificationManager != null) {
            notificationManager.createNotificationChannel(channel);
        }
    }

    public long lastClick = 0;

    /**
     * [防止快速点击]
     *
     * @return false --> 快读点击
     */
    public boolean fastClick(long intervalTime) {
        if (System.currentTimeMillis() - lastClick <= intervalTime) {
            return true;
        }
        lastClick = System.currentTimeMillis();
        return false;
    }

    /**
     * 更新通知栏进度条
     *
     * @param id       获取Notification的id
     * @param progress 获取的进度
     */
    public void updateNotification(int id, int progress, String fileName) {
        if (fastClick(300) && progress != 100) {
            return;
        }
        if (mBuilder != null) {
            mBuilder.setContentTitle(fileName);
            mBuilder.setSmallIcon(AppManager.getAppManager().getAppIcon(this));
            mBuilder.setProgress(100, progress, false);
            mBuilder.setContentText(progress + "%");
            mManager.notify(id, mBuilder.build());
        }
    }

    public void sendNotificationFullScreen(int notifyId, String title, String content, File apkFile) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(ConstantsHelper.DOWNLOAD_CHANNEL_ID,
                    ConstantsHelper.DOWNLOAD_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
            mManager.createNotificationChannel(channel);

            PendingIntent fullScreenPendingIntent = null;
            if (apkFile != null) {
                Intent i = new Intent(Intent.ACTION_VIEW);
                i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK |
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
                Uri apkFileUri = FileProvider.getUriForFile(getApplicationContext(),
                        getPackageName() + ".FileProvider", apkFile);
                i.setDataAndType(apkFileUri, "application/vnd.android.package-archive");
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    fullScreenPendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
                } else {
                    fullScreenPendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
                }
            }
            NotificationCompat.Builder notificationBuilder =
                    new NotificationCompat.Builder(this, ConstantsHelper.DOWNLOAD_CHANNEL_ID)
                            .setContentTitle(title)
                            .setTicker(content)
                            .setContentText(content)
                            .setAutoCancel(true)
                            .setSmallIcon(AppManager.getAppManager().getAppIcon(this))
                            .setDefaults(Notification.DEFAULT_ALL)
                            .setPriority(NotificationCompat.PRIORITY_MAX)
                            .setCategory(Notification.CATEGORY_CALL)
                            .setFullScreenIntent(fullScreenPendingIntent, true);
            mManager.notify(notifyId, notificationBuilder.build());
        }
    }

    public void clearAllNotification() {
        if (mManager == null) {
            return;
        }
        mManager.cancelAll();
    }

    /**
     * 取消通知栏通知
     */
    public void cancelNotification(int id) {
        if (mManager == null) {
            return;
        }
        mManager.cancel(id);
    }

}

调用通知

downloadNotificationUtil.showNotification(fileUrl.hashCode())
downloadNotificationUtil.updateNotification(fileUrl.hashCode(),
                        progress, FileUtils.getFileName(fileUrl))
downloadNotificationUtil.cancelNotification(fileUrl.hashCode())

提出问题

问题来了,读写文件肯定不能在主线程里操作对吧,那么我们同样也不能再subscribe操作符中写,因为大部分他都要回调主线程的,所以我们结合rxjava3的知识可以利用flatmap等操作符切换到子线程操作,那么我们可以直接修改一下方法

    public static Observable<File> enqueue(String url,String saveBasePath) {
        File tempFile = CommonUtil.getTempFile(url, saveBasePath);
        DownloadInterceptor interceptor = new DownloadInterceptor();
        return getDownloadRetrofit(interceptor)
                .create(BaseApiService.class)
                .downloadFile("bytes=" + tempFile.length() + "-", url)
                .subscribeOn(Schedulers.io())
                .flatMap(responseBody ->
                        //这里需要返回一个ObservableOnSubscribe那么我们新建一个对象
                )
                .observeOn(AndroidSchedulers.mainThread());

    }
public class DownloadObservable implements ObservableOnSubscribe<File> {
    private final File tempFile;
    private final DownloadNotificationUtil downloadNotificationUtil;
    private final String fileUrl;


    public DownloadObservable( String fileUrl, File tempFile) {
        this.tempFile = tempFile;
        this.fileUrl = fileUrl;
        downloadNotificationUtil = new DownloadNotificationUtil(BaseApplication.getInstance());
        downloadNotificationUtil.showNotification(fileUrl.hashCode());
    }

    @Override
    public void subscribe(ObservableEmitter<File> emitter) throws Exception {
        try (BufferedSource source = responseBody().source()) {
            long totalByte = responseBody().contentLength();
            long downloadByte = 0;
            if (!tempFile.getParentFile().exists()) {
                boolean mkdir = tempFile.getParentFile().mkdirs();
            }

            byte[] buffer = new byte[1024 * 4];
            RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "rwd");
            long tempFileLen = tempFile.length();
            randomAccessFile.seek(tempFileLen);
            while (true) {
                int len = responseBody().byteStream().read(buffer);
                if (len == -1) {
                    break;
                }

                randomAccessFile.write(buffer, 0, len);
                downloadByte += len;
                int progress = (int) ((downloadByte * 100) / totalByte);

                downloadNotificationUtil.updateNotification(fileUrl.hashCode(),
                        progress, FileUtils.getFileName(fileUrl));
            }
            randomAccessFile.close();
            emitter.onNext(tempFile);
            emitter.onComplete();
        } catch (IOException e) {
          downloadNotificationUtil.cancelNotification(fileUrl.hashCode());
            emitter.onError(e);
            emitter.onComplete();
        }
    }
}

然后我们完善enqueue

public static Observable<File> enqueue(String url,String saveBasePath) {
        File tempFile = CommonUtil.getTempFile(url, saveBasePath);
    
        return getDownloadRetrofit()
                .create(BaseApiService.class)
                .downloadFile("bytes=" + tempFile.length() + "-", url)
                .subscribeOn(Schedulers.io())
                .flatMap(responseBody ->
                        Observable.create(new DownloadObservable(interceptor, url, tempFile, saveBasePath))
                )
                .observeOn(AndroidSchedulers.mainThread());

    }

然后你就实现了有进度的下载通知功能

新的问题出现了

你会发现通知和吐司什么的报错,因为他不能在子线程操作UI,但是文件下载又必须在子线程,所以你可以直接创建一个handler去切换到主线程

    private final Handler mainHandler = new Handler(Looper.getMainLooper());

调用

 Disposable disposable = DownloadManger.getInstance().download(this, binding.editUrl.getText().toString().trim())
                    .subscribe(file -> {
                        LogUtil.show(ApiRetrofit.TAG,"下载成功:"+file.getAbsolutePath());
                        showToast("下载成功!");
                    }, throwable -> {
                        if (throwable instanceof BaseException baseException) {
                            showToast(baseException.getErrorMsg());
                            return;
                        }
                        showToast(throwable.getMessage());
                    });

这里基本算完成95%了,正常情况下你没问题,问题出在下载地址上,因为有的下载地址没有文件名

http://192.168.1.1:8089/api/download/file/168878445

这样就没了所以你没办法判断他的文件名和文件类型也就没办法写成文件,因为不知道写成什么类型的文件
所以我们需要读取文件请求头中的信息去获取文件类型和文件名

优化

先读写成.tmp的临时文件

也就是前面提到的File tempFile = CommonUtil.getTempFile(url, saveBasePath);方法就不提供了,就是在缓存目录下读写一个.tmp的临时文件,下载完读取请求头中的文件类型和文件名后重命名一下文件

工具类

因为有的部分大小写不一样,所以我们需要忽略大小写

 /**
     * 忽略大小写查找请求头参数
     */
    private static String findHeaderIgnoreCase(Headers headers, String headerName) {
        for (String name : headers.names()) {
            if (name.equalsIgnoreCase(headerName)) {
                return headers.get(name);
            }
        }
        return null;
    }

    private static String getFileNameFromForceDownloadHeader(String contentDispositionHeader) {
        if (TextUtils.isEmpty(contentDispositionHeader)) {
            return "unknown";
        }
        // 匹配Content-Disposition中的filename属性
        Pattern pattern = Pattern.compile(".*filename=\"?([^\\s;]+)\"?.*");
        Matcher matcher = pattern.matcher(contentDispositionHeader.toLowerCase());
        if (matcher.matches()) {
            return matcher.group(1);
        }
        return "unknown";
    }

写完临时文件后重命名

这个时候你会发现读取文件类型和文件名需要返回信息中的header信息,但是请求指定写

    @Streaming
    @GET
    Observable<ResponseBody> downloadFile(@Header("Range") String range, @Url String fileUrl);

你不可以吧ResponseBody写成Response系统不支持会报错

思考一下:那么我们怎么解决问题了?

哪里有返回的header信息?

当然要找retrofit了,那retrofit又如何获取header呢?那就是拦截器
所以我们需要再创建retrofit创建的时候给他设置一个拦截器,获取headerbody

public class DownloadInterceptor implements Interceptor {

    private Headers headers;
    private ResponseBody responseBody;

    public Headers getHeaders() {
        return headers;
    }

    public ResponseBody getResponseBody() {
        return responseBody;
    }

    public DownloadInterceptor() {
    }

    @Override
    public Response intercept(@NonNull Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        headers = originalResponse.headers();
        return originalResponse.newBuilder()
                .body(responseBody = originalResponse.body())
                .build();
    }
}

有了拦截器我们在下载的时候吧header传过去即可

注意

每次下载都要创建新的拦截器,不然他获取的header``body就是上次的

最后提供下完整代码

public class DownloadRetrofitFactory {
    private static final int TIME_OUT_SECOND = 120;
    private static Retrofit builder;
    private static final Interceptor headerInterceptor = chain -> {
        Request originalRequest = chain.request();
        Request.Builder requestBuilder = originalRequest.newBuilder()
                .addHeader("Accept-Encoding", "gzip")
                .method(originalRequest.method(), originalRequest.body());
        Request request = requestBuilder.build();
        return chain.proceed(request);
    };

    private static Retrofit getDownloadRetrofit(DownloadInterceptor downloadInterceptor) {

        OkHttpClient.Builder mBuilder = new OkHttpClient.Builder()
                .connectTimeout(TIME_OUT_SECOND, TimeUnit.SECONDS)
                .readTimeout(TIME_OUT_SECOND, TimeUnit.SECONDS)
                .writeTimeout(TIME_OUT_SECOND, TimeUnit.SECONDS)
                .addInterceptor(headerInterceptor)
                .addInterceptor(downloadInterceptor);
        if (builder == null) {
            builder = new Retrofit.Builder()
                    .baseUrl(PropertiesUtil.getInstance().loadConfig(BaseApplication.getInstance()).getBaseUrl())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
                    .client(mBuilder.build())
                    .build();
        } else {
            builder = builder.newBuilder()
                    .client(mBuilder.build())
                    .build();
        }
        return builder;
    }

    /**
     * 取消网络请求
     */
    public static void cancel(Disposable d) {
        if (null != d && !d.isDisposed()) {
            d.dispose();
        }
    }

    public static Observable<File> enqueue(String url,String saveBasePath) {
        File tempFile = CommonUtil.getTempFile(url, saveBasePath);
        DownloadInterceptor interceptor = new DownloadInterceptor();
        return getDownloadRetrofit(interceptor)
                .create(BaseApiService.class)
                .downloadFile("bytes=" + tempFile.length() + "-", url)
                .subscribeOn(Schedulers.io())
                .flatMap(responseBody ->
                        Observable.create(new DownloadObservable(interceptor, url, tempFile, saveBasePath))
                )
                .observeOn(AndroidSchedulers.mainThread());

    }
}

public class DownloadObservable implements ObservableOnSubscribe<File> {
    private final File tempFile;
    private final DownloadInterceptor interceptor;
    private final DownloadNotificationUtil downloadNotificationUtil;
    private final String saveBasePath;
    private final String fileUrl;
    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    public DownloadObservable(DownloadInterceptor interceptor, String fileUrl, File tempFile, String saveBasePath) {
        this.tempFile = tempFile;
        this.interceptor = interceptor;
        this.saveBasePath = saveBasePath;
        this.fileUrl = fileUrl;
        downloadNotificationUtil = new DownloadNotificationUtil(BaseApplication.getInstance());
        mainHandler.post(() -> downloadNotificationUtil.showNotification(fileUrl.hashCode()));
    }

    @Override
    public void subscribe(ObservableEmitter<File> emitter) throws Exception {
        try (BufferedSource source = interceptor.getResponseBody().source()) {
            long totalByte = interceptor.getResponseBody().contentLength();
            long downloadByte = 0;
            if (!tempFile.getParentFile().exists()) {
                boolean mkdir = tempFile.getParentFile().mkdirs();
            }

            byte[] buffer = new byte[1024 * 4];
            RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "rwd");
            long tempFileLen = tempFile.length();
            randomAccessFile.seek(tempFileLen);
            while (true) {
                int len = interceptor.getResponseBody().byteStream().read(buffer);
                if (len == -1) {
                    break;
                }

                randomAccessFile.write(buffer, 0, len);
                downloadByte += len;
                int progress = (int) ((downloadByte * 100) / totalByte);

                mainHandler.post(() -> downloadNotificationUtil.updateNotification(fileUrl.hashCode(),
                        progress, FileUtils.getFileName(fileUrl)));
            }
            randomAccessFile.close();

            String fileName;
            MediaType mediaType = interceptor.getResponseBody().contentType();
            String contentDisposition = findHeaderIgnoreCase(interceptor.getHeaders(), "Content-Disposition");
            //获取请求头中的Content-Disposition,有值的话说明指定了文件名和后缀名
            if (mediaType != null && !TextUtils.isEmpty(contentDisposition)) {
                fileName = FileUtils.autoRenameFileName(saveBasePath, getFileNameFromForceDownloadHeader(contentDisposition));
            } else {
                fileName = FileUtils.autoRenameFileName(saveBasePath, FileUtils.getFileNameByUrl(fileUrl));
            }
            File newFile = new File(saveBasePath + fileName);
            boolean renameSuccess = tempFile.renameTo(newFile);
            mainHandler.post(() -> downloadNotificationUtil.cancelNotification(fileUrl.hashCode()));
            if (renameSuccess) {
                mainHandler.post(() -> Toast.makeText(BaseApplication.getInstance(), "文件已保存至" + newFile.getAbsolutePath(), Toast.LENGTH_SHORT).show());
                emitter.onNext(newFile);
            } else {
                mainHandler.post(() -> Toast.makeText(BaseApplication.getInstance(), "文件已保存至" + tempFile.getAbsolutePath(), Toast.LENGTH_SHORT).show());
                emitter.onNext(tempFile);
            }
            emitter.onComplete();
        } catch (IOException e) {
            mainHandler.post(() -> downloadNotificationUtil.cancelNotification(fileUrl.hashCode()));
            emitter.onError(e);
            emitter.onComplete();
        }
    }

    /**
     * 忽略大小写查找请求头参数
     */
    private static String findHeaderIgnoreCase(Headers headers, String headerName) {
        for (String name : headers.names()) {
            if (name.equalsIgnoreCase(headerName)) {
                return headers.get(name);
            }
        }
        return null;
    }

    private static String getFileNameFromForceDownloadHeader(String contentDispositionHeader) {
        if (TextUtils.isEmpty(contentDispositionHeader)) {
            return "unknown";
        }
        // 匹配Content-Disposition中的filename属性
        Pattern pattern = Pattern.compile(".*filename=\"?([^\\s;]+)\"?.*");
        Matcher matcher = pattern.matcher(contentDispositionHeader.toLowerCase());
        if (matcher.matches()) {
            return matcher.group(1);
        }
        return "unknown";
    }
}
public class DownloadManger {
    /**
     * 进度条与通知UI刷新的handler和msg常量
     */
    private static volatile DownloadManger updateManger;
    private final List<String> downloadMap = new ArrayList<>();

    private DownloadManger() {

    }

    public static DownloadManger getInstance() {
        if (updateManger == null) {
            synchronized (DownloadManger.class) {
                if (updateManger == null) {
                    updateManger = new DownloadManger();
                }
            }
        }
        return updateManger;
    }

    /**
     * 下载文件
     *
     * @param mContext     当前视图
     * @param fileUrl      下载文件路径
     * @param saveBasePath 保存文件路径默认文件路径为RxNet.PATH,
     */
    public Observable<File> download(Activity mContext, String fileUrl, String saveBasePath) {
        if (TextUtils.isEmpty(fileUrl)) {
            return Observable.error(new BaseException(BaseException.DOWNLOAD_URL_404_MSG, BaseException.DOWNLOAD_URL_404));
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                //申请WRITE_EXTERNAL_STORAGE权限
                ActivityCompat.requestPermissions(mContext, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        0x02);
                return Observable.error(new BaseException(BaseException.DOWNLOAD_NOT_PERMISSION_MSG, BaseException.DOWNLOAD_NOT_PERMISSION));
            }
        }
        if (downloadMap.contains(fileUrl)) {
            return Observable.error(new BaseException(BaseException.DOWNLOADING_ERROR_MSG, BaseException.DOWNLOADING_ERROR));
        }
        downloadMap.add(fileUrl);
        if (TextUtils.isEmpty(saveBasePath)) {
            saveBasePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() +
                    File.separator + FileUtils.getDefaultBasePath(mContext) + File.separator;
        }
        return DownloadRetrofitFactory.enqueue(fileUrl, saveBasePath).map(file -> {
            downloadMap.remove(fileUrl);
            return file;
        });
    }

    public Observable<File> download(Activity mContext, String fileUrl) {
        return download(mContext, fileUrl,
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() +
                        File.separator + FileUtils.getDefaultBasePath(mContext) + File.separator);
    }

}

调用

 Disposable disposable = DownloadManger.getInstance().download(this, binding.editUrl.getText().toString().trim())
                    .subscribe(file -> {
                        LogUtil.show(ApiRetrofit.TAG,"下载成功:"+file.getAbsolutePath());
                        showToast("下载成功!");
                    }, throwable -> {
                        if (throwable instanceof BaseException baseException) {
                            showToast(baseException.getErrorMsg());
                            return;
                        }
                        showToast(throwable.getMessage());
                    });

没了!!!
甩个github地址:https://github.com/fzkf9225/mvvm-componnent-master/blob/master/common/src/main/java/pers/fz/mvvm/util/update/DownloadManger.java

拓展

上面只有单文件下载,其实多文件也一样,利用rxjava的特性

    /**
     * RxJava方式下载附件,需要自己判断权限
     */
    public Single<List<File>> download(Activity mContext, List<String> urlString) {
        return download(mContext, urlString, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() +
                File.separator + FileUtils.getDefaultBasePath(mContext) + File.separator);
    }

    /**
     * RxJava方式下载附件,需要自己判断权限
     */
    public Single<List<File>> download(Activity mContext, List<String> urlString, String saveBasePath) {
        return Observable.fromIterable(urlString)
                .flatMap((Function<String, ObservableSource<File>>) filePath -> download(mContext, filePath, saveBasePath))
                .toList()
                .subscribeOn(Schedulers.io());
    }

http://www.kler.cn/a/388771.html

相关文章:

  • 继承和多态(上)
  • 32位、64位、x86与x64:深入解析计算机架构
  • STM32嵌入式闹钟系统设计与实现
  • zabbix监控端界面时间与服务器时间不对应
  • react 中 FC 模块作用
  • 《TCP/IP网络编程》学习笔记 | Chapter 11:进程间通信
  • 2024年11月6日Github流行趋势
  • 【计网不挂科】计算机网络期末考试——【选择题&填空题&判断题&简述题】试卷(2)
  • 蓝桥杯:编程爱好者的试炼场
  • 运维的目标管理:以业务为核心,驱动运维价值最大化
  • 实时高效,全面测评快递100API的物流查询功能
  • 基于单片机洗衣机控制器的设计(论文+源码)
  • BMC运维管理:IPMI实现服务器远控制
  • 笔记整理—linux驱动开发部分(10)input子系统与相关框架
  • 计算机毕业设计 | SpringBoot社区物业管理系统(附源码)
  • 使用Golang实现开发中常用的【实例设计模式】
  • Android下的系统调用 (syscall),内联汇编syscall
  • 开源项目OpenVoice的本地部署
  • Swift中的Combine
  • GISBox一站式解决GIS数据处理问题
  • 基于Zynq FPGA的雷龙SD NAND存储芯片性能测试
  • 直接插入排序法
  • 解方程(C语言)
  • JavaScript API部分知识点
  • 第三百一十九节 Java线程教程 - Java线程中断
  • element-ui-plus给头像avatar增加头像框