From 6abd2a1ba82a4f8ecbbc247af24d45e430b8ac71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Android=20=E8=BD=AE=E5=AD=90=E5=93=A5?= Date: Sun, 21 Jan 2024 00:00:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E6=96=AD=E7=82=B9=E7=BB=AD=E4=BC=A0=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=85=A8=E5=B1=80=E4=B8=8B=E8=BD=BD=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E7=9B=91=E5=90=AC=20=E4=BC=98=E5=8C=96=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=88=96=E8=80=85=E4=B8=8B=E8=BD=BD=E5=9B=9E=E8=B0=83=E6=97=B6?= =?UTF-8?q?=E6=9C=BA=20=E4=BC=98=E5=8C=96=E6=A1=86=E6=9E=B6=E5=86=85?= =?UTF-8?q?=E9=83=A8=E6=96=B9=E6=B3=95=E5=92=8C=E7=B1=BB=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HelpDoc.md | 30 +--- README.md | 60 +++++--- app/build.gradle | 19 ++- app/proguard-rules.pro | 7 + .../com/hjq/easy/demo/AppApplication.java | 4 +- .../java/com/hjq/easy/demo/MainActivity.java | 73 ++++----- .../easy/demo/http/api/UpdateImageApi.java | 2 - .../demo/other/ContentResolverUriStore.java | 68 +++++++++ app/src/main/res/values/strings_http.xml | 2 +- library/build.gradle | 4 +- .../src/main/java/com/hjq/http/EasyUtils.java | 14 +- ...peBody.java => CustomTypeRequestBody.java} | 4 +- .../com/hjq/http/callback/BaseCallback.java | 31 ++-- .../hjq/http/callback/DownloadCallback.java | 143 +++++++++++++----- .../com/hjq/http/callback/NormalCallback.java | 11 +- .../com/hjq/http/config/IRequestHandler.java | 18 --- .../config/impl/RequestJsonBodyStrategy.java | 2 +- .../java/com/hjq/http/model/CallProxy.java | 37 +++-- .../hjq/http/model/FileContentResolver.java | 6 +- .../java/com/hjq/http/model/HttpHeaders.java | 2 +- .../java/com/hjq/http/model/HttpParams.java | 2 +- .../com/hjq/http/request/BodyRequest.java | 8 +- .../com/hjq/http/request/DownloadRequest.java | 47 ++++-- .../com/hjq/http/request/HttpRequest.java | 63 +++++--- 24 files changed, 440 insertions(+), 217 deletions(-) create mode 100644 app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java rename library/src/main/java/com/hjq/http/body/{CustomTypeBody.java => CustomTypeRequestBody.java} (84%) diff --git a/HelpDoc.md b/HelpDoc.md index 8bf4eb6..535e748 100644 --- a/HelpDoc.md +++ b/HelpDoc.md @@ -10,8 +10,6 @@ * [框架初始化](#框架初始化) - * [混淆规则](#混淆规则) - * [使用文档](#使用文档) * [配置接口](#配置接口) @@ -36,7 +34,7 @@ * [如何添加或者删除全局参数](#如何添加或者删除全局参数) - * [如何定义全局的动态参数](#如何定义全局的动态参数) + * [如何动态添加全局的参数或者请求头](#如何动态添加全局的参数或者请求头) * [如何在请求中忽略某个全局参数](#如何在请求中忽略某个全局参数) @@ -207,23 +205,6 @@ EasyConfig.getInstance() .addParam("token", data.getData().getToken()); ``` -#### 混淆规则 - -```groovy -# OkHttp3 --keepattributes Signature --keepattributes *Annotation* --keep class okhttp3.** { *; } --keep interface okhttp3.** { *; } --dontwarn okhttp3.** --dontwarn okio.** - -# 不混淆这个包下的类 --keep class com.xxx.xxx.xxx.xxx.** { - ; -} -``` - # 使用文档 #### 配置接口 @@ -399,6 +380,8 @@ EasyHttp.download(this) //.url("https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk") .url("http://dldir1.qq.com/weixin/android/weixin708android1540.apk") .md5("2E8BDD7686474A7BC4A51ADC3667CABF") + // 设置断点续传(默认不开启) + //.resumableTransfer(true) .listener(new OnDownloadListener() { @Override @@ -419,7 +402,8 @@ EasyHttp.download(this) @Override public void onDownloadFail(File file, Throwable throwable) { - toast("下载出错:" + throwable.getMessage()); + toast("下载失败:" + throwable.getMessage()); + file.delete(); } @Override @@ -705,14 +689,16 @@ EasyConfig.getInstance().addHeader("key", "value"); EasyConfig.getInstance().removeHeader("key"); ``` -#### 如何定义全局的动态参数 +#### 如何动态添加全局的参数或者请求头 ```java EasyConfig.getInstance().setInterceptor(new IRequestInterceptor() { @Override public void interceptArguments(@NonNull HttpRequest httpRequest, @NonNull HttpParams params, @NonNull HttpHeaders headers) { + // 添加请求头 headers.put("key", "value"); + // 添加参数 params.put("key", "value"); } }); diff --git a/README.md b/README.md index 5d54276..4c75e52 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ * 博客地址:[网络请求,如斯优雅](https://www.jianshu.com/p/93cd59dec002) -* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处下载Demo](https://github.com/getActivity/EasyHttp/releases/download/12.6/EasyHttp.apk) +* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处下载Demo](https://github.com/getActivity/EasyHttp/releases/download/12.8/EasyHttp.apk) ![](picture/demo_code.png) @@ -61,7 +61,7 @@ android { dependencies { // 网络请求框架:https://github.com/getActivity/EasyHttp - implementation 'com.github.getActivity:EasyHttp:12.6' + implementation 'com.github.getActivity:EasyHttp:12.8' // OkHttp 框架:https://github.com/square/okhttp // noinspection GradleDependency implementation 'com.squareup.okhttp3:okhttp:3.12.13' @@ -72,30 +72,64 @@ dependencies { #### 框架混淆规则 -* 在混淆规则文件 `proguard-rules.pro` 中加入 +* OkHttp3 框架混淆规则 ```text +# OkHttp3 框架混淆规则 +-keepattributes Signature +-keepattributes *Annotation* +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** +-dontwarn okio.** +``` + +* EasyHttp 框架混淆规则 + +```text +# EasyHttp 框架混淆规则 -keep class com.hjq.http.** {*;} ``` +* 不混淆实现 OnHttpListener 接口的类 + +```text +# 必须要加上此规则,否则会导致泛型解析失败 +-keep public class * implements com.hjq.http.listener.OnHttpListener { + *; +} +``` + +* 不混淆某个包下的 Bean 类 + +```text +# 必须要加上此规则,否则可能会导致 Bean 类的字段无法解析成后台返回的字段,xxx 请替换成对应包名 +-keep class com.xxx.xxx.xxx.xxx.** { + ; +} +``` + +* 以上混淆规则,可以在主模块的 `proguard-rules.pro` 文件中加入 + ## [框架的具体用法请点击这里查看](HelpDoc.md) ### 不同网络请求框架之间的对比 | 功能或细节 | [EasyHttp](https://github.com/getActivity/EasyHttp) | [Retrofit](https://github.com/square/retrofit) | [OkGo](https://github.com/jeasonlzy/okhttp-OkGo) | | :----: | :------: | :-----: | :-----: | -| 对应版本 | 12.6 | 2.9.0 | 3.0.4 | +| 对应版本 | 12.8 | 2.9.0 | 3.0.4 | | issues 数 | [![](https://img.shields.io/github/issues/getActivity/EasyHttp.svg)](https://github.com/getActivity/EasyHttp/issues) | [![](https://img.shields.io/github/issues/square/retrofit.svg)](https://github.com/square/retrofit/issues) | [![](https://img.shields.io/github/issues/jeasonlzy/okhttp-OkGo.svg)](https://github.com/jeasonlzy/okhttp-OkGo/issues) | -| **aar 包大小** | 93 KB | 123 KB | 131 KB | +| **aar 包大小** | 95 KB | 123 KB | 131 KB | | minSdk 要求 | API 14+ | API 21+ | API 14+ | | 配置多域名 | ✅ | ❌ | ✅ | | **动态 Host** | ✅ | ❌ | ❌ | | 全局参数 | ✅ | ❌ | ✅ | | 日志打印 | ✅ | ❌ | ✅ | | 超时重试 | ✅ | ✅ | ✅ | -| **请求缓存** | ✅ | ❌ | ✅ | -| **下载校验** | ✅ | ❌ | ❌ | +| **配置 Http 缓存** | ✅ | ❌ | ✅ | +| **下载文件校验** | ✅ | ❌ | ❌ | | **极速下载** | ✅ | ❌ | ❌ | +| **下载断点续传** | ✅ | ❌ | ✅ | | 上传进度监听 | ✅ | ❌ | ✅ | | Json 参数提交 | ✅ | ❌ | ✅ | | Json 日志打印格式化 | ✅ | ❌ | ❌ | @@ -283,18 +317,6 @@ EasyHttp.post(this) * [WanAndroid](https://www.wanandroid.com/) -#### 广告区 - -* 我现在任腾讯云服务器推广大使,大家如果有购买服务器的需求,可以通过下面的链接购买 - -[![](https://upload-dianshi-1255598498.file.myqcloud.com/upload/nodir/345X200-9ae456f58874df499adf7c331c02cb0fed12b81d.jpg)](https://curl.qcloud.com/A6cYskvv) - -[【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中](https://curl.qcloud.com/A6cYskvv) - -[![](https://upload-dianshi-1255598498.file.myqcloud.com/345-200-b28f7dee9552f4241ea6a543f15a9798049701d4.jpg)](https://curl.qcloud.com/up4fQsdn) - -[【腾讯云】中小企业福利专场,多款刚需产品,满足企业通用场景需求](https://curl.qcloud.com/up4fQsdn) - ## License ```text diff --git a/app/build.gradle b/app/build.gradle index 83e5065..c2f4ce1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'com.hjq.easy.demo' minSdkVersion 21 targetSdkVersion 31 - versionCode 1206 - versionName '12.6' + versionCode 1208 + versionName '12.8' } // 支持 JDK 1.8 @@ -73,7 +73,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.12.13' // 吐司框架:https://github.com/getActivity/Toaster - implementation 'com.github.getActivity:Toaster:12.5' + implementation 'com.github.getActivity:Toaster:12.6' // 权限请求框架:https://github.com/getActivity/XXPermissions implementation 'com.github.getActivity:XXPermissions:18.5' @@ -81,20 +81,25 @@ dependencies { // 标题栏框架:https://github.com/getActivity/TitleBar implementation 'com.github.getActivity:TitleBar:10.5' + // Gson 解析容错:https://github.com/getActivity/GsonFactory + implementation 'com.github.getActivity:GsonFactory:9.5' // Json 解析框架:https://github.com/google/gson implementation 'com.google.code.gson:gson:2.10.1' - // Gson 解析容错:https://github.com/getActivity/GsonFactory - implementation 'com.github.getActivity:GsonFactory:9.0' + // Kotlin 反射库:用于反射 Kotlin data class 类对象 + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.5.10' // 腾讯 MMKV:https://github.com/Tencent/MMKV - implementation 'com.tencent:mmkv-static:1.2.14' + implementation ('com.tencent:mmkv-static:1.3.2') { + // 避免版本不一致导致的依赖冲突,从而导致编译报错 + exclude group: 'androidx.annotation', module: 'annotation' + } // Bugly 异常捕捉:https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20190418140644 implementation 'com.tencent.bugly:crashreport:4.1.9' implementation 'com.tencent.bugly:nativecrashreport:3.9.2' // 日志调试框架:https://github.com/getActivity/Logcat - debugImplementation 'com.github.getActivity:Logcat:11.8' + debugImplementation 'com.github.getActivity:Logcat:11.82' // OkHttp 抓包框架:https://github.com/lygttpod/AndroidMonitor // debugImplementation 'io.github.lygttpod:monitor:0.0.7' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index aeaf7fe..a3c22dd 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,6 +32,13 @@ -dontwarn okhttp3.** -dontwarn okio.** +# EasyHttp +-keep class com.hjq.http.** {*;} +# 必须要加上此规则,否则会导致泛型解析失败 +-keep public class * implements com.hjq.http.listener.OnHttpListener { + *; +} + # 不混淆这个包下的类 -keep class com.hjq.easy.demo.http.** { ; diff --git a/app/src/main/java/com/hjq/easy/demo/AppApplication.java b/app/src/main/java/com/hjq/easy/demo/AppApplication.java index d553b7f..a298208 100644 --- a/app/src/main/java/com/hjq/easy/demo/AppApplication.java +++ b/app/src/main/java/com/hjq/easy/demo/AppApplication.java @@ -46,12 +46,12 @@ public void onParseObjectException(TypeToken typeToken, String fieldName, Jso } @Override - public void onParseListException(TypeToken typeToken, String fieldName, JsonToken listItemJsonToken) { + public void onParseListItemException(TypeToken typeToken, String fieldName, JsonToken listItemJsonToken) { handlerGsonParseException("解析 List 异常:" + typeToken + "#" + fieldName + ",后台返回的条目类型为:" + listItemJsonToken); } @Override - public void onParseMapException(TypeToken typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) { + public void onParseMapItemException(TypeToken typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) { handlerGsonParseException("解析 Map 异常:" + typeToken + "#" + fieldName + ",mapItemKey = " + mapItemKey + ",后台返回的条目类型为:" + mapItemJsonToken); } diff --git a/app/src/main/java/com/hjq/easy/demo/MainActivity.java b/app/src/main/java/com/hjq/easy/demo/MainActivity.java index 81f1ed5..46225ef 100644 --- a/app/src/main/java/com/hjq/easy/demo/MainActivity.java +++ b/app/src/main/java/com/hjq/easy/demo/MainActivity.java @@ -23,7 +23,6 @@ import com.hjq.easy.demo.http.model.HttpData; import com.hjq.http.EasyHttp; import com.hjq.http.EasyUtils; -import com.hjq.http.exception.FileMd5Exception; import com.hjq.http.listener.HttpCallbackProxy; import com.hjq.http.listener.OnDownloadListener; import com.hjq.http.listener.OnUpdateListener; @@ -164,21 +163,23 @@ public void onHttpSuccess(HttpData result) { return; } + /* // 如果是放到外部存储目录下则需要适配分区存储 -// String fileName = "EasyHttp.png"; -// File file; -// Uri outputUri; -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// // 适配 Android 10 分区存储特性 -// ContentValues values = new ContentValues(); -// // 设置显示的文件名 -// values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); -// // 生成一个新的 uri 路径 -// outputUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); -// file = new FileContentResolver(getContentResolver(), outputUri, fileName); -// } else { -// file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), fileName); -// } + String fileName = "EasyHttp.png"; + File file; + Uri outputUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // 适配 Android 10 分区存储特性 + ContentValues values = new ContentValues(); + // 设置显示的文件名 + values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); + // 生成一个新的 uri 路径 + outputUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + file = new FileContentResolver(getContentResolver(), outputUri, fileName); + } else { + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), fileName); + } + */ // 如果是放到外部存储的应用专属目录则不需要适配分区存储特性 File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "我是测试专用的图片.png"); @@ -236,22 +237,27 @@ public void onUpdateEnd(Call call) { return; } + /* // 如果是放到外部存储目录下则需要适配分区存储 -// String fileName = "微信 8.0.15.apk"; -// -// File file; -// Uri outputUri; -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// // 适配 Android 10 分区存储特性 -// ContentValues values = new ContentValues(); -// // 设置显示的文件名 -// values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); -// // 生成一个新的 uri 路径 -// outputUri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); -// file = new FileContentResolver(getContentResolver(), outputUri, fileName); -// } else { -// file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName); -// } + String fileName = "微信 8.0.15.apk"; + + File file; + Uri outputUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // 适配 Android 10 分区存储特性 + ContentValues values = new ContentValues(); + // 设置显示的文件名 + values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); + // 生成一个新的 uri 路径 + // 注意这里使用 ContentResolver 插入的时候都会生成新的 Uri + // 解决方式将 ContentValues 和 Uri 作为 key 和 value 进行持久化关联 + // outputUri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); + outputUri = ContentResolverUriStore.insert(this, Downloads.EXTERNAL_CONTENT_URI, values); + file = new FileContentResolver(getContentResolver(), outputUri, fileName); + } else { + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName); + } + */ // 如果是放到外部存储的应用专属目录则不需要适配分区存储特性 File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "微信 8.0.15.apk"); @@ -262,6 +268,8 @@ public void onUpdateEnd(Call call) { //.url("https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk") .url("https://dldir1.qq.com/weixin/android/weixin8015android2020_arm64.apk") .md5("b05b25d4738ea31091dd9f80f4416469") + // 设置断点续传(默认不开启) + .resumableTransfer(true) .listener(new OnDownloadListener() { @Override @@ -284,10 +292,7 @@ public void onDownloadSuccess(File file) { @Override public void onDownloadFail(File file, Throwable throwable) { Toaster.show("下载失败:" + throwable.getMessage()); - if (throwable instanceof FileMd5Exception) { - // 如果是文件 md5 校验失败,则删除文件 - file.delete(); - } + file.delete(); } @Override diff --git a/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java b/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java index a5e0f52..80e3a91 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java +++ b/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java @@ -1,10 +1,8 @@ package com.hjq.easy.demo.http.api; import androidx.annotation.NonNull; - import com.hjq.http.config.IRequestApi; import com.hjq.http.config.IRequestServer; - import java.io.File; /** diff --git a/app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java b/app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java new file mode 100644 index 0000000..b2d8ea6 --- /dev/null +++ b/app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java @@ -0,0 +1,68 @@ +package com.hjq.easy.demo.other; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2024/01/20 + * desc : ContentResolver uri 存储器 + */ +public class ContentResolverUriStore { + + private static final String CACHE_PREFERENCES_NAME = "content_resolver_cache_store_preferences"; + + public static @Nullable Uri insert(Context context, @RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values) { + ContentResolver contentResolver = context.getContentResolver(); + SharedPreferences sharedPreferences = context.getSharedPreferences(CACHE_PREFERENCES_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + // 使用字符串作为缓存键 + String cacheKey = "ContentResolver insert: " + "Uri = " + url + ", ContentValues = " + convertContentValuesToString(values); + String oldUriString = sharedPreferences.getString(cacheKey, ""); + if (oldUriString != null && !"".equals(oldUriString)) { + Cursor cursor = null; + try { + Uri oldUri = Uri.parse(oldUriString); + // 查询旧的 uri 是否真实并且可用,如果是的话,再进行复用 + cursor = contentResolver.query(oldUri, null, null, null, null); + if (cursor != null && cursor.getCount() != 0) { + return oldUri; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (cursor != null) { + try { + cursor.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + Uri newUri = contentResolver.insert(url, values); + // 存储数据到 SharedPreferences + editor.putString(cacheKey, String.valueOf(newUri)); + editor.apply(); + return newUri; + } + + /** + * 将 ContentValues 转换为字符串 + */ + private static String convertContentValuesToString(ContentValues contentValues) { + if (contentValues == null) { + return ""; + } + return contentValues.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings_http.xml b/app/src/main/res/values/strings_http.xml index 8252817..28ffea2 100644 --- a/app/src/main/res/values/strings_http.xml +++ b/app/src/main/res/values/strings_http.xml @@ -10,6 +10,6 @@ 服务器连接异常,请稍后再试 请求被中断,请重试 服务器数据返回异常,请稍后再试 - 下载失败,文件 md5 校验失败 + 文件 md5 校验失败 \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index e1f18d2..02aad34 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -5,8 +5,8 @@ android { defaultConfig { minSdkVersion 16 - versionCode 1206 - versionName "12.6" + versionCode 1208 + versionName "12.8" } // 使用 JDK 1.8 diff --git a/library/src/main/java/com/hjq/http/EasyUtils.java b/library/src/main/java/com/hjq/http/EasyUtils.java index 29e03cd..32bea8f 100644 --- a/library/src/main/java/com/hjq/http/EasyUtils.java +++ b/library/src/main/java/com/hjq/http/EasyUtils.java @@ -573,10 +573,20 @@ public static InputStream openFileInputStream(File file) throws FileNotFoundExce * 打开文件的输出流 */ public static OutputStream openFileOutputStream(File file) throws FileNotFoundException { + return openFileOutputStream(file, false); + } + + /** + * 打开文件的输出流 + * + * @param file 文件对象 + * @param append 是否追加内容 + */ + public static OutputStream openFileOutputStream(File file, boolean append) throws FileNotFoundException { if (file instanceof FileContentResolver) { - return ((FileContentResolver) file).openOutputStream(); + return ((FileContentResolver) file).openOutputStream(append); } - return new FileOutputStream(file); + return new FileOutputStream(file, append); } /** diff --git a/library/src/main/java/com/hjq/http/body/CustomTypeBody.java b/library/src/main/java/com/hjq/http/body/CustomTypeRequestBody.java similarity index 84% rename from library/src/main/java/com/hjq/http/body/CustomTypeBody.java rename to library/src/main/java/com/hjq/http/body/CustomTypeRequestBody.java index 360ab5e..fe3406d 100644 --- a/library/src/main/java/com/hjq/http/body/CustomTypeBody.java +++ b/library/src/main/java/com/hjq/http/body/CustomTypeRequestBody.java @@ -9,12 +9,12 @@ * time : 2022/09/17 * desc : 支持自定义 Content-Type 的 RequestBody */ -public class CustomTypeBody extends WrapperRequestBody { +public class CustomTypeRequestBody extends WrapperRequestBody { /** 内容类型 */ private MediaType mContentType; - public CustomTypeBody(RequestBody body) { + public CustomTypeRequestBody(RequestBody body) { super(body); } diff --git a/library/src/main/java/com/hjq/http/callback/BaseCallback.java b/library/src/main/java/com/hjq/http/callback/BaseCallback.java index 2b13cd5..8a1c7c1 100644 --- a/library/src/main/java/com/hjq/http/callback/BaseCallback.java +++ b/library/src/main/java/com/hjq/http/callback/BaseCallback.java @@ -25,8 +25,11 @@ public abstract class BaseCallback implements Callback { /** 请求配置 */ private final HttpRequest mHttpRequest; + /** 请求任务对象创建工厂 */ + private CallProxy.Factory mCallProxyFactory; + /** 请求任务对象 */ - private CallProxy mCall; + private CallProxy mCallProxy; /** 当前重试次数 */ private int mRetryCount; @@ -38,18 +41,28 @@ public BaseCallback(@NonNull HttpRequest request) { () -> HttpLifecycleManager.register(mHttpRequest.getLifecycleOwner())); } - public BaseCallback setCall(CallProxy call) { - mCall = call; + public BaseCallback setCallProxy(CallProxy callProxy) { + mCallProxy = callProxy; + return this; + } + + public BaseCallback setCallProxyFactory(CallProxy.Factory factory) { + mCallProxyFactory = factory; return this; } public void start() { - mCall.enqueue(this); - onStart(mCall); + onStart(); + mCallProxy = mCallProxyFactory.create(); + try { + mCallProxy.enqueue(this); + } catch (Throwable throwable) { + onHttpFailure(throwable); + } } - protected CallProxy getCall() { - return mCall; + protected CallProxy getCallProxy() { + return mCallProxy; } @Override @@ -82,7 +95,7 @@ public void onFailure(@NonNull Call call, @NonNull IOException e) { mRetryCount++; Call newCall = call.clone(); - mCall.setCall(newCall); + mCallProxy.setRealCall(newCall); newCall.enqueue(BaseCallback.this); // 请求超时,正在执行延迟重试 EasyLog.printLog(mHttpRequest, "The request timed out, a delayed retry is being performed, the number of retries: " + @@ -98,7 +111,7 @@ public void onFailure(@NonNull Call call, @NonNull IOException e) { /** * 请求开始 */ - protected abstract void onStart(Call call); + protected abstract void onStart(); /** * 请求成功 diff --git a/library/src/main/java/com/hjq/http/callback/DownloadCallback.java b/library/src/main/java/com/hjq/http/callback/DownloadCallback.java index e802ea7..8a948b4 100644 --- a/library/src/main/java/com/hjq/http/callback/DownloadCallback.java +++ b/library/src/main/java/com/hjq/http/callback/DownloadCallback.java @@ -1,5 +1,6 @@ package com.hjq.http.callback; +import android.text.TextUtils; import androidx.annotation.NonNull; import com.hjq.http.EasyLog; import com.hjq.http.EasyUtils; @@ -9,11 +10,15 @@ import com.hjq.http.exception.ResponseException; import com.hjq.http.lifecycle.HttpLifecycleManager; import com.hjq.http.listener.OnDownloadListener; +import com.hjq.http.model.CallProxy; import com.hjq.http.request.HttpRequest; import java.io.File; +import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicLong; import okhttp3.Call; +import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; @@ -42,14 +47,17 @@ public final class DownloadCallback extends BaseCallback { private OnDownloadListener mListener; /** 下载总字节 */ - private long mTotalByte; + private final AtomicLong mTotalByte = new AtomicLong(); /** 已下载字节 */ - private long mDownloadByte; + private final AtomicLong mDownloadByte = new AtomicLong(); /** 下载进度 */ private int mDownloadProgress; + /** 断点续传开关 */ + private boolean mResumableTransfer; + public DownloadCallback(@NonNull HttpRequest request) { super(request); mHttpRequest = request; @@ -70,9 +78,13 @@ public DownloadCallback setListener(OnDownloadListener listener) { return this; } + public DownloadCallback setResumableTransfer(boolean resumableTransfer) { + mResumableTransfer = resumableTransfer; + return this; + } + @Override - protected void onStart(Call call) { - mHttpRequest.getRequestHandler().downloadStart(mHttpRequest, mFile); + protected void onStart() { EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), this::dispatchDownloadStartCallback); } @@ -82,14 +94,33 @@ protected void onHttpResponse(Response response) throws Throwable { EasyLog.printLog(mHttpRequest, "RequestConsuming:" + (response.receivedResponseAtMillis() - response.sentRequestAtMillis()) + " ms"); + // 响应码 416 表示请求的范围不符合要求,这通常发生在使用断点续传(Range请求头)时,服务器无法满足请求的范围条件,造成这个问题的原因可能有以下几种: + // 1. 范围请求错误:请求头中的Range字段可能设置了无效的范围。请确保你设置的范围是有效的,且在文件范围内 + // 2. 服务器不支持范围请求:有些服务器不支持范围请求,尤其是对于静态文件服务。在这种情况下,服务器可能会返回 416 错误 + // 3. 服务器不允许断点续传:即使服务器支持范围请求,也可能配置为不允许断点续传。这可能是出于性能或其他原因的考虑 + if (response.code() == 416 && !TextUtils.isEmpty(response.request().header("Range"))) { + Request request = response.request().newBuilder().removeHeader("Range").build(); + CallProxy callProxy = getCallProxy(); + Call newCall = mHttpRequest.getRequestClient().getOkHttpClient().newCall(request); + callProxy.setRealCall(newCall); + Response newResponse = callProxy.execute(); + // 打印请求耗时时间 + EasyLog.printLog(mHttpRequest, "The response status code is 416" + + ", response message: " + response.message() + ", require special treatment" + + ", re-initiate a new request,new request consuming:" + + (newResponse.receivedResponseAtMillis() - newResponse.sentRequestAtMillis()) + " ms"); + // 替换之前的 Response 对象 + response = newResponse; + } + IRequestInterceptor interceptor = mHttpRequest.getRequestInterceptor(); if (interceptor != null) { response = interceptor.interceptResponse(mHttpRequest, response); } if (!response.isSuccessful()) { - throw new ResponseException("The request failed, responseCode: " + - response.code() + ", message: " + response.message(), response); + throw new ResponseException("The request failed, response code: " + + response.code() + ", response message: " + response.message(), response); } // 如果没有指定文件的 md5 值 @@ -102,6 +133,33 @@ protected void onHttpResponse(Response response) throws Throwable { } } + // 如果这个文件已经下载过,并且经过校验 MD5 是同一个文件的话,就直接回调下载成功监听 + if (verifyFileMd5()) { + // 文件已存在,跳过下载 + EasyLog.printLog(mHttpRequest, mFile.getPath() + " download file already exists, skip request"); + EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), () -> dispatchDownloadSuccessCallback(true)); + return; + } + + // 当前是否支持断点续传 + boolean supportResumableTransfer = false; + long fileLength = mFile.length(); + // 当前必须开启了断点续传的开关 + if (mResumableTransfer && response.code() == 206 && fileLength > 0) { + // 获取 Accept-Ranges 字段的值 + String acceptRanges = response.header("Accept-Ranges"); + String contentRange = response.header("Content-Range"); + // 若能够找到 Content-Range,则表明服务器支持断点续传 + // 有些服务器还会返回 Accept-Ranges,输出结果 Accept-Ranges: bytes,说明服务器支持按字节下载 + if (acceptRanges != null && !"".equals(acceptRanges)) { + // Accept-Ranges:bytes + supportResumableTransfer = "bytes".equalsIgnoreCase(acceptRanges); + } else if (contentRange != null && !"".equals(contentRange)) { + // Content-Range: bytes 7897088-221048888/221048889 + supportResumableTransfer = contentRange.matches("bytes\\s+\\d+-\\d+/\\d+"); + } + } + File parentFile = mFile.getParentFile(); if (parentFile != null) { EasyUtils.createFolder(parentFile); @@ -111,32 +169,38 @@ protected void onHttpResponse(Response response) throws Throwable { throw new NullBodyException("The response body is empty"); } - mTotalByte = body.contentLength(); - if (mTotalByte < 0) { - mTotalByte = 0; - } - - // 如果这个文件已经下载过,并且经过校验 MD5 是同一个文件的话,就直接回调下载成功监听 - if (mFile.isFile() && mMd5 != null && !"".equals(mMd5) && - mMd5.equalsIgnoreCase(EasyUtils.getFileMd5(EasyUtils.openFileInputStream(mFile)))) { - // 文件已存在,跳过下载 - EasyLog.printLog(mHttpRequest, mFile.getPath() + " file already exists, skip download"); - EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), () -> dispatchDownloadSuccessCallback(true)); - return; + mTotalByte.set(body.contentLength()); + if (mTotalByte.get() < 0) { + mTotalByte.set(0); } int readLength; - mDownloadByte = 0; + mDownloadByte.set(0); byte[] bytes = new byte[8192]; - InputStream inputStream = body.byteStream(); - OutputStream outputStream = EasyUtils.openFileOutputStream(mFile); - while ((readLength = inputStream.read(bytes)) != -1) { - mDownloadByte += readLength; - outputStream.write(bytes, 0, readLength); + InputStream responeInputStream = body.byteStream(); + + OutputStream fileOutputStream = EasyUtils.openFileOutputStream(mFile, supportResumableTransfer); + if (supportResumableTransfer) { + mDownloadByte.addAndGet(fileLength); + // 有一些响应头没有返回 Content-Length 请求头,会导致总字节数为 0 + if (mTotalByte.get() > 0) { + mTotalByte.addAndGet(fileLength); + } + EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), this::dispatchDownloadByteChangeCallback); + } + + while ((readLength = responeInputStream.read(bytes)) != -1) { + mDownloadByte.addAndGet(readLength); + fileOutputStream.write(bytes, 0, readLength); EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), this::dispatchDownloadByteChangeCallback); } - EasyUtils.closeStream(inputStream); - EasyUtils.closeStream(outputStream); + + // 刷新 IO 缓冲区 + fileOutputStream.flush(); + + EasyUtils.closeStream(responeInputStream); + EasyUtils.closeStream(fileOutputStream); + EasyUtils.closeStream(response); String md5 = EasyUtils.getFileMd5(EasyUtils.openFileInputStream(mFile)); if (mMd5 != null && !"".equals(mMd5) && !mMd5.equalsIgnoreCase(md5)) { @@ -144,9 +208,6 @@ protected void onHttpResponse(Response response) throws Throwable { throw new FileMd5Exception("File md5 hash verify failure", md5); } - // 下载成功 - mHttpRequest.getRequestHandler().downloadSuccess(mHttpRequest, response, mFile); - EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), () -> dispatchDownloadSuccessCallback(false)); } @@ -161,18 +222,28 @@ protected void onHttpFailure(final Throwable throwable) { EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), () -> dispatchDownloadFailCallback(finalThrowable)); } - private void dispatchDownloadStartCallback() { + public boolean verifyFileMd5() { + try { + return mFile.isFile() && mMd5 != null && !"".equals(mMd5) && + mMd5.equalsIgnoreCase(EasyUtils.getFileMd5(EasyUtils.openFileInputStream(mFile))); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return false; + } + + public void dispatchDownloadStartCallback() { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { mListener.onDownloadStart(mFile); } EasyLog.printLog(mHttpRequest, "Download file start, file path = " + mFile.getPath()); } - private void dispatchDownloadByteChangeCallback() { + public void dispatchDownloadByteChangeCallback() { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { - mListener.onDownloadByteChange(mFile, mTotalByte, mDownloadByte); + mListener.onDownloadByteChange(mFile, mTotalByte.get(), mDownloadByte.get()); } - int currentProgress = EasyUtils.getProgressProgress(mTotalByte, mDownloadByte); + int currentProgress = EasyUtils.getProgressProgress(mTotalByte.get(), mDownloadByte.get()); // 只有下载进度发生改变的时候才回调此方法,避免引起不必要的 View 重绘 if (currentProgress == mDownloadProgress) { return; @@ -182,10 +253,10 @@ private void dispatchDownloadByteChangeCallback() { mListener.onDownloadProgressChange(mFile, mDownloadProgress); } EasyLog.printLog(mHttpRequest, "Download file progress change, downloaded: " + mDownloadByte + " / " + mTotalByte + - ", progress: " + currentProgress + " %" + "file path = " + mFile.getPath()); + ", progress: " + currentProgress + " %" + ", file path = " + mFile.getPath()); } - private void dispatchDownloadSuccessCallback(boolean cache) { + public void dispatchDownloadSuccessCallback(boolean cache) { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { mListener.onDownloadSuccess(mFile, cache); mListener.onDownloadEnd(mFile); @@ -193,7 +264,7 @@ private void dispatchDownloadSuccessCallback(boolean cache) { EasyLog.printLog(mHttpRequest, "Download file success, file path = " + mFile.getPath()); } - private void dispatchDownloadFailCallback(Throwable throwable) { + public void dispatchDownloadFailCallback(Throwable throwable) { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { mListener.onDownloadFail(mFile, throwable); mListener.onDownloadEnd(mFile); diff --git a/library/src/main/java/com/hjq/http/callback/NormalCallback.java b/library/src/main/java/com/hjq/http/callback/NormalCallback.java index 4d40cdd..9c0de19 100644 --- a/library/src/main/java/com/hjq/http/callback/NormalCallback.java +++ b/library/src/main/java/com/hjq/http/callback/NormalCallback.java @@ -11,7 +11,6 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; -import okhttp3.Call; import okhttp3.Response; import okhttp3.ResponseBody; @@ -64,7 +63,7 @@ public void start() { // 读取缓存成功 EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), () -> { - onStart(getCall()); + dispatchHttpStartCallback(); dispatchHttpSuccessCallback(result, true); }); @@ -88,7 +87,7 @@ public void start() { } @Override - protected void onStart(Call call) { + protected void onStart() { EasyUtils.runOnAssignThread(mHttpRequest.getThreadSchedulers(), this::dispatchHttpStartCallback); } @@ -153,7 +152,7 @@ protected void onHttpFailure(Throwable throwable) { private void dispatchHttpStartCallback() { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { - mListener.onHttpStart(getCall()); + mListener.onHttpStart(getCallProxy()); } EasyLog.printLog(mHttpRequest, "Http request start"); } @@ -161,7 +160,7 @@ private void dispatchHttpStartCallback() { private void dispatchHttpSuccessCallback(Object result, boolean cache) { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { mListener.onHttpSuccess(result, cache); - mListener.onHttpEnd(getCall()); + mListener.onHttpEnd(getCallProxy()); } EasyLog.printLog(mHttpRequest, "Http request success"); } @@ -169,7 +168,7 @@ private void dispatchHttpSuccessCallback(Object result, boolean cache) { private void dispatchHttpFailCallback(Throwable throwable) { if (mListener != null && HttpLifecycleManager.isLifecycleActive(mHttpRequest.getLifecycleOwner())) { mListener.onHttpFail(throwable); - mListener.onHttpEnd(getCall()); + mListener.onHttpEnd(getCallProxy()); } EasyLog.printLog(mHttpRequest, "Http request fail"); } diff --git a/library/src/main/java/com/hjq/http/config/IRequestHandler.java b/library/src/main/java/com/hjq/http/config/IRequestHandler.java index 03e2433..ffdfc50 100644 --- a/library/src/main/java/com/hjq/http/config/IRequestHandler.java +++ b/library/src/main/java/com/hjq/http/config/IRequestHandler.java @@ -4,7 +4,6 @@ import androidx.annotation.Nullable; import com.hjq.http.EasyUtils; import com.hjq.http.request.HttpRequest; -import java.io.File; import java.lang.reflect.Type; import okhttp3.Response; @@ -39,23 +38,6 @@ public interface IRequestHandler { @NonNull Throwable requestFail(@NonNull HttpRequest httpRequest, @NonNull Throwable e); - /** - * 下载开始 - * - * @param httpRequest 请求接口对象 - * @param file 下载的文件对象 - */ - default void downloadStart(@NonNull HttpRequest httpRequest, @NonNull File file) {} - - /** - * 下载成功 - * - * @param httpRequest 请求接口对象 - * @param response 响应对象 - * @param file 下载的文件对象 - */ - default void downloadSuccess(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull File file) throws Throwable {} - /** * 下载失败 * diff --git a/library/src/main/java/com/hjq/http/config/impl/RequestJsonBodyStrategy.java b/library/src/main/java/com/hjq/http/config/impl/RequestJsonBodyStrategy.java index 5cc0c00..b42a2de 100644 --- a/library/src/main/java/com/hjq/http/config/impl/RequestJsonBodyStrategy.java +++ b/library/src/main/java/com/hjq/http/config/impl/RequestJsonBodyStrategy.java @@ -23,6 +23,6 @@ public void addParams(HttpParams params, String key, Object value) { @Override public RequestBody createRequestBody(HttpRequest httpRequest, HttpParams params) { - return new JsonRequestBody(params.getParams()); + return new JsonRequestBody(params.getMap()); } } \ No newline at end of file diff --git a/library/src/main/java/com/hjq/http/model/CallProxy.java b/library/src/main/java/com/hjq/http/model/CallProxy.java index ff93722..eec5e5b 100644 --- a/library/src/main/java/com/hjq/http/model/CallProxy.java +++ b/library/src/main/java/com/hjq/http/model/CallProxy.java @@ -1,9 +1,7 @@ package com.hjq.http.model; import androidx.annotation.NonNull; - import java.io.IOException; - import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; @@ -18,57 +16,66 @@ */ public final class CallProxy implements Call { - private Call mCall; + private Call mRealCall; + + public CallProxy(@NonNull Call realCall) { + mRealCall = realCall; + } - public CallProxy(@NonNull Call call) { - mCall = call; + public void setRealCall(@NonNull Call call) { + mRealCall = call; } - public void setCall(@NonNull Call call) { - mCall = call; + public Call getRealCall() { + return mRealCall; } @NonNull @Override public Request request() { - return mCall.request(); + return mRealCall.request(); } @NonNull @Override public Response execute() throws IOException { - return mCall.execute(); + return mRealCall.execute(); } @Override public void enqueue(@NonNull Callback responseCallback) { - mCall.enqueue(responseCallback); + mRealCall.enqueue(responseCallback); } @Override public void cancel() { - mCall.cancel(); + mRealCall.cancel(); } @Override public boolean isExecuted() { - return mCall.isExecuted(); + return mRealCall.isExecuted(); } @Override public boolean isCanceled() { - return mCall.isCanceled(); + return mRealCall.isCanceled(); } @NonNull @Override public Timeout timeout() { - return mCall.timeout(); + return mRealCall.timeout(); } @NonNull @Override public Call clone() { - return mCall.clone(); + return mRealCall.clone(); + } + + public interface Factory { + + CallProxy create(); } } \ No newline at end of file diff --git a/library/src/main/java/com/hjq/http/model/FileContentResolver.java b/library/src/main/java/com/hjq/http/model/FileContentResolver.java index 3c1c337..4f99e6a 100644 --- a/library/src/main/java/com/hjq/http/model/FileContentResolver.java +++ b/library/src/main/java/com/hjq/http/model/FileContentResolver.java @@ -104,8 +104,10 @@ public InputStream openInputStream() throws FileNotFoundException { /** * 打开文件输出流 */ - public OutputStream openOutputStream() throws FileNotFoundException { - return mContentResolver.openOutputStream(mContentUri); + public OutputStream openOutputStream(boolean append) throws FileNotFoundException { + // w:写入模式,如果文件存在则覆盖,如果文件不存在则创建 + // wa:追加模式,如果文件存在则追加到文件末尾,如果文件不存在则创建 + return mContentResolver.openOutputStream(mContentUri, append ? "wa" : "w"); } @Override diff --git a/library/src/main/java/com/hjq/http/model/HttpHeaders.java b/library/src/main/java/com/hjq/http/model/HttpHeaders.java index 96ccf53..1dc255d 100644 --- a/library/src/main/java/com/hjq/http/model/HttpHeaders.java +++ b/library/src/main/java/com/hjq/http/model/HttpHeaders.java @@ -46,7 +46,7 @@ public Set getKeys() { return mHeaders.keySet(); } - public Map getHeaders() { + public Map getMap() { return mHeaders; } } \ No newline at end of file diff --git a/library/src/main/java/com/hjq/http/model/HttpParams.java b/library/src/main/java/com/hjq/http/model/HttpParams.java index 03e6507..d870e2f 100644 --- a/library/src/main/java/com/hjq/http/model/HttpParams.java +++ b/library/src/main/java/com/hjq/http/model/HttpParams.java @@ -49,7 +49,7 @@ public Set getKeys() { return mParams.keySet(); } - public Map getParams() { + public Map getMap() { return mParams; } diff --git a/library/src/main/java/com/hjq/http/request/BodyRequest.java b/library/src/main/java/com/hjq/http/request/BodyRequest.java index c6703f4..6edb6e0 100644 --- a/library/src/main/java/com/hjq/http/request/BodyRequest.java +++ b/library/src/main/java/com/hjq/http/request/BodyRequest.java @@ -5,7 +5,7 @@ import com.hjq.http.EasyConfig; import com.hjq.http.EasyLog; import com.hjq.http.EasyUtils; -import com.hjq.http.body.CustomTypeBody; +import com.hjq.http.body.CustomTypeRequestBody; import com.hjq.http.body.JsonRequestBody; import com.hjq.http.body.ProgressMonitorRequestBody; import com.hjq.http.body.TextRequestBody; @@ -191,9 +191,9 @@ private RequestBody createRequestBody(HttpParams params, @Nullable String conten if (contentType != null && !"".equals(contentType)) { MediaType mediaType = MediaType.parse(contentType); if (mediaType != null) { - CustomTypeBody customTypeBody = new CustomTypeBody(requestBody); - customTypeBody.setContentType(mediaType); - requestBody = customTypeBody; + CustomTypeRequestBody customTypeRequestBody = new CustomTypeRequestBody(requestBody); + customTypeRequestBody.setContentType(mediaType); + requestBody = customTypeRequestBody; } } diff --git a/library/src/main/java/com/hjq/http/request/DownloadRequest.java b/library/src/main/java/com/hjq/http/request/DownloadRequest.java index 8715d1a..2fb6d49 100644 --- a/library/src/main/java/com/hjq/http/request/DownloadRequest.java +++ b/library/src/main/java/com/hjq/http/request/DownloadRequest.java @@ -42,6 +42,9 @@ public final class DownloadRequest extends HttpRequest { /** 校验的 md5 */ private String mMd5; + /** 断点续传开关 */ + private boolean mResumableTransfer = false; + /** 下载监听回调 */ private OnDownloadListener mListener; @@ -112,6 +115,14 @@ public DownloadRequest md5(String md5) { return this; } + /** + * 是否支持断点续传 + */ + public DownloadRequest resumableTransfer(boolean enable) { + mResumableTransfer = enable; + return this; + } + /** * 设置下载监听 */ @@ -132,21 +143,35 @@ public DownloadRequest start() { StackTraceElement[] stackTrace = new Throwable().getStackTrace(); - EasyUtils.postDelayedRunnable(() -> { + Runnable runnable = () -> { + // 放到子线程中执行,避免占用主线程资源 if (!HttpLifecycleManager.isLifecycleActive(getLifecycleOwner())) { // 宿主已被销毁,请求无法进行 EasyLog.printLog(this, "LifecycleOwner has been destroyed and the request cannot be made"); return; } EasyLog.printStackTrace(this, stackTrace); - mCallProxy = new CallProxy(createCall()); - new DownloadCallback(this) - .setFile(mFile) - .setMd5(mMd5) - .setListener(mListener) - .setCall(mCallProxy) - .start(); - }, delayMillis); + + DownloadCallback downloadCallback = new DownloadCallback(this); + downloadCallback.setFile(mFile) + .setMd5(mMd5) + .setResumableTransfer(mResumableTransfer) + .setListener(mListener) + .setCallProxyFactory(() -> mCallProxy = new CallProxy(createCall())); + + // 如果本地有这个文件,并且校验通过,则回调下载成功 + if (downloadCallback.verifyFileMd5()) { + // 文件已存在,跳过请求 + EasyLog.printLog(this, mFile.getPath() + " download file already exists, skip download"); + // 回调相关的监听方法 + EasyUtils.runOnAssignThread(getThreadSchedulers(), downloadCallback::dispatchDownloadStartCallback); + EasyUtils.runOnAssignThread(getThreadSchedulers(), () -> downloadCallback.dispatchDownloadSuccessCallback(true)); + return; + } + downloadCallback.start(); + }; + + sendRunnable(runnable, delayMillis, getTag()); return this; } @@ -187,6 +212,10 @@ public String getRequestMethod() { @Override protected Request createRequest(String url, String tag, HttpParams params, HttpHeaders headers, IRequestBodyStrategy requestBodyStrategy) { + if (mResumableTransfer && mFile.isFile() && mFile.length() > 0) { + // 添加断点续传请求头 + headers.put("Range", "bytes=" + mFile.length() + "-"); + } // 这里设置 api 的目的是为了打日志的时候不崩溃,因为现在打日志需要 api 对象 return mRealRequest.api(getRequestApi()).createRequest(url, tag, params, headers, requestBodyStrategy); } diff --git a/library/src/main/java/com/hjq/http/request/HttpRequest.java b/library/src/main/java/com/hjq/http/request/HttpRequest.java index a991b50..c8704c6 100644 --- a/library/src/main/java/com/hjq/http/request/HttpRequest.java +++ b/library/src/main/java/com/hjq/http/request/HttpRequest.java @@ -207,7 +207,6 @@ public T schedulers(@NonNull ThreadSchedulers schedulers) { */ @NonNull protected Call createCall() { - IRequestBodyStrategy requestBodyStrategy = mRequestType.getBodyType(); HttpParams params = new HttpParams(); @@ -292,9 +291,6 @@ protected Call createCall() { if (mRequestInterceptor != null) { request = mRequestInterceptor.interceptRequest(this, request); } - if (request == null) { - throw new NullPointerException("The request object cannot be empty"); - } return mRequestClient.getOkHttpClient().newCall(request); } @@ -309,27 +305,24 @@ public void request(@Nullable OnHttpListener listener) { StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable runnable = () -> { - if (!HttpLifecycleManager.isLifecycleActive(mLifecycleOwner)) { - // 宿主已被销毁,请求无法进行 - EasyLog.printLog(HttpRequest.this, + // 放到子线程中执行,避免占用主线程资源 + EasyUtils.runOnAssignThread(ThreadSchedulers.IO, () -> { + if (!HttpLifecycleManager.isLifecycleActive(mLifecycleOwner)) { + // 宿主已被销毁,请求无法进行 + EasyLog.printLog(HttpRequest.this, "LifecycleOwner has been destroyed and the request cannot be made"); - return; - } - EasyLog.printStackTrace(HttpRequest.this, stackTrace); + return; + } + EasyLog.printStackTrace(HttpRequest.this, stackTrace); - mCallProxy = new CallProxy(createCall()); - new NormalCallback(HttpRequest.this) + new NormalCallback(HttpRequest.this) .setListener(listener) - .setCall(mCallProxy) + .setCallProxyFactory(() -> mCallProxy = new CallProxy(createCall())) .start(); + }); }; - if (mDelayMillis > 0) { - // issue 地址:https://github.com/getActivity/EasyHttp/issues/159 - int what = mTag == null ? Integer.MAX_VALUE : mTag.hashCode(); - EasyUtils.postDelayedRunnable(runnable, what, mDelayMillis); - } else { - runnable.run(); - } + + sendRunnable(runnable, mDelayMillis, mTag); } /** @@ -372,7 +365,14 @@ public Bean execute(ResponseClass responseClass) throws Throwable { if (cacheMode == CacheMode.USE_CACHE_FIRST) { // 使用异步请求来刷新缓存 new NormalCallback(this) - .setCall(mCallProxy) + .setCallProxyFactory(() -> { + // 如果存在缓存的情况下,则后面的逻辑不会继续请求,可以直接使用 CallProxy 对象字段 + // 如果不存在缓存的话,则重新 new 一个 CallProxy 对象,这是因为后面的逻辑会请重新发起网络请求 + if (cacheResult != null) { + return mCallProxy; + } + return new CallProxy(createCall()) ; + }) .start(); } if (cacheResult != null) { @@ -522,6 +522,23 @@ public ThreadSchedulers getThreadSchedulers() { return mThreadSchedulers; } + /** + * 发送一个任务 + * + * @param runnable 任务对象 + * @param delayMillis 延迟时间 + * @param tag 任务标记 + */ + protected void sendRunnable(Runnable runnable, long delayMillis, String tag) { + if (delayMillis > 0) { + // issue 地址:https://github.com/getActivity/EasyHttp/issues/159 + int what = tag == null ? Integer.MAX_VALUE : tag.hashCode(); + EasyUtils.postDelayedRunnable(runnable, what, delayMillis); + } else { + runnable.run(); + } + } + /** * 获取延迟请求时间 */ @@ -567,6 +584,7 @@ protected void addHttpHeaders(HttpHeaders headers, String key, Object value) { /** * 创建请求的对象 */ + @NonNull protected Request createRequest(String url, String tag, HttpParams params, HttpHeaders headers, IRequestBodyStrategy requestBodyStrategy) { Request.Builder requestBuilder = createRequestBuilder(url, tag); addRequestHeader(requestBuilder, headers); @@ -582,7 +600,8 @@ protected Request createRequest(String url, String tag, HttpParams params, HttpH /** * 创建请求构建对象 */ - public Request.Builder createRequestBuilder(String url, String tag) { + @NonNull + protected Request.Builder createRequestBuilder(String url, String tag) { Request.Builder requestBuilder = new Request.Builder(); requestBuilder.url(url); if (tag != null) {