Quellcode durchsuchen

第一次提交

sunqiang vor 1 Jahr
Commit
405caa1cee
100 geänderte Dateien mit 6115 neuen und 0 gelöschten Zeilen
  1. 10 0
      .gitignore
  2. 1 0
      HttpCoreLibrary/.gitignore
  3. 51 0
      HttpCoreLibrary/build.gradle
  4. 0 0
      HttpCoreLibrary/consumer-rules.pro
  5. 21 0
      HttpCoreLibrary/proguard-rules.pro
  6. 5 0
      HttpCoreLibrary/src/main/AndroidManifest.xml
  7. 80 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/HttpClient.kt
  8. 19 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/HttpConfig.kt
  9. 390 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/client/ApiRepository.kt
  10. 204 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/client/ExamClient.kt
  11. 9 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/client/factory/ClientFactory.kt
  12. 9 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/client/factory/RetrofitFactory.kt
  13. 278 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/client/retrofit/ApiService.java
  14. 546 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/client/retrofit/ExamRetrofit.kt
  15. 16 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/config/ConfigCore.kt
  16. 27 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/config/ConfigFactory.kt
  17. 68 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/config/ConfigParam.kt
  18. 30 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/config/OkHttpDNS.kt
  19. 22 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/converter/NullOnEmptyConverterFactory.kt
  20. 9 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/exception/NetException.kt
  21. 50 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/TokenHeaderInterceptor.kt
  22. 26 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/formatter/GsonFormatter.kt
  23. 37 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/formatter/JSONFormatter.kt
  24. 26 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/formatter/OrgJsonFormatter.kt
  25. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/net/DownloadListener.kt
  26. 156 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/net/DownloadTask.kt
  27. 5 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/CommonDataResponse.kt
  28. 6 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/CommonListResponse.kt
  29. 17 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/CommonResponse.kt
  30. 46 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/LogInterceptor.java
  31. 94 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/LogUtil.java
  32. 16 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/AccessTokenReq.java
  33. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/AssessRecordReq.java
  34. 16 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/CourseListReq.java
  35. 20 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ExamAnswerReq.java
  36. 13 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ExamLearnReq.java
  37. 15 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ExamScoreReq.java
  38. 13 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/FaceCompareReq.java
  39. 12 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/HandPaperReq.java
  40. 12 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/HandPractiseReq.java
  41. 17 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/LearnLoginReq.java
  42. 19 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/LearnRecordReq.java
  43. 20 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/MockAnswerReq.java
  44. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/MockTestReq.java
  45. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/PaperQuReq.java
  46. 16 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ViolationReq.java
  47. 22 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/AccessRecordBean.java
  48. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/AccessTokenResp.java
  49. 13 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ApkInfoResp.java
  50. 12 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/BannerImageBean.java
  51. 23 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/CategoryTree.java
  52. 28 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/CertVo.java
  53. 170 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ExamCourseVo.java
  54. 113 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ExamTopic.java
  55. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ExamVerify.java
  56. 22 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/HandPaperBean.java
  57. 20 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/HandPractiseBean.java
  58. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/LearnBonusBean.java
  59. 19 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/LearnLoginVo.java
  60. 62 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/LearnRecordVo.java
  61. 50 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/MockTestVo.java
  62. 14 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/TopicClassifyVo.java
  63. 22 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/UserInfo.java
  64. 22 0
      HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ViolationBean.java
  65. 8 0
      LICENSE
  66. 3 0
      README.md
  67. 1 0
      RcCore/.gitignore
  68. 53 0
      RcCore/build.gradle
  69. 0 0
      RcCore/consumer-rules.pro
  70. BIN
      RcCore/libs/tbs_sdk_v4.3.0.165_20210628_103707.jar
  71. 21 0
      RcCore/proguard-rules.pro
  72. 5 0
      RcCore/src/main/AndroidManifest.xml
  73. 8 0
      RcCore/src/main/java/com/rc/core/event/RefreshEvent.kt
  74. 27 0
      RcCore/src/main/java/com/rc/core/log/RcLog.kt
  75. 71 0
      RcCore/src/main/java/com/rc/core/ui/ActivityCollector.kt
  76. 106 0
      RcCore/src/main/java/com/rc/core/ui/activity/RcBaseActivity.kt
  77. 235 0
      RcCore/src/main/java/com/rc/core/ui/activity/RcRefreshActivity.kt
  78. 36 0
      RcCore/src/main/java/com/rc/core/ui/common/AbsUIDelegate.kt
  79. 25 0
      RcCore/src/main/java/com/rc/core/ui/common/IUIListener.kt
  80. 93 0
      RcCore/src/main/java/com/rc/core/ui/common/UIDelegateImpl.kt
  81. 101 0
      RcCore/src/main/java/com/rc/core/ui/dialog/LoadingDialog.kt
  82. 112 0
      RcCore/src/main/java/com/rc/core/ui/dialog/RcBaseDialog.kt
  83. 83 0
      RcCore/src/main/java/com/rc/core/ui/fragment/RcBaseFragment.kt
  84. 37 0
      RcCore/src/main/java/com/rc/core/ui/fragment/RcLazyFragment.kt
  85. 225 0
      RcCore/src/main/java/com/rc/core/ui/fragment/RcRefreshFragment.kt
  86. 301 0
      RcCore/src/main/java/com/rc/core/ui/widget/MultipleStatusView.kt
  87. 53 0
      RcCore/src/main/java/com/rc/core/ui/widget/decoration/GridSpacingItemDecoration.java
  88. 148 0
      RcCore/src/main/java/com/rc/core/ui/widget/decoration/NoLastLineItemDecoration.java
  89. 195 0
      RcCore/src/main/java/com/rc/core/util/ApkController.kt
  90. 61 0
      RcCore/src/main/java/com/rc/core/util/ApkUpdater.kt
  91. 208 0
      RcCore/src/main/java/com/rc/core/util/CrashHandler.kt
  92. 120 0
      RcCore/src/main/java/com/rc/core/util/CrashHelper.kt
  93. 102 0
      RcCore/src/main/java/com/rc/core/util/DateUtils.kt
  94. 91 0
      RcCore/src/main/java/com/rc/core/util/DeviceUtils.kt
  95. 68 0
      RcCore/src/main/java/com/rc/core/util/EscapeUnescape.java
  96. 13 0
      RcCore/src/main/java/com/rc/core/util/Extension.kt
  97. 58 0
      RcCore/src/main/java/com/rc/core/util/Format.kt
  98. 162 0
      RcCore/src/main/java/com/rc/core/util/MediaUtils.kt
  99. 128 0
      RcCore/src/main/java/com/rc/core/util/ScreenAdapter.kt
  100. 0 0
      RcCore/src/main/java/com/rc/core/util/VideoFullScreenWebChromeClient.kt

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+*.iml
+/.gradle
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/gradle-build

+ 1 - 0
HttpCoreLibrary/.gitignore

@@ -0,0 +1 @@
+/build

+ 51 - 0
HttpCoreLibrary/build.gradle

@@ -0,0 +1,51 @@
+plugins {
+    id 'com.android.library'
+    id 'kotlin-android'
+}
+
+android {
+    compileSdkVersion env.compileSdkVersion
+    buildToolsVersion env.buildToolsVersion
+
+    defaultConfig {
+        minSdkVersion env.minSdkVersion
+        targetSdkVersion env.targetSdkVersion
+        versionCode 1
+        versionName "1.0"
+
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility env.jdk_version
+        targetCompatibility env.jdk_version
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+}
+
+dependencies {
+
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation dep.kotlinStdlib
+    implementation dep.androidxCoreKtx
+    implementation dep.androidxLocalbroadcastmanager
+
+    api dep.retrofit
+    implementation dep.converterGson
+    implementation dep.converterScalars
+    implementation dep.rxJavaAdapter
+    implementation dep.okhttp3Logs
+    api dep.rxJava
+    api dep.rxAndroid
+    api dep.gson
+
+//    implementation dep.luban
+}

+ 0 - 0
HttpCoreLibrary/consumer-rules.pro


+ 21 - 0
HttpCoreLibrary/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 5 - 0
HttpCoreLibrary/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.rc.httpcore">
+
+</manifest>

+ 80 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/HttpClient.kt

@@ -0,0 +1,80 @@
+package com.rc.httpcore
+
+import android.content.Context
+import android.util.Log
+import com.rc.httpcore.client.factory.ClientFactory
+import com.rc.httpcore.client.factory.RetrofitFactory
+import com.rc.httpcore.config.OkHttpDNS
+import com.rc.httpcore.converter.NullOnEmptyConverterFactory
+import com.rc.httpcore.interceptor.TokenHeaderInterceptor
+import com.rc.httpcore.vo.LogInterceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+import java.util.logging.Level
+import java.util.logging.Logger
+
+object HttpClient {
+
+    private const val TIMEOUT_DEFAULT = 3 * 60L
+
+    private var mAppContext: Context? = null
+    private val LOGGER: Logger = Logger.getLogger(HttpClient::class.java.name)
+    var token: String?=null
+    var vName: String = "1.0.0"
+
+
+    fun init(appContext: Context) {
+        this.mAppContext = appContext
+    }
+
+    fun getAppContext() = mAppContext
+
+    fun createClientFactory(): ClientFactory {
+        return when (HttpConfig.HTTP_STRATEGY_Retrofit) {
+            HttpConfig.HTTP_STRATEGY -> RetrofitFactory()
+            else -> RetrofitFactory()
+        }
+    }
+
+    fun <T> createRetrofitApi(
+        apiClass: Class<T>,
+        baseUrl: String = HttpConfig.API_BASE_URL,
+    ): T {
+        return buildRetrofit(baseUrl).create(apiClass)
+    }
+
+    private fun buildRetrofit(
+        baseUrl: String = HttpConfig.API_BASE_URL,
+        okHttpClient: OkHttpClient = buildHttpClient()
+    ): Retrofit {
+        return Retrofit.Builder()
+            .client(okHttpClient)
+            .baseUrl(baseUrl)
+            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+            .addConverterFactory(NullOnEmptyConverterFactory())
+            .addConverterFactory(GsonConverterFactory.create())
+            .build()
+    }
+
+    private fun buildHttpClient(): OkHttpClient {
+//        //设置日志打印级别
+//        val interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message ->
+//            LOGGER.log(Level.INFO, message)
+//        })
+//        interceptor.level = HttpLoggingInterceptor.Level.BODY
+        return OkHttpClient.Builder()
+            .readTimeout(TIMEOUT_DEFAULT, TimeUnit.SECONDS) // 设置读取超时时间
+            .connectTimeout(TIMEOUT_DEFAULT, TimeUnit.SECONDS) // 设置请求超时时间
+            .writeTimeout(TIMEOUT_DEFAULT, TimeUnit.SECONDS) // 设置写入超时时间
+            .addNetworkInterceptor(TokenHeaderInterceptor())
+            .addInterceptor(LogInterceptor())//添加请求日志
+            .retryOnConnectionFailure(true) // 设置出现错误进行重新连接
+//            .dns(OkHttpDNS())
+            .build()
+    }
+
+}

+ 19 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/HttpConfig.kt

@@ -0,0 +1,19 @@
+package com.rc.httpcore
+
+class HttpConfig {
+
+    companion object {
+
+        //var API_BASE_URL = "http://lab.zjznai.com/labSystem/"
+        //var API_BASE_URL = "http://pc44sory.xiaomy.net:31738/"
+//        var API_BASE_URL = "http://192.168.251.2/labSystem/"
+        var API_BASE_URL = "http://192.168.1.9:8080/"
+
+
+        var FILE_BROWSER_BASE_URL = "http://192.168.251.2:8012/"
+
+        const val HTTP_STRATEGY_Retrofit = 1
+
+        var HTTP_STRATEGY = HTTP_STRATEGY_Retrofit
+    }
+}

+ 390 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/ApiRepository.kt

@@ -0,0 +1,390 @@
+package com.rc.httpcore.client
+
+import com.rc.httpcore.HttpClient
+import com.rc.httpcore.vo.CommonListResponse
+import com.rc.httpcore.vo.request.*
+import com.rc.httpcore.vo.response.*
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+
+object ApiRepository {
+
+    private val mClientFactory by lazy { HttpClient.createClientFactory() }
+
+    /**
+     * 登录获取token
+     */
+    fun authOneLogin(): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .authOneLogin()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 查询APK版本
+     *
+     * @param id 设备唯一编码
+     */
+    fun apkVersion(id: String): Observable<ApkInfoResp> {
+        return mClientFactory.createLabClient()
+            .apkVersion(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 上传APK更新状态
+     *
+     * @param state 0:升级失败; 1:升级成功; 2:升级中
+     */
+    fun onepcApkUpdate(id: String, state: String): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .onepcApkUpdate(id, state)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 查询轮播图
+     *
+     * @param id 设备唯一编码
+     */
+    fun bannerImages(id: String): Observable<List<BannerImageBean>> {
+        return mClientFactory.createLabClient()
+            .bannerImages(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 人脸比对
+     */
+    fun faceCompare(param: FaceCompareReq): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .faceCompare(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 学习一体机 用户端登录
+     *
+     * @param username 刷学生卡数据
+     * @param androidId 设备唯一码
+     * @param realLogin false:第一次登录,获取token;ture:第二次登录,真正的登录
+     */
+    fun learnLogin(username: String, androidId: String, realLogin: Boolean = false,isLogins:String): Observable<LearnLoginVo> {
+        val param = LearnLoginReq().apply {
+            userName = username
+            type = if (realLogin) "2" else "1"
+            machineCode = androidId
+            isLogin=isLogins
+        }
+        return mClientFactory.createLabClient()
+            .learnLogin(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    fun loginToken(username: String, androidId: String): Observable<LearnLoginVo> {
+        val param = LearnLoginReq().apply {
+            userName = username
+            machineCode = androidId
+
+        }
+        return mClientFactory.createLabClient()
+            .loginToken(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 安全学习-学习分类
+     */
+    fun categoryTreeList(): Observable<List<CategoryTree>> {
+        return mClientFactory.createLabClient()
+            .categoryTreeList()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 安全学习-查询学习资源列表
+     */
+    fun examCourseList(param: CourseListReq): Observable<CommonListResponse<ExamCourseVo>> {
+        return mClientFactory.createLabClient()
+            .examCourseList(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 安全学习-获取学习资源详细信息
+     */
+    fun examCourseDetail(id: String): Observable<ExamCourseVo> {
+        return mClientFactory.createLabClient()
+            .examCourseDetail(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 安全学习-开始学习
+     */
+    fun examLearnStart(param: ExamLearnReq): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .examLearnStart(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 安全学习-开始学习
+     */
+    fun examLearnFinish(param: ExamLearnReq): Observable<LearnBonusBean> {
+        return mClientFactory.createLabClient()
+            .examLearnFinish(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 安全学习-个人学习记录
+     */
+    fun examLearnRecord(param: LearnRecordReq): Observable<CommonListResponse<LearnRecordVo>> {
+        return mClientFactory.createLabClient()
+            .examLearnRecord(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 查询题目类型下拉框
+     */
+    fun classifyQueryOption(): Observable<List<TopicClassifyVo>> {
+        return mClientFactory.createLabClient()
+            .classifyQueryOption()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 查询模拟列表
+     */
+    fun onlineList(param: MockTestReq): Observable<CommonListResponse<MockTestVo>> {
+        return mClientFactory.createLabClient()
+            .onlineList(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 开始-查询考试前后否需要人脸验证
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    fun examStartVerify(type: String, examId: String = "-1"): Observable<ExamVerify> {
+        return mClientFactory.createLabClient()
+            .examStartVerify(type, examId)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 开始考试-创建考卷
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    fun examStart(type: String, examId: String = "-1"): Observable<ExamTopic> {
+        return mClientFactory.createLabClient()
+            .examStart(type, examId)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 考试 保存用户答案 一题一交
+     */
+    fun examFillAnswer(param: ExamAnswerReq): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .examFillAnswer(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 交卷- 完成考试
+     */
+    fun examHandPaper(param: HandPaperReq): Observable<HandPaperBean> {
+        return mClientFactory.createLabClient()
+            .examHandPaper(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 个人考试成绩列表查询
+     */
+    fun paperMyList(param: ExamScoreReq): Observable<CommonListResponse<ExamTopic>> {
+        return mClientFactory.createLabClient()
+            .paperMyList(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 个人考试成绩详情查询
+     */
+    fun paperDetail(id: String): Observable<ExamTopic> {
+        return mClientFactory.createLabClient()
+            .paperDetail(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 考试 查询问题和选项 - 一题一查
+     */
+    fun paperExamQuDetail(param: PaperQuReq): Observable<ExamTopic.ElPaperQu> {
+        return mClientFactory.createLabClient()
+            .paperExamQuDetail(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 查询我的证书列表
+     */
+    fun queryMyCert(pageNum: Int, pageSize: Int): Observable<CommonListResponse<CertVo>> {
+        return mClientFactory.createLabClient()
+            .queryMyCert(pageNum, pageSize)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 模拟练习-开始练习
+     */
+    fun mockTestStart(ids: String): Observable<ExamTopic> {
+        return mClientFactory.createLabClient()
+            .mockTestStart(ids)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 练习 查询问题和选项 - 一题一查
+     */
+    fun paperMockQuDetail(param: PaperQuReq): Observable<ExamTopic.ElPaperQu> {
+        return mClientFactory.createLabClient()
+            .paperMockQuDetail(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 练习 保存用户答案 - 一题一交
+     */
+    fun mockAnswer(param: MockAnswerReq): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .mockAnswer(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 结束练习
+     */
+    fun handPractise(param: HandPractiseReq): Observable<HandPractiseBean> {
+        return mClientFactory.createLabClient()
+            .handPractise(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 个人中心-用户信息
+     */
+    fun queryUserInfo(): Observable<UserInfo> {
+        return mClientFactory.createLabClient()
+            .queryUserInfo()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 个人中心-违规信息
+     */
+    fun queryViolationList(param: ViolationReq): Observable<List<ViolationBean>> {
+        return mClientFactory.createLabClient()
+            .queryViolationList(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 是否有待处理的违规
+     */
+    fun existTodoViolation(): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .existTodoViolation()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 开始考核-生成考核考卷
+     * @param id 章节ID
+     */
+    fun classTestStart(id: String): Observable<ExamTopic> {
+        return mClientFactory.createLabClient()
+            .classTestStart(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 结束考核
+     * @param id 考核ID(试卷ID)
+     */
+    fun handAssess(id: String): Observable<HandPractiseBean> {
+        return mClientFactory.createLabClient()
+            .handAssess(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 考核记录列表
+     */
+    fun classTestRecord(param: AssessRecordReq): Observable<CommonListResponse<AccessRecordBean>> {
+        return mClientFactory.createLabClient()
+            .classTestRecord(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 考核详情
+     * @param id 考核ID(试卷ID)
+     */
+    fun classTestDetail(id: String): Observable<ExamTopic> {
+        return mClientFactory.createLabClient()
+            .classTestDetail(id)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * 退出登录
+     */
+    fun loginOut(): Observable<Boolean> {
+        return mClientFactory.createLabClient()
+            .loginOut()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+}

+ 204 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/ExamClient.kt

@@ -0,0 +1,204 @@
+package com.rc.httpcore.client
+
+import com.rc.httpcore.vo.CommonDataResponse
+import com.rc.httpcore.vo.CommonListResponse
+import com.rc.httpcore.vo.CommonResponse
+import com.rc.httpcore.vo.request.*
+import com.rc.httpcore.vo.response.*
+import io.reactivex.Observable
+
+interface ExamClient {
+
+    /**
+     * 登录获取token
+     */
+    fun authOneLogin(): Observable<Boolean>
+
+    /**
+     * 查询APK版本
+     *
+     * @param id 设备唯一编码
+     */
+    fun apkVersion(id: String): Observable<ApkInfoResp>
+
+    /**
+     * 上传APK更新状态
+     *
+     * @param state 0:升级失败; 1:升级成功; 2:升级中
+     */
+    fun onepcApkUpdate(id: String, state: String): Observable<Boolean>
+
+    /**
+     * 查询轮播图
+     *
+     * @param id 设备唯一编码
+     */
+    fun bannerImages(id: String): Observable<List<BannerImageBean>>
+
+    /**
+     * 人脸比对
+     */
+    fun faceCompare(param: FaceCompareReq): Observable<Boolean>
+
+    /**
+     * 学习一体机 用户端登录
+     *
+     * @param param 刷学生卡数据 userName
+     */
+    fun learnLogin(param: LearnLoginReq): Observable<LearnLoginVo>
+
+    /**
+     * 获取token
+     *
+     * @param param 刷学生卡数据 userName
+     */
+    fun loginToken(param: LearnLoginReq): Observable<LearnLoginVo>
+
+    /**
+     * 安全学习-学习分类
+     */
+    fun categoryTreeList(): Observable<List<CategoryTree>>
+
+    /**
+     * 安全学习-查询学习资源列表
+     */
+    fun examCourseList(param: CourseListReq): Observable<CommonListResponse<ExamCourseVo>>
+
+    /**
+     * 安全学习-获取学习资源详细信息
+     */
+    fun examCourseDetail(id: String): Observable<ExamCourseVo>
+
+    /**
+     * 安全学习-开始学习
+     */
+    fun examLearnStart(param: ExamLearnReq): Observable<Boolean>
+
+    /**
+     * 安全学习-开始学习
+     */
+    fun examLearnFinish(param: ExamLearnReq): Observable<LearnBonusBean>
+
+    /**
+     * 安全学习-个人学习记录
+     */
+    fun examLearnRecord(param: LearnRecordReq): Observable<CommonListResponse<LearnRecordVo>>
+
+    /**
+     * 查询题目类型下拉框
+     */
+    fun classifyQueryOption(): Observable<List<TopicClassifyVo>>
+
+    /**
+     * 查询模拟列表
+     */
+    fun onlineList(param: MockTestReq): Observable<CommonListResponse<MockTestVo>>
+
+    /**
+     * 开始-查询考试前后否需要人脸验证
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    fun examStartVerify(type: String, examId: String = "-1"): Observable<ExamVerify>
+
+    /**
+     * 开始考试-创建考卷
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    fun examStart(type: String, examId: String = "-1"): Observable<ExamTopic>
+
+    /**
+     * 考试 保存用户答案 一题一交
+     */
+    fun examFillAnswer(param: ExamAnswerReq): Observable<Boolean>
+
+    /**
+     * 交卷- 完成考试
+     */
+    fun examHandPaper(param: HandPaperReq): Observable<HandPaperBean>
+
+    /**
+     * 个人考试成绩列表查询
+     */
+    fun paperMyList(param: ExamScoreReq): Observable<CommonListResponse<ExamTopic>>
+
+    /**
+     * 个人考试成绩详情查询
+     */
+    fun paperDetail(id: String): Observable<ExamTopic>
+
+    /**
+     * 考试 查询问题和选项 - 一题一查
+     */
+    fun paperExamQuDetail(param: PaperQuReq): Observable<ExamTopic.ElPaperQu>
+
+    /**
+     * 查询我的证书列表
+     */
+    fun queryMyCert(pageNum: Int, pageSize: Int): Observable<CommonListResponse<CertVo>>
+
+    /**
+     * 模拟练习-开始练习
+     */
+    fun mockTestStart(ids: String): Observable<ExamTopic>
+
+    /**
+     * 练习 查询问题和选项 - 一题一查
+     */
+    fun paperMockQuDetail(param: PaperQuReq): Observable<ExamTopic.ElPaperQu>
+
+    /**
+     * 练习 保存用户答案 - 一题一交
+     */
+    fun mockAnswer(param: MockAnswerReq): Observable<Boolean>
+
+    /**
+     * 结束练习
+     */
+    fun handPractise(param: HandPractiseReq): Observable<HandPractiseBean>
+
+    /**
+     * 个人中心-用户信息
+     */
+    fun queryUserInfo(): Observable<UserInfo>
+
+    /**
+     * 个人中心-违规信息
+     */
+    fun queryViolationList(param: ViolationReq): Observable<List<ViolationBean>>
+
+    /**
+     * 是否有待处理的违规
+     */
+    fun existTodoViolation(): Observable<Boolean>
+
+    /**
+     * 开始考核-生成考核考卷
+     * @param id 章节ID
+     */
+    fun classTestStart(id: String): Observable<ExamTopic>
+
+    /**
+     * 结束考核
+     * @param id 考核ID(试卷ID)
+     */
+    fun handAssess(id: String): Observable<HandPractiseBean>
+
+    /**
+     * 考核记录列表
+     */
+    fun classTestRecord(param: AssessRecordReq): Observable<CommonListResponse<AccessRecordBean>>
+
+    /**
+     * 考核详情
+     * @param id 考核ID(试卷ID)
+     */
+    fun classTestDetail(id: String): Observable<ExamTopic>
+
+    /**
+     * 退出登录
+     */
+    fun loginOut(): Observable<Boolean>
+
+}

+ 9 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/factory/ClientFactory.kt

@@ -0,0 +1,9 @@
+package com.rc.httpcore.client.factory
+
+import com.rc.httpcore.client.ExamClient
+
+interface ClientFactory {
+
+    fun createLabClient(): ExamClient
+
+}

+ 9 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/factory/RetrofitFactory.kt

@@ -0,0 +1,9 @@
+package com.rc.httpcore.client.factory
+
+import com.rc.httpcore.client.retrofit.ExamRetrofit
+
+class RetrofitFactory : ClientFactory {
+
+    override fun createLabClient() = ExamRetrofit()
+
+}

+ 278 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/retrofit/ApiService.java

@@ -0,0 +1,278 @@
+package com.rc.httpcore.client.retrofit;
+
+import com.rc.httpcore.vo.CommonDataResponse;
+import com.rc.httpcore.vo.CommonListResponse;
+import com.rc.httpcore.vo.CommonResponse;
+import com.rc.httpcore.vo.request.AccessTokenReq;
+import com.rc.httpcore.vo.request.ExamAnswerReq;
+import com.rc.httpcore.vo.request.ExamLearnReq;
+import com.rc.httpcore.vo.request.ExamScoreReq;
+import com.rc.httpcore.vo.request.FaceCompareReq;
+import com.rc.httpcore.vo.request.HandPaperReq;
+import com.rc.httpcore.vo.request.HandPractiseReq;
+import com.rc.httpcore.vo.request.LearnLoginReq;
+import com.rc.httpcore.vo.request.LearnRecordReq;
+import com.rc.httpcore.vo.request.MockAnswerReq;
+import com.rc.httpcore.vo.request.PaperQuReq;
+import com.rc.httpcore.vo.response.AccessRecordBean;
+import com.rc.httpcore.vo.response.AccessTokenResp;
+import com.rc.httpcore.vo.response.ApkInfoResp;
+import com.rc.httpcore.vo.response.BannerImageBean;
+import com.rc.httpcore.vo.response.CategoryTree;
+import com.rc.httpcore.vo.response.CertVo;
+import com.rc.httpcore.vo.response.ExamCourseVo;
+import com.rc.httpcore.vo.response.ExamTopic;
+import com.rc.httpcore.vo.response.ExamVerify;
+import com.rc.httpcore.vo.response.HandPaperBean;
+import com.rc.httpcore.vo.response.HandPractiseBean;
+import com.rc.httpcore.vo.response.LearnBonusBean;
+import com.rc.httpcore.vo.response.LearnLoginVo;
+import com.rc.httpcore.vo.response.LearnRecordVo;
+import com.rc.httpcore.vo.response.MockTestVo;
+import com.rc.httpcore.vo.response.TopicClassifyVo;
+import com.rc.httpcore.vo.response.UserInfo;
+import com.rc.httpcore.vo.response.ViolationBean;
+
+import java.util.List;
+import java.util.Map;
+
+import io.reactivex.Observable;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+import retrofit2.http.QueryMap;
+
+public interface ApiService {
+
+    /**
+     * 登录获取token
+     */
+    @POST("auth/one/login")
+    Observable<CommonDataResponse<AccessTokenResp>> authOneLogin(@Body AccessTokenReq param);
+
+    /**
+     * 查询APK版本
+     */
+    @GET("laboratory/apkfile/onepcApkDetail/{id}")
+    Observable<CommonDataResponse<ApkInfoResp>> apkVersion(@Path("id") String id);
+
+    /**
+     * 上传APK更新状态
+     *
+     * @param state 0:升级失败; 1:升级成功; 2:升级中
+     */
+    @PUT("laboratory/apkfile/onepcApkUpdate/{id}/{state}")
+    Observable<CommonResponse> onepcApkUpdate(@Path("id") String id, @Path("state") String state);
+
+    /**
+     * 查询轮播图
+     */
+    @GET("laboratory/onemachine/rotation/chart")
+    Observable<CommonDataResponse<List<BannerImageBean>>> bannerImages(@Query("hardwareNum") String hardwareNum);
+
+    /**
+     * 人脸比对
+     */
+    @POST("algorithm/faceApi/compare")
+    Observable<CommonDataResponse<String>> faceCompare(@Body FaceCompareReq param);
+
+    /**
+     * 学习一体机 用户端登录
+     */
+    @POST("auth/learn/login")
+    Observable<CommonDataResponse<LearnLoginVo>> learnLogin(@Body LearnLoginReq param);
+
+    /**
+     * 学习一体机 用户端登录
+     */
+    @POST("auth/learn/getToken")
+    Observable<CommonDataResponse<LearnLoginVo>> loginToken(@Body LearnLoginReq param);
+
+    /**
+     * 安全学习-学习分类
+     */
+    @GET("exam/api/category/treeList/{id}")
+    Observable<CommonDataResponse<List<CategoryTree>>> categoryTreeList(@Path("id") String id);
+
+    /**
+     * 安全学习-查询学习资源列表
+     */
+    @GET("exam/api/course/list")
+    Observable<CommonListResponse<ExamCourseVo>> examCourseList(@QueryMap Map<String, Object> filters);
+
+    /**
+     * 安全学习-获取学习资源详细信息
+     */
+    @GET("exam/api/course/{id}")
+    Observable<CommonDataResponse<ExamCourseVo>> examCourseDetail(@Path("id") String id);
+
+    /**
+     * 安全学习-开始学习
+     */
+    @POST("exam/api/course/learn/start")
+    Observable<CommonResponse> examLearnStart(@Body ExamLearnReq param);
+
+    /**
+     * 安全学习-完成学习
+     */
+    @POST("exam/api/course/learn/finish")
+    Observable<CommonDataResponse<LearnBonusBean>> examLearnFinish(@Body ExamLearnReq param);
+
+    /**
+     * 安全学习-个人学习记录
+     */
+    @POST("exam/api/course/learn/list")
+    Observable<CommonListResponse<LearnRecordVo>> examLearnRecord(@Query("pageNum") int pageNum, @Query("pageSize") int pageSize, @Body LearnRecordReq param);
+
+    /**
+     * 查询题目类型下拉框
+     */
+    @POST("exam/api/classify/queryOption")
+    Observable<CommonDataResponse<List<TopicClassifyVo>>> classifyQueryOption(@Body Object obj);
+
+    /**
+     * 查询模拟列表
+     */
+    @GET("exam/api/exam/onlineList")
+    Observable<CommonListResponse<MockTestVo>> onlineList(@Query("pageNum") int pageNum, @Query("pageSize") int pageSize, @Query("classifyIds") String classifyIds);
+
+    /**
+     * 开始-查询考试前后否需要人脸验证
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    @GET("exam/api/exam/start/verify/{type}")
+    Observable<CommonDataResponse<ExamVerify>> examStartVerify(@Path("type") String type, @Query("examId") String examId);
+
+    /**
+     * 开始考试-创建考卷
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    @GET("exam/api/exam/start/{type}")
+    Observable<CommonDataResponse<ExamTopic>> examStart(@Path("type") String type, @Query("examId") String examId);
+
+    /**
+     * 考试 保存用户答案 一题一交
+     */
+    @POST("exam/api/exam/fillAnswer")
+    Observable<CommonResponse> examFillAnswer(@Body ExamAnswerReq param);
+
+    /**
+     * 交卷- 完成考试
+     */
+    @POST("exam/api/exam/handPaper")
+    Observable<CommonDataResponse<HandPaperBean>> examHandPaper(@Body HandPaperReq param);
+
+    /**
+     * 个人考试成绩列表查询
+     */
+    @POST("exam/api/paper/myList")
+    Observable<CommonListResponse<ExamTopic>> paperMyList(@Query("pageNum") int pageNum, @Query("pageSize") int pageSize, @Body ExamScoreReq param);
+
+    /**
+     * 个人考试成绩详情查询
+     */
+    @GET("exam/api/paper/{id}")
+    Observable<CommonDataResponse<ExamTopic>> paperDetail(@Path("id") String id);
+
+    /**
+     * 考试 查询问题和选项 - 一题一查
+     */
+    @POST("exam/api/exam/paperDetail")
+    Observable<CommonDataResponse<ExamTopic.ElPaperQu>> paperExamQuDetail(@Body PaperQuReq param);
+
+    /**
+     * 查询我的证书列表
+     */
+    @POST("exam/api/queryMyCert")
+    Observable<CommonListResponse<CertVo>> queryMyCert(@Query("pageNum") int pageNum, @Query("pageSize") int pageSize, @Body Object obj);
+
+    /**
+     * 模拟练习-开始练习
+     */
+    @GET("exam/api/practise/start/{ids}")
+    Observable<CommonDataResponse<ExamTopic>> mockTestStart(@Path("ids") String ids);
+
+    /**
+     * 练习 查询问题和选项 - 一题一查
+     */
+    @POST("exam/api/practise/paperDetail")
+    Observable<CommonDataResponse<ExamTopic.ElPaperQu>> paperMockQuDetail(@Body PaperQuReq param);
+
+    /**
+     * 练习 保存用户答案 - 一题一交
+     */
+    @POST("exam/api/practise/fillAnswer")
+    Observable<CommonResponse> mockAnswer(@Body MockAnswerReq param);
+
+    /**
+     * 结束练习
+     */
+    @POST("exam/api/practise/handPractise")
+    Observable<CommonDataResponse<HandPractiseBean>> handPractise(@Body HandPractiseReq param);
+
+    /**
+     * 个人中心-用户信息
+     */
+    @GET("laboratory/violation/userInfo")
+    Observable<CommonDataResponse<UserInfo>> queryUserInfo();
+
+    /**
+     * 个人中心-违规信息
+     */
+    @GET("laboratory/violation/historyList")
+    Observable<CommonListResponse<ViolationBean>> queryViolationList(
+            @Query("pageNum") int pageNum, @Query("pageSize") int pageSize,
+            @Query("statTime") String statTime, @Query("endTime") String endTime,
+            @Query("overStatus") String overStatus);
+
+    /**
+     * 是否有待处理的违规
+     */
+    @GET("laboratory/violation/exist")
+    Observable<CommonResponse> existTodoViolation();
+
+    /**
+     * 开始考核-生成考核考卷
+     *
+     * @param id 章节ID
+     */
+    @POST("exam/api/assess/start/{id}")
+    Observable<CommonDataResponse<ExamTopic>> classTestStart(@Path("id") String id);
+
+    /**
+     * 结束考核
+     *
+     * @param id 考核ID(试卷ID)
+     */
+    @POST("exam/api/assess/finish/{id}")
+    Observable<CommonDataResponse<HandPractiseBean>> handAssess(@Path("id") String id);
+
+    /**
+     * 考核记录列表
+     *
+     * @param id 章节ID
+     */
+    @POST("exam/api/assess/list/{id}")
+    Observable<CommonListResponse<AccessRecordBean>> classTestRecord(
+            @Path("id") String id, @Query("pageNum") int pageNum, @Query("pageSize") int pageSize);
+
+    /**
+     * 考核详情
+     *
+     * @param id 考核ID(试卷ID)
+     */
+    @POST("exam/api/assess/detail/{id}")
+    Observable<CommonDataResponse<ExamTopic>> classTestDetail(@Path("id") String id);
+
+    /**
+     * 退出登录
+     */
+    @POST("auth/learn/loginOut")
+    Observable<CommonResponse> loginOut();
+
+}

+ 546 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/retrofit/ExamRetrofit.kt

@@ -0,0 +1,546 @@
+package com.rc.httpcore.client.retrofit
+
+import com.rc.httpcore.HttpClient
+import com.rc.httpcore.client.ExamClient
+import com.rc.httpcore.exception.NetException
+import com.rc.httpcore.vo.CommonListResponse
+import com.rc.httpcore.vo.CommonResponse
+import com.rc.httpcore.vo.request.*
+import com.rc.httpcore.vo.response.*
+import io.reactivex.Observable
+
+class ExamRetrofit : ExamClient {
+
+    private val apiService by lazy {
+        HttpClient.createRetrofitApi(ApiService::class.java)
+    }
+
+    /**
+     * 登录获取token
+     */
+    override fun authOneLogin(): Observable<Boolean> {
+        val param = AccessTokenReq().apply {
+            username = "onecUser"
+            password = "admin123"
+        }
+        return apiService.authOneLogin(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                HttpClient.token = response.data?.access_token
+                return@map !HttpClient.token.isNullOrEmpty()
+            }
+    }
+
+    /**
+     * 查询APK版本
+     *
+     * @param id 设备唯一编码
+     */
+    override fun apkVersion(id: String): Observable<ApkInfoResp> {
+        return apiService.apkVersion(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+                return@map response.data
+            }
+    }
+
+    /**
+     * 上传APK更新状态
+     *
+     * @param state 0:升级失败; 1:升级成功; 2:升级中
+     */
+    override fun onepcApkUpdate(id: String, state: String): Observable<Boolean> {
+        return apiService.onepcApkUpdate(id, state)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+                return@map true
+            }
+    }
+
+    /**
+     * 查询轮播图
+     *
+     * @param id 设备唯一编码
+     */
+    override fun bannerImages(id: String): Observable<List<BannerImageBean>> {
+        return apiService.bannerImages(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+                return@map response.data
+            }
+    }
+
+    /**
+     * 人脸比对
+     */
+    override fun faceCompare(param: FaceCompareReq): Observable<Boolean> {
+        return apiService.faceCompare(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map true
+            }
+    }
+
+    /**
+     * 学习一体机 用户端登录
+     *
+     * @param param 刷学生卡数据 userName
+     */
+    override fun learnLogin(param: LearnLoginReq): Observable<LearnLoginVo> {
+        return apiService.learnLogin(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+//                if (response.code == "4466") {
+//                    response.data?.isLogin = "1"
+//                    response.data?.expires_in = response.msg
+////                    HttpClient.token = response.data?.access_token
+//                    return@map response.data
+//                } else {
+                    HttpClient.token = response.data?.access_token
+                    return@map response.data
+//                }
+
+            }
+    }
+
+    override fun loginToken(param: LearnLoginReq): Observable<LearnLoginVo> {
+        return apiService.loginToken(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+//                HttpClient.token = response.data?.access_token
+                return@map response.data
+            }
+    }
+
+    /**
+     * 安全学习-学习分类
+     */
+    override fun categoryTreeList(): Observable<List<CategoryTree>> {
+        return apiService.categoryTreeList("0")
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 安全学习-查询学习资源列表
+     */
+    override fun examCourseList(param: CourseListReq): Observable<CommonListResponse<ExamCourseVo>> {
+        val filters = mutableMapOf<String, Any>().apply {
+            put("cateId", param.cateId ?: "")
+            put("title", param.title ?: "")
+            put("pageNum", param.pageNum)
+            put("pageSize", param.pageSize)
+            if (!param.scopeType.isNullOrEmpty()) {
+                put("scopeType", param.scopeType)
+            }
+//            put("status", "1")
+        }
+        return apiService.examCourseList(filters)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response
+            }
+    }
+
+    /**
+     * 安全学习-获取学习资源详细信息
+     */
+    override fun examCourseDetail(id: String): Observable<ExamCourseVo> {
+        return apiService.examCourseDetail(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 安全学习-开始学习
+     */
+    override fun examLearnStart(param: ExamLearnReq): Observable<Boolean> {
+        return apiService.examLearnStart(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map true
+            }
+    }
+
+    /**
+     * 安全学习-开始学习
+     */
+    override fun examLearnFinish(param: ExamLearnReq): Observable<LearnBonusBean> {
+        return apiService.examLearnFinish(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 安全学习-个人学习记录
+     */
+    override fun examLearnRecord(param: LearnRecordReq): Observable<CommonListResponse<LearnRecordVo>> {
+        return apiService.examLearnRecord(param.pageNum, param.pageSize, param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response
+            }
+    }
+
+    /**
+     * 查询题目类型下拉框
+     */
+    override fun classifyQueryOption(): Observable<List<TopicClassifyVo>> {
+        return apiService.classifyQueryOption(Object())
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 查询模拟列表
+     */
+    override fun onlineList(param: MockTestReq): Observable<CommonListResponse<MockTestVo>> {
+        return apiService.onlineList(param.pageNum, param.pageSize, param.classifyIds)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response
+            }
+    }
+
+    /**
+     * 开始-查询考试前后否需要人脸验证
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    override fun examStartVerify(type: String, examId: String): Observable<ExamVerify> {
+        return apiService.examStartVerify(type, examId)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 开始考试-创建考卷
+     *
+     * @param examId 非模拟考试模式下都传-1
+     */
+    override fun examStart(type: String, examId: String): Observable<ExamTopic> {
+        return apiService.examStart(type, examId)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 考试 保存用户答案 一题一交
+     */
+    override fun examFillAnswer(param: ExamAnswerReq): Observable<Boolean> {
+        return apiService.examFillAnswer(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map true
+            }
+    }
+
+    /**
+     * 交卷- 完成考试
+     */
+    override fun examHandPaper(param: HandPaperReq): Observable<HandPaperBean> {
+        return apiService.examHandPaper(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 个人考试成绩列表查询
+     */
+    override fun paperMyList(param: ExamScoreReq): Observable<CommonListResponse<ExamTopic>> {
+        return apiService.paperMyList(param.pageNum, param.pageSize, param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response
+            }
+    }
+
+    /**
+     * 个人考试成绩详情查询
+     */
+    override fun paperDetail(id: String): Observable<ExamTopic> {
+        return apiService.paperDetail(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 考试 查询问题和选项 - 一题一查
+     */
+    override fun paperExamQuDetail(param: PaperQuReq): Observable<ExamTopic.ElPaperQu> {
+        return apiService.paperExamQuDetail(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 查询我的证书列表
+     */
+    override fun queryMyCert(pageNum: Int, pageSize: Int): Observable<CommonListResponse<CertVo>> {
+        return apiService.queryMyCert(pageNum, pageSize, Object())
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response
+            }
+    }
+
+    /**
+     * 模拟练习-开始练习
+     */
+    override fun mockTestStart(ids: String): Observable<ExamTopic> {
+        return apiService.mockTestStart(ids)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 练习 查询问题和选项 - 一题一查
+     */
+    override fun paperMockQuDetail(param: PaperQuReq): Observable<ExamTopic.ElPaperQu> {
+        return apiService.paperMockQuDetail(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 练习 保存用户答案 - 一题一交
+     */
+    override fun mockAnswer(param: MockAnswerReq): Observable<Boolean> {
+        return apiService.mockAnswer(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map true
+            }
+    }
+
+    /**
+     * 结束练习
+     */
+    override fun handPractise(param: HandPractiseReq): Observable<HandPractiseBean> {
+        return apiService.handPractise(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 个人中心-用户信息
+     */
+    override fun queryUserInfo(): Observable<UserInfo> {
+        return apiService.queryUserInfo()
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 个人中心-违规信息
+     */
+    override fun queryViolationList(param: ViolationReq): Observable<List<ViolationBean>> {
+        return apiService.queryViolationList(
+            param.pageNum,
+            param.pageSize,
+            param.statTime,
+            param.endTime,
+            param.overStatus
+        )
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.rows
+            }
+    }
+
+    /**
+     * 是否有待处理的违规
+     */
+    override fun existTodoViolation(): Observable<Boolean> {
+        return apiService.existTodoViolation()
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map true
+            }
+    }
+
+    /**
+     * 开始考核-生成考核考卷
+     * @param id 章节ID
+     */
+    override fun classTestStart(id: String): Observable<ExamTopic> {
+        return apiService.classTestStart(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 结束考核
+     * @param id 考核ID(试卷ID)
+     */
+    override fun handAssess(id: String): Observable<HandPractiseBean> {
+        return apiService.handAssess(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 考核记录列表
+     */
+    override fun classTestRecord(param: AssessRecordReq): Observable<CommonListResponse<AccessRecordBean>> {
+        return apiService.classTestRecord(param.chapterId, param.pageNum, param.pageSize)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response
+            }
+    }
+
+    /**
+     * 考核详情
+     * @param id 考核ID(试卷ID)
+     */
+    override fun classTestDetail(id: String): Observable<ExamTopic> {
+        return apiService.classTestDetail(id)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map response.data
+            }
+    }
+
+    /**
+     * 退出登录
+     */
+    override fun loginOut(): Observable<Boolean> {
+        return apiService.loginOut()
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.msg)
+                }
+
+                return@map true
+            }
+    }
+
+}

+ 16 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/config/ConfigCore.kt

@@ -0,0 +1,16 @@
+package com.rc.httpcore.config
+
+import com.rc.httpcore.HttpConfig
+
+object ConfigCore {
+
+    fun initConfig(config: ConfigParam?, baseUrlSp: String?, fileBrowserBaseUrlSp: String?) {
+        if (null != config) {
+            HttpConfig.API_BASE_URL = if (baseUrlSp.isNullOrEmpty()) config.baseUrl else baseUrlSp
+            HttpConfig.FILE_BROWSER_BASE_URL =
+                if (fileBrowserBaseUrlSp.isNullOrEmpty()) config.fileBrowserBaseUrl else fileBrowserBaseUrlSp
+            HttpConfig.HTTP_STRATEGY = config.httpStrategy
+        }
+    }
+
+}

+ 27 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/config/ConfigFactory.kt

@@ -0,0 +1,27 @@
+package com.rc.httpcore.config
+
+import com.rc.httpcore.HttpConfig
+
+class ConfigFactory {
+
+    companion object {
+        val buildDebugConfig = ConfigParam.Builder()
+            .baseUrl("http://180.76.134.43:30002/")
+            .fileBrowserBaseUrl("http://180.76.134.43:31007/")
+            .httpStrategy(HttpConfig.HTTP_STRATEGY_Retrofit)
+            .mqttServerUri("tcp://180.76.134.43:18830")
+            .mqttUName("dlc")
+            .mqttUPwd("123456")
+            .build()
+
+        val buildReleaseConfig = ConfigParam.Builder()
+            .baseUrl("https://lab.sxitdlc.com/labSystem/")
+            .fileBrowserBaseUrl("http://180.76.134.43:31007/")
+            .httpStrategy(HttpConfig.HTTP_STRATEGY_Retrofit)
+            .mqttServerUri("tcp://180.76.134.43:1883")
+            .mqttUName("mqtt")
+            .mqttUPwd("mqtt@zd1883")
+            .build()
+    }
+
+}

+ 68 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/config/ConfigParam.kt

@@ -0,0 +1,68 @@
+package com.rc.httpcore.config
+
+import com.rc.httpcore.HttpConfig
+
+class ConfigParam(builder: Builder) {
+
+    val baseUrl: String = builder.baseUrl
+    val fileBrowserBaseUrl: String = builder.fileBrowserBaseUrl
+
+    val httpStrategy: Int = builder.httpStrategy
+
+    val mqttServerUri: String = builder.mqttServerUri
+    val mqttUName: String = builder.mqttUName
+    val mqttUPwd: String = builder.mqttUPwd
+
+    class Builder {
+        internal var baseUrl: String
+        internal var fileBrowserBaseUrl: String
+        internal var httpStrategy: Int
+        internal var mqttServerUri: String
+        internal var mqttUName: String
+        internal var mqttUPwd: String
+
+        init {
+            baseUrl = "http://pc44sory.xiaomy.net:31738/"
+            fileBrowserBaseUrl = "http://180.76.134.43:31007/"
+            httpStrategy = HttpConfig.HTTP_STRATEGY_Retrofit
+            mqttServerUri = "tcp://42.193.12.99:1883"
+            mqttUName = "dlc"
+            mqttUPwd = "123456"
+        }
+
+        fun baseUrl(baseUrl: String): Builder {
+            this.baseUrl = baseUrl
+            return this
+        }
+
+        fun fileBrowserBaseUrl(fileBrowserBaseUrl: String): Builder {
+            this.fileBrowserBaseUrl = fileBrowserBaseUrl
+            return this
+        }
+
+        fun httpStrategy(httpStrategy: Int): Builder {
+            this.httpStrategy = httpStrategy
+            return this
+        }
+
+        fun mqttServerUri(mqttServerUri: String): Builder {
+            this.mqttServerUri = mqttServerUri
+            return this
+        }
+
+        fun mqttUName(mqttUName: String): Builder {
+            this.mqttUName = mqttUName
+            return this
+        }
+
+        fun mqttUPwd(mqttUPwd: String): Builder {
+            this.mqttUPwd = mqttUPwd
+            return this
+        }
+
+        fun build(): ConfigParam {
+            return ConfigParam(this)
+        }
+
+    }
+}

+ 30 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/config/OkHttpDNS.kt

@@ -0,0 +1,30 @@
+package com.rc.httpcore.config
+
+import android.text.TextUtils
+import android.util.Log
+import okhttp3.Dns
+import java.net.InetAddress
+import java.net.UnknownHostException
+
+
+class OkHttpDNS : Dns {
+    private var mIpString: String? = null
+
+    @Throws(UnknownHostException::class)
+    override fun lookup(hostname: String): List<InetAddress> {
+        Log.d("OkHttpDNS", "hostname----:$hostname")
+        if (TextUtils.isEmpty(hostname) || null == hostname) return Dns.SYSTEM.lookup(hostname)
+
+        //根据域名获取IP
+        val ips = InetAddress.getAllByName(hostname)
+        for (inetAddress in ips) {
+            mIpString = inetAddress.hostAddress
+            Log.d("OkHttpDNS", "mIpString----:$mIpString")
+        }
+        return if (null != mIpString && !TextUtils.isEmpty(mIpString)) {
+            listOf(*InetAddress.getAllByName(mIpString))
+        } else {
+            Dns.SYSTEM.lookup(hostname)
+        }
+    }
+}

+ 22 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/converter/NullOnEmptyConverterFactory.kt

@@ -0,0 +1,22 @@
+package com.rc.httpcore.converter
+
+import okhttp3.ResponseBody
+import retrofit2.Converter
+import retrofit2.Retrofit
+import java.lang.reflect.Type
+
+class NullOnEmptyConverterFactory : Converter.Factory() {
+
+    override fun responseBodyConverter(
+        type: Type?,
+        annotations: Array<out Annotation>?,
+        retrofit: Retrofit?
+    ): Converter<ResponseBody, *> {
+        val delegate: Converter<ResponseBody, Any>? =
+            retrofit?.nextResponseBodyConverter(this, type, annotations)
+
+        return Converter<ResponseBody, Any>() {
+            if (it.contentLength() == 0L) null else delegate?.convert(it)
+        }
+    }
+}

+ 9 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/exception/NetException.kt

@@ -0,0 +1,9 @@
+package com.rc.httpcore.exception
+
+class NetException(val code: String, message: String?): Exception(message) {
+
+    override fun toString(): String {
+        val message = super.getLocalizedMessage() ?: ""
+        return "$message (${code})"
+    }
+}

+ 50 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/TokenHeaderInterceptor.kt

@@ -0,0 +1,50 @@
+package com.rc.httpcore.interceptor
+
+import android.content.Intent
+import android.util.Log
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.google.gson.Gson
+import com.rc.httpcore.HttpClient
+import com.rc.httpcore.vo.CommonResponse
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import java.lang.Exception
+import java.nio.charset.StandardCharsets
+
+class TokenHeaderInterceptor : Interceptor {
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val originalRequest: Request = chain.request()
+        val requestBuilder: Request.Builder = originalRequest.newBuilder()
+
+        if (!HttpClient.token.isNullOrEmpty()) {
+            requestBuilder.addHeader("authorization", HttpClient.token!!)
+        }
+        requestBuilder
+            .header("vName", HttpClient.vName)
+            .header("Accept", "application/json, text/plain, */*")
+            .build()
+        val newlyRequest = requestBuilder.build()
+        val response = chain.proceed(newlyRequest)
+        try {
+            response.body()?.let {
+                val source = it.source()
+                source.request(Long.MAX_VALUE)
+                val data = source.buffer.clone().readString(StandardCharsets.UTF_8)
+
+                val commonResponse = Gson().fromJson<CommonResponse>(data, CommonResponse::class.java)
+                if (commonResponse.isTokenExpired()) {
+                    HttpClient.getAppContext()?.apply {
+                        val intent = Intent("com.rc.core.token_expired")
+                        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+
+        return response
+    }
+}

+ 26 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/formatter/GsonFormatter.kt

@@ -0,0 +1,26 @@
+package com.rc.httpcore.interceptor.formatter
+
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonParser
+
+class GsonFormatter : JSONFormatter() {
+
+    companion object {
+        fun buildIfSupported(): JSONFormatter? {
+            return try {
+                Class.forName("com.google.gson.Gson")
+                GsonFormatter()
+            } catch (ignore: ClassNotFoundException) {
+                null
+            }
+        }
+    }
+
+    private val GSON = GsonBuilder().setPrettyPrinting().create()
+    private val PARSER = JsonParser()
+
+    override fun format(source: String?): String? {
+        return GSON.toJson(PARSER.parse(source))
+    }
+
+}

+ 37 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/formatter/JSONFormatter.kt

@@ -0,0 +1,37 @@
+package com.rc.httpcore.interceptor.formatter
+
+import org.json.JSONException
+
+/**
+ * json格式化
+ */
+open class JSONFormatter {
+
+    companion object {
+
+        private val FORMATTER = findJSONFormatter()
+
+        fun formatJSON(source: String?): String? {
+            return try {
+                FORMATTER.format(source)
+            } catch (e: java.lang.Exception) {
+                ""
+            }
+        }
+
+        private fun findJSONFormatter(): JSONFormatter {
+            val jsonFormatter = OrgJsonFormatter.buildIfSupported()
+            if (jsonFormatter != null) {
+                return jsonFormatter
+            }
+            val gsonFormatter = GsonFormatter.buildIfSupported()
+            return gsonFormatter ?: JSONFormatter()
+        }
+    }
+
+    @Throws(JSONException::class)
+    open fun format(source: String?): String? {
+        return ""
+    }
+
+}

+ 26 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/interceptor/formatter/OrgJsonFormatter.kt

@@ -0,0 +1,26 @@
+package com.rc.httpcore.interceptor.formatter
+
+import org.json.JSONObject
+
+class OrgJsonFormatter : JSONFormatter() {
+
+    companion object {
+
+        private const val INDENT_SPACES = 4
+
+        fun buildIfSupported(): OrgJsonFormatter? {
+            return try {
+                Class.forName("org.json.JSONObject")
+                OrgJsonFormatter()
+            } catch (ignore: ClassNotFoundException) {
+                null
+            }
+        }
+    }
+
+    override fun format(source: String?): String? {
+        return if (source.isNullOrEmpty())
+            "" else JSONObject(source).toString(INDENT_SPACES)
+    }
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/net/DownloadListener.kt

@@ -0,0 +1,14 @@
+package com.rc.httpcore.net
+
+interface DownloadListener {
+
+    fun onProgress(progress: Int)
+
+    fun onSuccess()
+
+    fun onFailed(errMsg: String?)
+
+    fun onPaused()
+
+    fun onCanceled()
+}

+ 156 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/net/DownloadTask.kt

@@ -0,0 +1,156 @@
+package com.rc.httpcore.net
+
+import android.os.AsyncTask
+import android.os.Environment
+import android.text.TextUtils
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.File
+import java.io.InputStream
+import java.io.RandomAccessFile
+
+/**
+ * eg: http://www.download.com/projectname/dl/test.apk
+ * 将文件下载到sd卡/Download目录。
+ * 下载到本地的文件件名,如果没指定则为test.apk。
+ *
+ */
+class DownloadTask(
+    private val listener: DownloadListener,
+    private val dlPath: String = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path,
+    private var fileName: String?,
+    private val headers: Map<String, String>? = null
+) :
+    AsyncTask<String, Int, Int>() {
+
+    companion object {
+        const val TYPE_SUCCESS: Int = 1
+        const val TYPE_FAILED: Int = 2
+        const val TYPE_PAUSED: Int = 3
+        const val TYPE_CANCELED: Int = 4
+    }
+
+    private var isCanceled: Boolean = false
+    private var isPaused: Boolean = false
+
+    private var lastProgress: Int = 0
+
+    override fun doInBackground(vararg params: String?): Int {
+        var file: File? = null
+        var inputStream: InputStream? = null
+        var saveFile: RandomAccessFile? = null
+        try {
+            var downloadedLength = 0L
+            val downloadUrl = params[0]
+            downloadUrl ?: return TYPE_FAILED
+
+            if (TextUtils.isEmpty(fileName)) {
+                fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"))
+            }
+
+            file = File(dlPath, fileName!!)
+            if (file.exists()) {
+                downloadedLength = file.length()
+            }
+            val contentLength: Long = getContentLength(downloadUrl)
+            if (contentLength == 0L) {
+                return TYPE_FAILED
+            } else if (contentLength == downloadedLength) {
+                // 已经下载的字节数和文件的总字节数相等,说明已经下载完成。
+                return TYPE_SUCCESS
+            }
+            val builder = Request.Builder()
+            headers?.let {
+                for ((key, value) in it)
+                    builder.addHeader(key, value)
+            }
+            val request = builder
+                // 断点下载,指定从那个字节开始下
+                .addHeader("RANGE", "bytes=" + downloadedLength + "-")
+                .url(downloadUrl)
+                .build()
+            val response = OkHttpClient()
+                .newCall(request)
+                .execute()
+
+            inputStream = response.body()!!.byteStream()
+            saveFile = RandomAccessFile(file, "rw")
+            saveFile.seek(downloadedLength) // 跳过已经下载的字节
+            val buff = ByteArray(1024)
+            var total = 0
+            var len = 0;
+            while ({ len = inputStream.read(buff);len }() != -1) {
+                if (isCanceled) {
+                    return TYPE_CANCELED
+                } else if (isPaused) {
+                    return TYPE_PAUSED
+                } else {
+                    total += len
+                    saveFile.write(buff, 0, len)
+                    // 计算已经下载的百分比
+                    val progress: Int = ((total + downloadedLength) * 100 / contentLength).toInt()
+                    publishProgress(progress)
+                }
+            }
+            response.body()!!.close()
+            return TYPE_SUCCESS
+        } catch (e: Exception) {
+            e.printStackTrace()
+        } finally {
+            try {
+                inputStream?.close()
+                saveFile?.close()
+                if (isCanceled) file?.delete()
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }
+        return TYPE_FAILED
+    }
+
+    override fun onProgressUpdate(vararg values: Int?) {
+        val progress = values[0]
+        progress?.let {
+            if (it > lastProgress) {
+                listener.onProgress(it)
+                lastProgress = it
+            }
+        }
+    }
+
+    override fun onPostExecute(status: Int?) {
+        when (status) {
+            TYPE_SUCCESS -> listener.onSuccess()
+            TYPE_FAILED -> listener.onFailed(null)
+            TYPE_PAUSED -> listener.onPaused()
+            TYPE_CANCELED -> listener.onCanceled()
+            else -> Unit
+        }
+    }
+
+    fun pauseDownload() {
+        this.isPaused = true
+    }
+
+    fun cancelDownload() {
+        this.isCanceled = true
+    }
+
+    private fun getContentLength(downloadUrl: String): Long {
+        val builder = Request.Builder()
+        headers?.let {
+            for ((key, value) in it)
+                builder.addHeader(key, value)
+        }
+        val request = builder.url(downloadUrl).build()
+        val response = OkHttpClient()
+            .newCall(request)
+            .execute()
+        if (response.isSuccessful) {
+            val contentLength = response.body()!!.contentLength()
+            response.close()
+            return contentLength
+        }
+        return 0
+    }
+}

+ 5 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/CommonDataResponse.kt

@@ -0,0 +1,5 @@
+package com.rc.httpcore.vo
+
+class CommonDataResponse<T> : CommonResponse() {
+    var data: T? = null
+}

+ 6 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/CommonListResponse.kt

@@ -0,0 +1,6 @@
+package com.rc.httpcore.vo
+
+class CommonListResponse<T> : CommonResponse() {
+    var rows: List<T>? = null
+    var total: Int = 0
+}

+ 17 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/CommonResponse.kt

@@ -0,0 +1,17 @@
+package com.rc.httpcore.vo
+
+open class CommonResponse {
+
+    var code: String = ""
+    var msg: String = ""
+
+    companion object {
+        const val CODE_SUCCESS = "200"
+        const val CODE_TOKEN_EXPIRED = "401"
+    }
+
+    fun isSuccess() = CODE_SUCCESS == code
+
+    fun isTokenExpired() = CODE_TOKEN_EXPIRED == code
+
+}

+ 46 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/LogInterceptor.java

@@ -0,0 +1,46 @@
+package com.rc.httpcore.vo;
+
+import android.util.Log;
+
+import java.io.IOException;
+
+import okhttp3.FormBody;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+
+public class LogInterceptor implements Interceptor {
+
+    public static String TAG = "HttpClient";
+
+    @Override
+    public okhttp3.Response intercept(Interceptor.Chain chain) throws IOException {
+        Request request = chain.request();
+        long startTime = System.currentTimeMillis();
+        okhttp3.Response response = chain.proceed(chain.request());
+        long endTime = System.currentTimeMillis();
+        long duration=endTime-startTime;
+        okhttp3.MediaType mediaType = response.body().contentType();
+        String content = response.body().string();
+        Log.d(TAG,"\n");
+        Log.d(TAG,"----------Start----------------");
+        Log.d(TAG, "| "+request.toString());
+        String method=request.method();
+        if("POST".equals(method)){
+            StringBuilder sb = new StringBuilder();
+            if (request.body() instanceof FormBody) {
+                FormBody body = (FormBody) request.body();
+                for (int i = 0; i < body.size(); i++) {
+                    sb.append(body.encodedName(i) + "=" + body.encodedValue(i) + ",");
+                }
+                sb.delete(sb.length() - 1, sb.length());
+                Log.d(TAG, "| RequestParams:{"+sb.toString()+"}");
+            }
+        }
+        LogUtil.eLength(TAG,"| Response:" + content);
+//        Log.d(TAG, "| Response:" + content);
+        Log.d(TAG,"----------End:"+duration+"毫秒----------");
+        return response.newBuilder()
+                .body(okhttp3.ResponseBody.create(mediaType, content))
+                .build();
+    }
+}

+ 94 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/LogUtil.java

@@ -0,0 +1,94 @@
+package com.rc.httpcore.vo;
+
+import android.util.Log;
+
+public class LogUtil {
+
+    private LogUtil() {
+        /* cannot be instantiated */
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+
+    public static boolean isDebug;// 是否需要打印bug,在application的onCreate函数里面初始化
+    private static final String TAG = "HttpClient";
+
+
+    // 下面四个是默认tag的函数
+    public static void i(String msg) {
+        if (isDebug)
+            Log.i(TAG, msg);
+    }
+
+
+    public static void d(String msg) {
+        if (isDebug)
+            Log.d(TAG, msg);
+    }
+
+
+    public static void e(String msg) {
+        if (isDebug)
+            Log.e(TAG, msg);
+    }
+
+
+    public static void v(String msg) {
+        if (isDebug)
+            Log.v(TAG, msg);
+    }
+
+
+    // 下面是传入自定义tag的函数
+    public static void i(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+
+    public static void d(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+
+    public static void e(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+
+    public static void v(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+
+    /**
+     * 截断输出日志
+     *
+     * @param msg
+     */
+    public static void eLength(String tag, String msg) {
+
+        if (tag == null || tag.length() == 0
+                || msg == null || msg.length() == 0)
+            return;
+
+
+        int segmentSize = 3 * 1024;
+        long length = msg.length();
+        if (length <= segmentSize) {// 长度小于等于限制直接打印
+            Log.e(tag, msg);
+        } else {
+            while (msg.length() > segmentSize) {// 循环分段打印日志
+                String logContent = msg.substring(0, segmentSize);
+                msg = msg.replace(logContent, "");
+                Log.e(tag, logContent);
+            }
+            Log.e(tag, msg);// 打印剩余日志
+        }
+
+    }
+
+}

+ 16 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/AccessTokenReq.java

@@ -0,0 +1,16 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AccessTokenReq {
+
+    public String username;
+    public String password;
+
+//    Pair("username", "onecUser"),
+//    Pair("password", "admin123"))
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/AssessRecordReq.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AssessRecordReq {
+
+    public String chapterId; // 章节ID
+    public int pageNum;
+    public int pageSize;
+
+}

+ 16 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/CourseListReq.java

@@ -0,0 +1,16 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class CourseListReq {
+
+    public String cateId; // 分类ID
+    public String title; // 标题
+    public int pageNum;
+    public int pageSize;
+    public String scopeType; // 适用范围(1. 安全准入考试,2. 负面清单考试,3. 黑名单考试)
+
+}

+ 20 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ExamAnswerReq.java

@@ -0,0 +1,20 @@
+package com.rc.httpcore.vo.request;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ExamAnswerReq {
+
+    public String paperId; // 试卷ID
+    public String quId; // 题目ID
+    public List<Answer> elPaperQuAnswerList; // 用户答案
+
+    public static class Answer {
+        public String id; // 选项id
+    }
+
+}

+ 13 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ExamLearnReq.java

@@ -0,0 +1,13 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ExamLearnReq {
+
+    public String chapterId; // 章节id
+    public String courseId; // 课程id
+
+}

+ 15 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ExamScoreReq.java

@@ -0,0 +1,15 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ExamScoreReq {
+
+    public String scopeType;
+
+    public int pageNum;
+    public int pageSize;
+
+}

+ 13 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/FaceCompareReq.java

@@ -0,0 +1,13 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class FaceCompareReq {
+
+    public byte[] data; // 特征码
+    public String userId;
+
+}

+ 12 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/HandPaperReq.java

@@ -0,0 +1,12 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HandPaperReq {
+
+    public String id; // 试卷id
+
+}

+ 12 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/HandPractiseReq.java

@@ -0,0 +1,12 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HandPractiseReq {
+
+    public String id;
+
+}

+ 17 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/LearnLoginReq.java

@@ -0,0 +1,17 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LearnLoginReq {
+
+    public String id;
+    public String userName;
+    public String type; // 登录类型,1 表示第一次调用获取信息,2 人脸验证之后调用 实现真正的登录
+    public String machineCode; // 设备码
+    public int aioType = 3; // 4-化学品终端 3-学习一体机
+    public String isLogin = "0"; //0 未登录   二次登陆 1
+
+}

+ 19 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/LearnRecordReq.java

@@ -0,0 +1,19 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LearnRecordReq {
+
+    public String cateId; // 分类ID
+    public String cateTitle; // 分类标题
+    public String courseId; // 课程ID
+    public String courseTitle; // 课程标题
+    public String scopeType; // 适用范围(1. 安全准入考试,2. 负面清单考试,3. 黑名单考试)
+
+    public int pageNum;
+    public int pageSize;
+
+}

+ 20 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/MockAnswerReq.java

@@ -0,0 +1,20 @@
+package com.rc.httpcore.vo.request;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class MockAnswerReq {
+
+    public String practiseId; // 练习ID
+    public String quId; // 题目ID
+    public List<Answer> answerList; // 用户答案
+
+    public static class Answer {
+        public String id; // 选项id
+    }
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/MockTestReq.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class MockTestReq {
+
+    public int pageNum;
+    public int pageSize;
+    public String classifyIds;
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/PaperQuReq.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class PaperQuReq {
+
+    public String paperId; // 试卷ID
+    public String practiseId; // 练习ID
+    public String quId; // 题目ID
+
+}

+ 16 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/request/ViolationReq.java

@@ -0,0 +1,16 @@
+package com.rc.httpcore.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ViolationReq {
+
+    public int pageNum;
+    public int pageSize;
+    public String statTime; // yyyy-MM-dd
+    public String endTime; // yyyy-MM-dd
+    public String overStatus;
+
+}

+ 22 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/AccessRecordBean.java

@@ -0,0 +1,22 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AccessRecordBean {
+
+    public String paperId; // 试卷ID
+    public String assessName; // 考核名称
+    public String assessTime; // 考核时间 yyyy-MM-dd HH:mm:ss
+    public String allQu; // 全部题数
+    public String radioQu; // 单选题数
+    public String multiQu; // 多选题数
+    public String judgeQu; // 判断题数
+    public String accuracy; // 考核正确率,100 表示通过,其余都不通过
+    public String points; // 获得奖励分
+    public String errors; // 错误数
+    public String correctly; // 正确数
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/AccessTokenResp.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AccessTokenResp {
+
+    public String access_token;
+    public String user_id;
+    public String expires_in;
+    public String username;
+}

+ 13 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ApkInfoResp.java

@@ -0,0 +1,13 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ApkInfoResp {
+
+    public String apkFileUpload;
+    public String version;
+
+}

+ 12 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/BannerImageBean.java

@@ -0,0 +1,12 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class BannerImageBean {
+
+    public String imgUrl;
+
+}

+ 23 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/CategoryTree.java

@@ -0,0 +1,23 @@
+package com.rc.httpcore.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class CategoryTree {
+
+    public String userId;
+    public String id;
+    public String title;
+    public String parentId;
+    public String level;
+    public List<CategoryTree> children;
+
+    @Override
+    public String toString() {
+        return title;
+    }
+}

+ 28 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/CertVo.java

@@ -0,0 +1,28 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class CertVo {
+
+    public String createBy;
+    public String createTime;
+    public String updateBy;
+    public String updateTime;
+    public String deptId;
+    public String deptName;
+    public String userId;
+    public String id;
+    public String joinUserId; // 用户id
+    public String userNickName;
+    public String userName;
+    public String examId; //关联考试id
+    public String certId; // 关联证书id
+    public String expirationTime; // 过期时间
+    public String certTitle; // 证书名称
+    public String certUrl; // 证书地址
+    public String code;
+
+}

+ 170 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ExamCourseVo.java

@@ -0,0 +1,170 @@
+package com.rc.httpcore.vo.response;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ExamCourseVo implements Serializable {
+
+    public String allCateTitle; // 课程分类(级联)
+    public String title; // 课程名称
+    public String cateTitle; // 分类标题
+    public String chapters; // 章节数
+    public String accessNum; // 访问人数
+    public String createTime; // 创建时间
+    public String duration; // 课程时长
+    public String durationStr;
+    public String durations; // 课程总时长
+    public String durationsStr;
+
+    public long learnDurations; //  用户学习总时长
+    public String learnDurationStr;
+
+    public String learns; // 学习人数
+    public String type; // 课程类型
+    public String id;
+
+    public String updateTime; // 更新时间
+    public String remark;
+    public String deptId; // 题目ID
+    public String deptName; // 部门名称
+    public String userId; // 用户ID(数据权限)
+    public String cateId; // 分类ID
+    public String pri; // 是否公开
+    public String level; // 等级
+    public String orderRest; // 是否限制顺序
+    public String label; // 课程标签
+    public String effectiveTime; // PDF翻页有效时间
+    public String ceType; // 课程有效时间类型
+    public String ceTime; // 课程有效时间
+    public String keywords; // 课程关键字
+    public String img; // 封面图片
+    public String description; // 课程简介
+    public String status; // 课程状态 0 下架,1 上架
+    public String videoDraggable; // 是否显示课程拖拽进度 0 不可拖拽
+    public String points; // 积分值
+    public String machinePoints; // 学习机积分值
+    public String settings; // 设置选项
+    public List<ChapterVo> chapterList;
+    public String scopeType; // 适用范围(1. 安全准入考试,2. 负面清单考试,3. 黑名单考试)
+    public String learnStatus; //  0 学习中,2 未学习,3 待考核,1 已完成
+
+    public String decodeScopeType() {
+        if (null == scopeType || scopeType.isEmpty()) {
+            return "";
+        }
+        String[] array = scopeType.split(",");
+        StringBuilder builder = new StringBuilder();
+        for (String type : array) {
+            builder.append(decodeScopeType(type)).append("、");
+        }
+        if (builder.length() > 0) {
+            builder.deleteCharAt(builder.length() - 1);
+        }
+        return builder.toString();
+    }
+
+    private String decodeScopeType(String type) {
+        if ("1".equals(type)) {
+            return "安全准入考试";
+        } else if ("2".equals(type)) {
+            return "负面清单考试";
+        } else if ("3".equals(type)) {
+            return "黑名单考试";
+        }
+        return "";
+    }
+
+    public String decodeLearnStatus() {
+        if ("0".equals(learnStatus)) {
+            return "学习中";
+        } else if ("1".equals(learnStatus)) {
+            return "已完成";
+        } else if ("2".equals(learnStatus)) {
+            return "未学习";
+        } else if ("3".equals(learnStatus)) {
+            return "待考核";
+        }
+        return "";
+    }
+
+    public static class ChapterVo implements Serializable {
+        public String createBy;
+        public String createTime; // 创建时间
+        public String updateBy;
+        public String updateTime; // 更新时间
+        public String remark;
+        public String deptId; // 题目ID
+        public String deptName; // 部门名称
+        public String userId; // 用户ID(数据权限)
+        public String id; // 章节ID
+        public String courseId; // 课程ID
+        public String title; // 标题
+        public String parentId; // 父级ID
+        public String position;
+        public String resourcesId; // 关联资源ID
+        public String chapterData; // 资源地址或上传的地址
+        public String description; // 简介
+        public String free; // 是否免费试读章节,0为收费,1为免费
+        public String type; // 0 为目录 其他类型同课件类型 文件类型 1 文档,2 视频,3 图片,4 音频,5 富文本
+        public String postfix; // 文件后缀
+        public String allowdownload; // 允许下载资源
+        public long duration; // 课件时长
+        public String durationStr; // 课件时长
+        public String learnStatus; // 学习状态 0 学习中,2 未学习,1 已完成
+        public String hasChildren;
+        public List<ChapterVo> children;
+
+        public String isAssess; // 是否考核(0 否,1 是)
+        public String assessName; // 考核名称
+        public String assessStatus; // 考核状态 0 未完成,1 已完成
+        public int hasAssess; // 考核次数
+
+        /**
+         * 该章节是否学习和考核都完成
+         */
+        public boolean chapterCompleted() {
+            // 学习未完成
+            if (!"1".equals(learnStatus)) return false;
+            // 配置了考核,但是考核未通过
+            if ("1".equals(isAssess) && !"1".equals(assessStatus)) return false;
+            return true;
+        }
+
+        /**
+         * 是否显示课后考核,①配置了课后考核;②并且课后考后未通过
+         */
+        public boolean showClassTest() {
+            // 没有配置 >> 不显示
+            if (!"1".equals(isAssess)) return false;
+            // 有配置,考核通过 >> 不显示
+            if ("1".equals(assessStatus)) return false;
+            return true;
+        }
+
+        /**
+         * 是否显示考试记录,①配置了考试;②考试次数大于0
+         */
+        public boolean showClassTestRecord() {
+            return "1".equals(isAssess) && hasAssess > 0;
+        }
+
+        /**
+         * 0 学习中,2 未学习,3 待考核,1 已完成
+         */
+        public int formatLearnStatus() {
+            if ("0".equals(learnStatus)) return 0; // 学习中
+            if ("1".equals(learnStatus)) {
+                // 待考核(考核未完成):配置了考核,但是考核未通过
+                boolean isAssessing = "1".equals(isAssess) && !"1".equals(assessStatus);
+                return isAssessing ? 3 : 1;
+            } else return 2; // 未学习
+        }
+
+    }
+
+}

+ 113 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ExamTopic.java

@@ -0,0 +1,113 @@
+package com.rc.httpcore.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ExamTopic {
+
+    public String id;
+    public String createTime; // 创建时间
+    public String updateTime; // 更新时间
+    public String deptId; // 题目ID
+    public String deptName; // 部门名称
+    public String examId; // 考试ID
+    public String joinUserId; // 用户ID
+    public String limitTime; // 截止时间
+    public String objScore; // 客观得分
+    public String qualifyScore; // 及格分
+    public String state; // 试卷状态
+    public String subjScore; // 主观分
+    public String title; // 考试标题
+    public String totalScore; // 试卷总分
+    public int totalTime; // 考试时长
+    public String userId; // 用户ID(数据权限)
+    public String userTime; // 用户时长
+    public String userTimeStr;
+    public String userScore; // 用户得分
+    public String violationId; // 违规id
+    public String scopeType;
+    public String hasSaq;
+    public String classifyNames; // 分类名称 多个以逗号分隔
+
+    public List<ElPaperQu> elPaperQuList; // 试卷考题信息
+
+    public ExamConfig examConfig;
+    public OtherData otherData;
+
+    public List<ElPaperQu> radioList; // 单选题列表
+    public List<ElPaperQu> multiList; // 多选题列表
+    public List<ElPaperQu> judgeList; // 判断题
+
+    public static class ElPaperQu {
+        public int currentNum; // 当前题目编号 for android local
+        public int topicTotal; // 总题目数 for android local
+
+        public String actualScore; // 实际得分
+        public String analysis; // 整题题解
+        public String answer;
+        public String answered; // 是否已答 0 未答,1 已答
+        public String content; // 题目问题
+        public String createBy;
+        public String createTime; // 创建时间
+        public String updateBy;
+        public String updateTime; // 更新时间
+        public String userId; // 用户ID(数据权限)
+        public String deptId; // 题目ID
+        public String deptName; // 部门名称
+        public String isRight; // 是否答对
+        public String id;
+        public String paperId; // 试卷ID
+        public String practiseId; // 练习ID
+        public String quId; // 题目ID
+        public String quType; // 题目类型
+        public String sort; // 问题排序
+        public String score; // 单题分分值
+        public List<ElPaperQuAnswer> elPaperQuAnswerList; // 试卷考题答案【考试用】
+        public List<ElPaperQuAnswer> answerList; // 试卷考题答案【练习用】
+    }
+
+    public static class ElPaperQuAnswer {
+        public String abc; // 选项标签
+        public String answerId; // 回答项ID
+        public String checked; // 是否选中
+        public String content; // 选项描述
+        public String createBy;
+        public String createTime; // 创建时间
+        public String deptId; // 题目ID
+        public String deptName; // 部门名称
+        public String id;
+        public String isRight; // 是否正确项
+        public String paperId; // 试卷ID
+        public String practiseId; // 练习ID
+        public String quId; // 题目ID
+        public String sort; // 排序
+        public String updateBy;
+        public String updateTime; // 更新时间
+        public String userId; // 用户ID(数据权限)
+        public String analysis; // 整题题解
+    }
+
+    public static class ExamConfig {
+        public int radioSumCount; // 单选题数量
+        public int radioSumScore; // 单选题总分
+        public int multiSumCount; // 多选题数量
+        public int multiSumScore; // 多选题总分
+        public int judgeSumCount; // 判断题数量
+        public int judgeSumScore; // 判断题总分
+    }
+
+    public static class OtherData {
+        public String allQu; // 题目总数
+        public String radioQu; // 单选题数
+        public String multiQu; // 多选题数
+        public String judgeQu; // 判断题数
+        public String accuracy; // 正确率,100表示通过,其余都不通过
+        public String title; // 章节名称
+        public String assessName; // 考核名称
+    }
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ExamVerify.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ExamVerify {
+
+    public int preFaceVerify; // 考前是否人脸验证 0 否,1 是
+    public int endFaceVerify; // 考后是否人脸验证 0 否,1 是
+    public String examId;
+
+}

+ 22 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/HandPaperBean.java

@@ -0,0 +1,22 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HandPaperBean {
+
+    public String userCertUrl; // 证书地址
+    public String correctly; // 正确数
+    public String answered; // 已答数
+    public String userTime; // 考试用时
+    public String unAnswered; // 未答数
+    public String userScore; // 考试得分
+    public String errors; // 错误数
+    public boolean passed; // 合格 / 不合格
+
+    public String examType; // 考试类型
+    public String paperId; // 试卷ID
+
+}

+ 20 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/HandPractiseBean.java

@@ -0,0 +1,20 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HandPractiseBean {
+
+    public String unAnswered; // 未答数
+    public String answered; // 已答数
+    public String errors; // 错误数
+    public String correctly; // 正确数
+    public String points; // 获得积分
+    public String accuracy; // 正确率
+
+    public boolean passed; // 合格 / 不合格
+    public String paperId; // 试卷ID
+
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/LearnBonusBean.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LearnBonusBean {
+
+    public String points; // 获得积分
+    public String durations; // 学习时长
+    public String durationStr;
+
+}

+ 19 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/LearnLoginVo.java

@@ -0,0 +1,19 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LearnLoginVo {
+
+    public String access_token;
+    public String user_id;
+    public String type;
+    public String expires_in;  // msg
+    public String username;
+    public String nickName; // 用户姓名
+
+    public String isLogin = "0"; //0 未登录   二次登陆 1
+
+}

+ 62 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/LearnRecordVo.java

@@ -0,0 +1,62 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LearnRecordVo {
+
+    public String cateId; // 分类ID
+    public String cateTitle; // 分类标题
+    public String courseId; // 课程ID
+    public String courseTitle; // 课程标题
+    public String learnTime; // 学习时间
+    public String learnDuration; // 学习时长
+    public String learnDurationStr;
+    public String bonusPoints; // 获得奖励分
+    public String joinUserId;// ": 22
+
+    public String scopeType; // 适用范围(1. 安全准入考试,2. 负面清单考试,3. 黑名单考试)
+    public String learnStatus; //  0 学习中,2 未学习,3 待考核,1 已完成
+
+    public String decodeScopeType() {
+        if (null == scopeType || scopeType.isEmpty()) {
+            return "";
+        }
+        String[] array = scopeType.split(",");
+        StringBuilder builder = new StringBuilder();
+        for (String type : array) {
+            builder.append(decodeScopeType(type)).append("、");
+        }
+        if (builder.length() > 0) {
+            builder.deleteCharAt(builder.length() -1);
+        }
+        return builder.toString();
+    }
+
+    private String decodeScopeType(String type) {
+        if ("1".equals(type)) {
+            return "安全准入考试";
+        } else if ("2".equals(type)) {
+            return "负面清单考试";
+        } else if ("3".equals(type)) {
+            return "黑名单考试";
+        }
+        return "";
+    }
+
+    public String decodeLearnStatus() {
+        if ("0".equals(learnStatus)) {
+            return "学习中";
+        } else if ("1".equals(learnStatus)) {
+            return "已完成";
+        } else if ("2".equals(learnStatus)) {
+            return "未学习";
+        } else if ("3".equals(learnStatus)) {
+            return "待考核";
+        }
+        return "";
+    }
+
+}

+ 50 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/MockTestVo.java

@@ -0,0 +1,50 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class MockTestVo {
+
+    public String id;
+    public String title; // 考试名称
+
+    public String classifyNames; // 分类名称 多个以逗号分隔
+
+    public String updateTime; // 更新时间
+    public String totalScore; // 总分数
+    public String userId; // 用户ID(数据权限)
+    public String timeLimit; // 是否限时
+    public String state; // 考试状态 1 正常 0 下架
+    public String startTime; // 开始时间
+    public String scopeType; // 适用范围(1. 安全准入考试,2. 负面清单考试,3. 黑名单考试,4.模拟考试)
+
+    public String openType; // 1公开2部门3定员
+    public String joinType; // 组题方式1题库,2指定
+    public String level; // 难度:1中等,2较难,3难
+    public String preFaceVerify; // 考前是否人脸验证 0 否,1 是
+    public String qualifyScore; // 及格分数
+
+    public String endFaceVerify; // 考后是否人脸验证 0 否,1 是
+    public String endTime; // 结束时间
+
+    public String deptId; // 题目ID
+    public String deptName; // 部门名称
+//    public List<String> deptIds; // 考试部门列表
+
+    public String createBy;
+    public String createTime; // 创建时间
+    public String updateBy;
+    public String remark;
+
+    public String certId; // 关联证书id
+    public String code; // 学科类别
+    public String content; // 考试描述
+    public String conversion; // 积分换算值(1 积分=多少试卷分数)
+    public String totalTime; // 总时长(分钟)
+    public String classifyIds; // 分类ID多个以逗号分隔
+    public String courseIds; // 资源ID多个以逗号分隔
+//            public String repoList;
+//            public String examCerts;
+}

+ 14 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/TopicClassifyVo.java

@@ -0,0 +1,14 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class TopicClassifyVo {
+
+    public String userId;
+    public String id;
+    public String title;
+
+}

+ 22 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/UserInfo.java

@@ -0,0 +1,22 @@
+package com.rc.httpcore.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class UserInfo {
+
+    public String userId;
+    public String avatar; // 头像
+    public String userName; // 姓名
+    public String deptName;
+    public String status;
+    public String violatioNum; // 违规次数
+    public String negativeListCount; // 负面清单次数
+    public String blackListNum; // 黑名单次数
+    public String totalPoints; // 积分
+    public String bonusPoints; // 奖励分
+    public String studyLen; // 学习时长统计
+
+}

+ 22 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/vo/response/ViolationBean.java

@@ -0,0 +1,22 @@
+package com.rc.httpcore.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ViolationBean {
+
+    public String createTime;
+    public String remake; // 违规描述
+    public List<Item> violationList;
+
+    public static class Item {
+        public String type; // 类型,1:警告;2:约谈;3:学习;4:练习;5:考试
+        public String content; // 违规行为描述
+        public String status; // 处理状态,0:未完成;1已完成
+    }
+
+}

+ 8 - 0
LICENSE

@@ -0,0 +1,8 @@
+MIT License
+Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# lab-android
+
+学习一体机

+ 1 - 0
RcCore/.gitignore

@@ -0,0 +1 @@
+/build

+ 53 - 0
RcCore/build.gradle

@@ -0,0 +1,53 @@
+plugins {
+    id 'com.android.library'
+    id 'kotlin-android'
+}
+
+android {
+    compileSdkVersion env.compileSdkVersion
+    buildToolsVersion env.buildToolsVersion
+
+    defaultConfig {
+        minSdkVersion env.minSdkVersion
+        targetSdkVersion env.targetSdkVersion
+        versionCode 1
+        versionName "1.0"
+
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility env.jdk_version
+        targetCompatibility env.jdk_version
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+    buildFeatures{
+        viewBinding = true
+    }
+
+}
+
+dependencies {
+
+    implementation project(':HttpCoreLibrary')
+
+    api fileTree(dir: "libs", include: ["*.jar"])
+    implementation dep.kotlinStdlib
+    implementation dep.androidxCoreKtx
+    implementation dep.androidxSwipeRefreshLayout
+
+    implementation dep.androidxAppCompat
+    implementation dep.androidMaterial
+    implementation dep.RecyclerViewAdapterHelper
+
+    implementation dep.eventbus
+
+}

+ 0 - 0
RcCore/consumer-rules.pro


BIN
RcCore/libs/tbs_sdk_v4.3.0.165_20210628_103707.jar


+ 21 - 0
RcCore/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 5 - 0
RcCore/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.rc.core">
+
+</manifest>

+ 8 - 0
RcCore/src/main/java/com/rc/core/event/RefreshEvent.kt

@@ -0,0 +1,8 @@
+package com.rc.core.event
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+data class RefreshEvent(var refresh: Boolean = true, var flag: String)

+ 27 - 0
RcCore/src/main/java/com/rc/core/log/RcLog.kt

@@ -0,0 +1,27 @@
+package com.rc.core.log
+
+import java.util.logging.Level
+import java.util.logging.Logger
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object RcLog {
+
+    private val logger = Logger.getLogger(RcLog::class.java.name)
+
+    fun warn(message: String, throwable: Throwable? = null) {
+        printLog(Level.WARNING, message, throwable)
+    }
+
+    fun info(message: String, throwable: Throwable? = null) {
+        printLog(Level.INFO, message, throwable)
+    }
+
+    private fun printLog(level: Level, message: String, throwable: Throwable? = null) {
+        logger.log(level, message, throwable)
+    }
+
+}

+ 71 - 0
RcCore/src/main/java/com/rc/core/ui/ActivityCollector.kt

@@ -0,0 +1,71 @@
+package com.rc.core.ui
+
+import android.app.Activity
+import java.util.ArrayList
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object ActivityCollector {
+
+    /**
+     * 存放应用已开启的所有Activity.
+     */
+    private val mActivities by lazy { ArrayList<Activity>() }
+
+    /**
+     * 添加一个Activity到Activity列表中.
+     *
+     * @param activity
+     * : Activity实例.
+     */
+    fun addActivity(activity: Activity) {
+        mActivities.add(activity)
+    }
+
+    /**
+     * 在Activity列表中,从顶部删除一定数量的Activity.
+     * -1表示删除所有的Activity, 如果count大于Activity列表的size, 将删除所有的Activity.
+     *
+     * @param count
+     * : 要删的Activity数量.
+     */
+    fun removeActivity(count: Int) {
+        val listSize = mActivities.size
+        val delCount = if (-1 == count || count > listSize) listSize else count
+        for (i in 0 until delCount) {
+            val activity = mActivities[listSize - i - 1]
+            if (!activity.isFinishing) {
+                activity.finish()
+            }
+            mActivities.remove(activity)
+        }
+    }
+
+    /**
+     * 从Activity列表中删除指定的Activity.
+     *
+     * @param activity
+     * : 要删的Activity.
+     */
+    fun removeActivity(activity: Activity) {
+        if (mActivities.contains(activity)) {
+            mActivities.remove(activity)
+        }
+    }
+
+    /**
+     * 关闭所有的Activity, 退出程序.
+     */
+    fun finishAll() {
+        for (activity in mActivities) {
+            if (!activity.isFinishing) {
+                activity.finish()
+            }
+        }
+        mActivities.clear()
+    }
+
+}

+ 106 - 0
RcCore/src/main/java/com/rc/core/ui/activity/RcBaseActivity.kt

@@ -0,0 +1,106 @@
+package com.rc.core.ui.activity
+
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import com.rc.core.ui.ActivityCollector
+import com.rc.core.ui.common.AbsUIDelegate
+import com.rc.core.ui.common.IUIListener
+import com.rc.core.util.ScreenAdapter
+import io.reactivex.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcBaseActivity<VB : ViewBinding> : AppCompatActivity(), IUIListener {
+
+    private lateinit var _viewBinding: VB
+
+    protected val viewBinding get() = _viewBinding
+
+    private lateinit var mUIDelegate: AbsUIDelegate
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        ActivityCollector.addActivity(this)
+        ScreenAdapter.setCustomDensity(this)
+        mUIDelegate = AbsUIDelegate.create()
+        configImmersiveMode()
+        beforeSetContentView()
+        _viewBinding = createViewBinding()
+        setContentView(_viewBinding.root)
+        initViews(savedInstanceState)
+        initListener()
+        initData()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        ScreenAdapter.setCustomDensity(this)
+    }
+
+    protected abstract fun createViewBinding(): VB
+
+    protected open fun configImmersiveMode() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                window.decorView.systemUiVisibility =
+                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+            }
+            window.statusBarColor = statusBarColor()
+        }
+    }
+
+    @ColorInt
+    protected open fun statusBarColor(): Int = Color.WHITE
+
+    protected open fun beforeSetContentView() {
+    }
+
+    protected open fun initViews(savedInstanceState: Bundle?) {
+    }
+
+    protected open fun initListener() {
+    }
+
+    protected open fun initData() {
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mUIDelegate.clearDisposable()
+        ActivityCollector.removeActivity(this)
+    }
+
+    override fun showLoading(message: String?, cancelable: Boolean) {
+        mUIDelegate.showLoading(this, message, cancelable)
+    }
+
+    override fun dismissLoading() {
+        mUIDelegate.dismissLoading()
+    }
+
+    override fun showToast(message: String) {
+        mUIDelegate.showToast(this, message)
+    }
+
+    override fun showNetError(throwable: Throwable) {
+        mUIDelegate.showNetError(this, throwable)
+    }
+
+    override fun addDisposable(disposable: Disposable) {
+        mUIDelegate.addDisposable(disposable)
+    }
+
+    override fun createItemDecoration(): RecyclerView.ItemDecoration? {
+        return mUIDelegate.createItemDecoration(this)
+    }
+
+}

+ 235 - 0
RcCore/src/main/java/com/rc/core/ui/activity/RcRefreshActivity.kt

@@ -0,0 +1,235 @@
+package com.rc.core.ui.activity
+
+import android.os.Bundle
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import androidx.viewbinding.ViewBinding
+import com.chad.library.adapter.base.BaseQuickAdapter
+import com.chad.library.adapter.base.listener.OnItemChildClickListener
+import com.chad.library.adapter.base.listener.OnItemClickListener
+import com.chad.library.adapter.base.listener.OnLoadMoreListener
+import com.chad.library.adapter.base.viewholder.BaseViewHolder
+import com.rc.core.R
+import com.rc.core.event.RefreshEvent
+import com.rc.core.ui.widget.MultipleStatusView
+import com.rc.core.util.net.NetConnectedListener
+import com.rc.core.util.net.NetWatchdog
+import io.reactivex.disposables.Disposable
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcRefreshActivity<T, VB : ViewBinding>
+constructor(val mAdapter: BaseQuickAdapter<T, BaseViewHolder>) : RcBaseActivity<VB>(),
+    OnLoadMoreListener,
+    OnItemClickListener,
+    OnItemChildClickListener {
+
+    companion object {
+        const val FIRST_PAGE = 1
+        const val PAGE_SIZE = 15
+    }
+
+    protected abstract val mMultipleStatusView: MultipleStatusView?
+    protected abstract val mSrlRefresh: SwipeRefreshLayout
+    protected abstract val mRvContent: RecyclerView
+
+    protected var mCurrentPage = FIRST_PAGE
+    protected var mRefresh = false
+
+    private val mNetWatchdog: NetWatchdog by lazy { NetWatchdog(this) }
+    private var mLoadingData = false
+
+    override fun initListener() {
+        mMultipleStatusView?.setOnRetryClickListener { loadData(true) }
+        mSrlRefresh.setOnRefreshListener { loadData(true) }
+
+        initNetWatchdog()
+    }
+
+    override fun initViews(savedInstanceState: Bundle?) {
+        initRecyclerView()
+    }
+
+    private fun initRecyclerView() {
+        mAdapter.setOnItemClickListener(this)
+        mAdapter.setOnItemChildClickListener(this)
+
+        mRvContent.layoutManager = createLayoutManager()
+//        mAdapter.loadMoreModule.loadMoreView = ItLoadMoreView()
+        mAdapter.loadMoreModule.setOnLoadMoreListener(this)
+        mAdapter.loadMoreModule.isAutoLoadMore = true
+        // 当自动加载开启,同时数据不满一屏时,是否继续执行自动加载更多(默认为true)
+        mAdapter.loadMoreModule.isEnableLoadMoreIfNotFullPage = true
+        mAdapter.animationEnable = true
+        mAdapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.SlideInBottom)
+
+        createItemDecoration()?.let {
+            mRvContent.addItemDecoration(it)
+        }
+
+        mRvContent.adapter = mAdapter
+    }
+
+    protected open fun createLayoutManager(): RecyclerView.LayoutManager {
+        return LinearLayoutManager(this)
+    }
+
+    /**
+     * 初始化网络监听
+     */
+    private fun initNetWatchdog() {
+        mNetWatchdog.startWatch()
+        mNetWatchdog.setNetConnectedListener(object : NetConnectedListener {
+            override fun onReNetConnected(isReconnect: Boolean) {
+                mMultipleStatusView?.let {
+                    if (isReconnect) {
+                        it.showContent()
+                    }
+                }
+                if (mAdapter.data.isEmpty() && !mLoadingData && isReconnect) {
+                    loadData(true)
+                }
+            }
+
+            override fun onNetUnConnected() {
+                if (mAdapter.data.isEmpty()) {
+                    mMultipleStatusView?.showNoNetwork()
+                }
+            }
+        })
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        EventBus.getDefault().unregister(this)
+        mNetWatchdog.stopWatch()
+    }
+
+    /**
+     * 数据加载成功的处理逻辑
+     *
+     * @param data 从服务端查询的数据
+     */
+    protected open fun dispatchLoadDataSuccess(data: List<T>?) {
+        mLoadingData = false
+        if (isDestroyed) return
+
+        mMultipleStatusView?.showContent()
+        if (mRefresh) {
+            if (null == data || data.isEmpty()) {
+                if (null == mMultipleStatusView) {
+                    mAdapter.setEmptyView(R.layout.view_list_empty)
+                } else {
+                    mMultipleStatusView!!.showEmpty()
+                }
+            }
+            mAdapter.setNewInstance(data?.toMutableList())
+            mAdapter.loadMoreModule.loadMoreComplete()
+            if (null == data || data.size < PAGE_SIZE) {
+                mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+            }
+            mSrlRefresh.isRefreshing = false
+        } else {
+            if (null == data || data.size < PAGE_SIZE) {
+                if (null != data) {
+                    mAdapter.addData(data)
+                }
+                mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+            } else {
+                mAdapter.addData(data)
+                mAdapter.loadMoreModule.loadMoreComplete()
+            }
+        }
+    }
+
+    /**
+     * 数据加载失败的处理逻辑
+     */
+    protected open fun dispatchLoadDataFailure(throwable: Throwable) {
+        mLoadingData = false
+        if (isDestroyed) return
+
+        if (mRefresh) {
+            if (null == mMultipleStatusView) {
+                mAdapter.setEmptyView(R.layout.view_list_empty)
+            } else {
+                mMultipleStatusView!!.showError()
+            }
+            mSrlRefresh.isRefreshing = false
+        } else {
+            mCurrentPage = if (mCurrentPage-- < FIRST_PAGE) FIRST_PAGE else mCurrentPage
+            mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+        }
+        throwable.printStackTrace()
+        showNetError(throwable)
+    }
+
+    protected open fun goneLoadMoreView() = false
+
+    private fun showLoadingView() {
+        mMultipleStatusView?.let {
+            if (mRefresh) {
+                it.showLoading()
+            }
+        }
+    }
+
+    protected fun loadData(refresh: Boolean) {
+        mLoadingData = true
+        if (refresh) {
+            mCurrentPage = FIRST_PAGE
+        }
+        mRefresh = refresh
+        showLoadingView()
+        queryData()?.let { addDisposable(it) }
+    }
+
+    /**
+     * 查询数据
+     */
+    abstract fun queryData(): Disposable?
+
+    override fun onLoadMore() {
+        val dataSize = mAdapter.data.size
+        if (dataSize < PAGE_SIZE) {
+            mAdapter.loadMoreModule.loadMoreEnd(false)
+        } else {
+            if (dataSize % PAGE_SIZE != 0) {
+                mAdapter.loadMoreModule.loadMoreEnd(false)
+            } else {
+                mCurrentPage++
+                loadData(false)
+            }
+        }
+    }
+
+    override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
+    }
+
+    override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        EventBus.getDefault().register(this)
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    fun onMessageEvent(event: RefreshEvent) {
+        if (event.refresh) {
+            dispatchRefreshEvent(event)
+        }
+    }
+
+    protected open fun dispatchRefreshEvent(event: RefreshEvent) {
+    }
+
+}

+ 36 - 0
RcCore/src/main/java/com/rc/core/ui/common/AbsUIDelegate.kt

@@ -0,0 +1,36 @@
+package com.rc.core.ui.common
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import io.reactivex.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class AbsUIDelegate {
+
+    companion object {
+        fun create() = UIDelegateImpl()
+    }
+
+    abstract fun showLoading(
+        context: Context,
+        message: String? = null,
+        cancelable: Boolean = false
+    )
+
+    abstract fun dismissLoading()
+
+    abstract fun showToast(context: Context?, message: String)
+
+    abstract fun showNetError(context: Context?, throwable: Throwable)
+
+    abstract fun addDisposable(disposable: Disposable)
+
+    abstract fun clearDisposable()
+
+    abstract fun createItemDecoration(context: Context?): RecyclerView.ItemDecoration?
+
+}

+ 25 - 0
RcCore/src/main/java/com/rc/core/ui/common/IUIListener.kt

@@ -0,0 +1,25 @@
+package com.rc.core.ui.common
+
+import androidx.recyclerview.widget.RecyclerView
+import io.reactivex.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+interface IUIListener {
+
+    fun showLoading(message: String? = null, cancelable: Boolean = false)
+
+    fun dismissLoading()
+
+    fun showToast(message: String)
+
+    fun showNetError(throwable: Throwable)
+
+    fun addDisposable(disposable: Disposable)
+
+    fun createItemDecoration(): RecyclerView.ItemDecoration?
+
+}

+ 93 - 0
RcCore/src/main/java/com/rc/core/ui/common/UIDelegateImpl.kt

@@ -0,0 +1,93 @@
+package com.rc.core.ui.common
+
+import android.content.Context
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.RecyclerView
+import com.rc.core.R
+import com.rc.core.ui.dialog.LoadingDialog
+import com.rc.core.ui.widget.decoration.NoLastLineItemDecoration
+import com.rc.httpcore.exception.NetException
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.disposables.Disposable
+import retrofit2.HttpException
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class UIDelegateImpl : AbsUIDelegate() {
+
+    private var mPdLoading: LoadingDialog? = null
+
+    override fun showLoading(
+        context: Context,
+        message: String?,
+        cancelable: Boolean
+    ) {
+        if (null == mPdLoading) {
+            mPdLoading = LoadingDialog(context, message)
+        }
+        mPdLoading?.let {
+            it.setMessage(message)
+            it.setCancelable(cancelable)
+            it.show()
+        }
+    }
+
+    override fun dismissLoading() {
+        mPdLoading?.let {
+            it.dismiss()
+            mPdLoading = null
+        }
+    }
+
+    override fun showToast(context: Context?, message: String) {
+        context?.let {
+            Toast.makeText(it, message, Toast.LENGTH_SHORT).show()
+        }
+    }
+
+    override fun showNetError(context: Context?, throwable: Throwable) {
+        when (throwable) {
+            is NetException -> {
+                if (throwable.message.isNullOrEmpty()) {
+                    "接口请求失败(${throwable.code})"
+                } else {
+                    throwable.message!!
+                }
+            }
+            is SocketTimeoutException -> "请求超时,请稍后重试"
+            is ConnectException -> "无法连接服务器,请检查网络"
+            is HttpException -> "服务器繁忙,请稍后重试"
+            else -> null
+        }?.let { showToast(context, it) }
+    }
+
+    private var mCompositeDisposable: CompositeDisposable? = null
+
+    override fun addDisposable(disposable: Disposable) {
+        mCompositeDisposable = (mCompositeDisposable ?: CompositeDisposable()).apply {
+            if (!isDisposed) add(disposable)
+        }
+    }
+
+    override fun clearDisposable() {
+        mCompositeDisposable = mCompositeDisposable?.let {
+            it.clear()
+            null
+        }
+    }
+
+    override fun createItemDecoration(context: Context?): RecyclerView.ItemDecoration? {
+        return context?.let {
+            NoLastLineItemDecoration(it, DividerItemDecoration.VERTICAL).apply {
+                setDrawable(ContextCompat.getDrawable(it, R.drawable.shape_item_divider)!!)
+            }
+        }
+    }
+}

+ 101 - 0
RcCore/src/main/java/com/rc/core/ui/dialog/LoadingDialog.kt

@@ -0,0 +1,101 @@
+package com.rc.core.ui.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.*
+import com.rc.core.R
+import com.rc.core.databinding.DialogLoadingBinding
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class LoadingDialog(context: Context, private val message: String? = null) :
+    Dialog(context, R.style.LoadingDialog) {
+
+    fun setMessage(message: String?) {
+        viewBinding.message.visibility = if (message.isNullOrEmpty()) View.GONE else View.VISIBLE
+        viewBinding.message.text = message
+    }
+
+    private val viewBinding: DialogLoadingBinding by lazy {
+        DialogLoadingBinding.inflate(LayoutInflater.from(context))
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(viewBinding.root)
+
+        setMessage(message)
+    }
+
+}
+
+//class LoadingDialog : DialogFragment() {
+//
+//    override fun onCreate(savedInstanceState: Bundle?) {
+//        super.onCreate(savedInstanceState)
+//        setStyle(STYLE_NO_TITLE, R.style.LoadingDialog)
+//    }
+//
+//    private lateinit var mViewBinding: DialogLoadingBinding
+//
+//    override fun onCreateView(
+//        inflater: LayoutInflater,
+//        container: ViewGroup?,
+//        savedInstanceState: Bundle?
+//    ): View {
+//        mViewBinding = DialogLoadingBinding.inflate(inflater, container, false)
+//        return mViewBinding.root
+//    }
+//
+//    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+//        super.onViewCreated(view, savedInstanceState)
+//
+//        mViewBinding.message.visibility = if (mMessage.isNullOrEmpty()) View.GONE else View.VISIBLE
+//        mViewBinding.message.text = mMessage
+//    }
+//
+//    override fun onStart() {
+//        super.onStart()
+//        initWindowConfig()
+//    }
+//
+//    private fun initWindowConfig() {
+//        dialog?.window?.apply {
+//            setWindowAnimations(R.style.PopWindowAnimStyle)
+//            setGravity(Gravity.CENTER)
+//            attributes.width = WindowManager.LayoutParams.MATCH_PARENT
+//            attributes.height = WindowManager.LayoutParams.WRAP_CONTENT
+//        }
+//    }
+//
+//    private var mMessage: CharSequence? = null
+//
+//    fun setMessage(message: CharSequence?): LoadingDialog {
+//        this.mMessage = message
+//        return this
+//    }
+//
+//    private var addTag = false
+//
+//    fun show(manager: FragmentManager): LoadingDialog {
+//        if (addTag) return this
+//        addTag = true
+//
+//        if (this.isAdded || null != manager.findFragmentByTag(LoadingDialog::class.simpleName)) {
+//            manager.beginTransaction().remove(this).commit()
+//        }
+//        show(manager, LoadingDialog::class.simpleName)
+//
+//        Handler().post {
+//            addTag = false
+//        }
+//
+//        return this
+//    }
+//
+//}

+ 112 - 0
RcCore/src/main/java/com/rc/core/ui/dialog/RcBaseDialog.kt

@@ -0,0 +1,112 @@
+package com.rc.core.ui.dialog
+
+import android.os.Bundle
+import android.view.*
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import com.rc.core.R
+import com.rc.core.ui.common.AbsUIDelegate
+import com.rc.core.ui.common.IUIListener
+import io.reactivex.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcBaseDialog<VB : ViewBinding> :
+    DialogFragment(), IUIListener {
+
+    private lateinit var _viewBinding: VB
+
+    protected val viewBinding get() = _viewBinding
+
+    private lateinit var mUIDelegate: AbsUIDelegate
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mUIDelegate = AbsUIDelegate.create()
+
+        setStyle(STYLE_NO_TITLE, R.style.LoadingDialog)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        initWindowConfig()
+        _viewBinding = createViewBinding(inflater, container)
+        return _viewBinding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initViews(savedInstanceState)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        initDialogSize()
+    }
+
+    protected open fun initDialogSize() {
+        dialog?.window?.apply {
+            attributes.width = WindowManager.LayoutParams.MATCH_PARENT
+            attributes.height = WindowManager.LayoutParams.WRAP_CONTENT
+        }
+    }
+
+    protected open fun initWindowConfig() {
+//        dialog?.window?.apply {
+//            setWindowAnimations(R.style.BottomDialog_Animation)
+//            setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+//        }
+//
+//        dialog?.window?.apply {
+//            attributes.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
+//            attributes.windowAnimations = R.style.BottomDialog_Animation
+//            requestFeature(Window.FEATURE_NO_TITLE)
+//            setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+//        }
+//        dialog?.setCanceledOnTouchOutside(true)
+    }
+
+    protected abstract fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): VB
+
+    protected abstract fun initViews(savedInstanceState: Bundle?)
+
+    override fun showLoading(message: String?, cancelable: Boolean) {
+        mUIDelegate.showLoading(requireContext(), message, cancelable)
+    }
+
+    override fun dismissLoading() {
+        mUIDelegate.dismissLoading()
+    }
+
+    override fun showToast(message: String) {
+        mUIDelegate.showToast(context, message)
+    }
+
+    override fun showNetError(throwable: Throwable) {
+        mUIDelegate.showNetError(context, throwable)
+    }
+
+    override fun addDisposable(disposable: Disposable) {
+        mUIDelegate.addDisposable(disposable)
+    }
+
+    override fun onDestroyView() {
+        mUIDelegate.clearDisposable()
+        super.onDestroyView()
+    }
+
+    override fun createItemDecoration(): RecyclerView.ItemDecoration? {
+        return mUIDelegate.createItemDecoration(context)
+    }
+
+}

+ 83 - 0
RcCore/src/main/java/com/rc/core/ui/fragment/RcBaseFragment.kt

@@ -0,0 +1,83 @@
+package com.rc.core.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import com.rc.core.ui.common.AbsUIDelegate
+import com.rc.core.ui.common.IUIListener
+import io.reactivex.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcBaseFragment<VB : ViewBinding> :
+    Fragment(), IUIListener {
+
+    private lateinit var _viewBinding: VB
+
+    protected val viewBinding get() = _viewBinding
+
+    private lateinit var mUIDelegate: AbsUIDelegate
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mUIDelegate = AbsUIDelegate.create()
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        _viewBinding = createViewBinding(inflater, container)
+        return _viewBinding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initViews(savedInstanceState)
+    }
+
+    protected abstract fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): VB
+
+    protected abstract fun initViews(savedInstanceState: Bundle?)
+
+    override fun showLoading(message: String?, cancelable: Boolean) {
+        context?.let { mUIDelegate.showLoading(it, message, cancelable) }
+    }
+
+    override fun dismissLoading() {
+        mUIDelegate.dismissLoading()
+    }
+
+    override fun showToast(message: String) {
+        mUIDelegate.showToast(context, message)
+    }
+
+    override fun showNetError(throwable: Throwable) {
+        mUIDelegate.showNetError(context, throwable)
+    }
+
+    override fun addDisposable(disposable: Disposable) {
+        mUIDelegate.addDisposable(disposable)
+    }
+
+    override fun onDestroyView() {
+        mUIDelegate.clearDisposable()
+        super.onDestroyView()
+    }
+
+    override fun createItemDecoration(): RecyclerView.ItemDecoration? {
+        return mUIDelegate.createItemDecoration(context)
+    }
+
+}

+ 37 - 0
RcCore/src/main/java/com/rc/core/ui/fragment/RcLazyFragment.kt

@@ -0,0 +1,37 @@
+package com.rc.core.ui.fragment
+
+import androidx.viewbinding.ViewBinding
+
+/**
+ * 懒加载
+ *
+ * @author ReiChin_
+ */
+abstract class RcLazyFragment<VB : ViewBinding> : RcBaseFragment<VB>() {
+
+    override fun onResume() {
+        super.onResume()
+        lazyLoad()
+    }
+
+    private fun lazyLoad() {
+        if (!loadDateCompleted()) {
+            onLoadData()
+        }
+    }
+
+    /**
+     * 加载数据
+     */
+    protected abstract fun onLoadData()
+
+    /**
+     * 数据是否加载完毕,子类可覆写此方法,根据需要返回。
+     * 1) false,每次画面可见时,都会回调[.onLoadData]。
+     * 2) true,每次画面可见时,不会回调[.onLoadData]。
+     *
+     * @return 数据是否加载完成。
+     */
+    protected open fun loadDateCompleted() = false
+
+}

+ 225 - 0
RcCore/src/main/java/com/rc/core/ui/fragment/RcRefreshFragment.kt

@@ -0,0 +1,225 @@
+package com.rc.core.ui.fragment
+
+import android.os.Bundle
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import androidx.viewbinding.ViewBinding
+import com.chad.library.adapter.base.BaseQuickAdapter
+import com.chad.library.adapter.base.listener.OnItemChildClickListener
+import com.chad.library.adapter.base.listener.OnItemClickListener
+import com.chad.library.adapter.base.listener.OnLoadMoreListener
+import com.chad.library.adapter.base.viewholder.BaseViewHolder
+import com.rc.core.event.RefreshEvent
+import com.rc.core.ui.widget.MultipleStatusView
+import com.rc.core.util.net.NetConnectedListener
+import com.rc.core.util.net.NetWatchdog
+import io.reactivex.disposables.Disposable
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcRefreshFragment<T, VB : ViewBinding>
+constructor(val mAdapter: BaseQuickAdapter<T, BaseViewHolder>) : RcLazyFragment<VB>(),
+    OnLoadMoreListener,
+    OnItemClickListener,
+    OnItemChildClickListener {
+
+    companion object {
+        protected const val FIRST_PAGE = 1
+        protected const val PAGE_SIZE = 15
+    }
+
+    protected abstract val mMultipleStatusView: MultipleStatusView?
+    protected abstract val mSrlRefresh: SwipeRefreshLayout
+    protected abstract val mRvContent: RecyclerView
+
+    protected var mCurrentPage = FIRST_PAGE
+    protected var mRefresh = false
+
+    private val mNetWatchdog: NetWatchdog by lazy { NetWatchdog(context!!) }
+    private var mLoadingData = false
+
+    override fun initViews(savedInstanceState: Bundle?) {
+        initNetWatchdog()
+        initSwipeRecyclerView()
+    }
+
+    private fun initSwipeRecyclerView() {
+        mMultipleStatusView?.setOnRetryClickListener { loadData(true) }
+        mSrlRefresh.setOnRefreshListener { loadData(true) }
+
+        mAdapter.setOnItemClickListener(this)
+        mAdapter.setOnItemChildClickListener(this)
+
+        mRvContent.layoutManager = LinearLayoutManager(context)
+//        mAdapter.loadMoreModule.loadMoreView = ItLoadMoreView()
+        mAdapter.loadMoreModule.setOnLoadMoreListener(this)
+        mAdapter.loadMoreModule.isAutoLoadMore = true
+        // 当自动加载开启,同时数据不满一屏时,是否继续执行自动加载更多(默认为true)
+        mAdapter.loadMoreModule.isEnableLoadMoreIfNotFullPage = true
+        mAdapter.animationEnable = true
+        mAdapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.SlideInBottom)
+
+        createItemDecoration()?.let {
+            mRvContent.addItemDecoration(it)
+        }
+
+        mRvContent.adapter = mAdapter
+    }
+
+    /**
+     * 初始化网络监听
+     */
+    private fun initNetWatchdog() {
+        mNetWatchdog.startWatch()
+        mNetWatchdog.setNetConnectedListener(object : NetConnectedListener {
+            override fun onReNetConnected(isReconnect: Boolean) {
+                mMultipleStatusView?.let {
+                    if (isReconnect) {
+                        it.showContent()
+                    }
+                }
+                if (mAdapter.data.isEmpty() && !mLoadingData && isReconnect) {
+                    loadData(true)
+                }
+            }
+
+            override fun onNetUnConnected() {
+                if (mAdapter.data.isEmpty()) {
+                    mMultipleStatusView?.showNoNetwork()
+                }
+            }
+        })
+    }
+
+    override fun onDestroyView() {
+        mNetWatchdog.stopWatch()
+        super.onDestroyView()
+    }
+
+    protected fun loadData(refresh: Boolean) {
+        mLoadingData = true
+        if (refresh) {
+            mCurrentPage = FIRST_PAGE
+        }
+        mRefresh = refresh
+        showLoadingView()
+        val disposable = queryData()
+        addDisposable(disposable)
+    }
+
+    abstract fun queryData(): Disposable
+
+    override fun onLoadData() {
+        loadData(true)
+    }
+
+    override fun onLoadMore() {
+        val dataSize = mAdapter.data.size
+        if (dataSize < PAGE_SIZE) {
+            mAdapter.loadMoreModule.loadMoreEnd(false)
+        } else {
+            if (dataSize % PAGE_SIZE != 0) {
+                mAdapter.loadMoreModule.loadMoreEnd(false)
+            } else {
+                mCurrentPage++
+                loadData(false)
+            }
+        }
+    }
+
+    /**
+     * 数据加载成功的处理逻辑
+     *
+     * @param data 从服务端查询的数据
+     */
+    protected open fun dispatchLoadDataSuccess(data: List<T>?) {
+        mLoadingData = false
+        if (isDetached) return
+
+        mMultipleStatusView?.showContent()
+        if (mRefresh) {
+            if (null == data || data.isEmpty()) {
+                mMultipleStatusView?.showEmpty()
+            }
+            mAdapter.setNewInstance(data?.toMutableList())
+            mAdapter.loadMoreModule.loadMoreComplete()
+            if (null == data || data.size < PAGE_SIZE) {
+                mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+            }
+            mSrlRefresh.isRefreshing = false
+        } else {
+            if (null == data || data.size < PAGE_SIZE) {
+                if (null != data) {
+                    mAdapter.addData(data)
+                }
+                mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+            } else {
+                mAdapter.addData(data)
+                mAdapter.loadMoreModule.loadMoreComplete()
+            }
+        }
+    }
+
+    /**
+     * 数据加载失败的处理逻辑
+     */
+    protected open fun dispatchLoadDataFailure() {
+        mLoadingData = false
+        if (isDetached) return
+
+        if (mRefresh) {
+            mMultipleStatusView?.showError()
+            mSrlRefresh.isRefreshing = false
+        } else {
+            mCurrentPage = if (mCurrentPage-- < FIRST_PAGE) FIRST_PAGE else mCurrentPage
+            mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+        }
+    }
+
+    protected open fun goneLoadMoreView() = false
+
+    private fun showLoadingView() {
+        mMultipleStatusView?.let {
+            if (mRefresh) {
+                it.showLoading()
+            }
+        }
+    }
+
+    override fun loadDateCompleted() = mAdapter.data.isNotEmpty()
+
+    override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
+    }
+
+    override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        EventBus.getDefault().register(this)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        EventBus.getDefault().unregister(this)
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    fun onMessageEvent(event: RefreshEvent) {
+        if (event.refresh) {
+            dispatchRefreshEvent(event)
+        }
+    }
+
+    protected open fun dispatchRefreshEvent(event: RefreshEvent) {
+    }
+
+}

+ 301 - 0
RcCore/src/main/java/com/rc/core/ui/widget/MultipleStatusView.kt

@@ -0,0 +1,301 @@
+package com.rc.core.ui.widget
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import androidx.annotation.LayoutRes
+import com.rc.core.R
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class MultipleStatusView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) :
+    RelativeLayout(context, attrs, defStyleAttr) {
+
+    companion object {
+        private const val STATUS_CONTENT = 0x00
+        private const val STATUS_LOADING = 0x01
+        private const val STATUS_EMPTY = 0x02
+        private const val STATUS_ERROR = 0x03
+        private const val STATUS_NO_NETWORK = 0x04
+
+        private val DEFAULT_LAYOUT_PARAMS =
+            LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
+
+        private const val NULL_RESOURCE_ID = -1
+    }
+
+    private var mEmptyView: View? = null
+    private var mErrorView: View? = null
+    private var mLoadingView: View? = null
+    private var mNoNetworkView: View? = null
+    private var mContentView: View? = null
+    private var mEmptyViewResId = NULL_RESOURCE_ID
+    private var mErrorViewResId = NULL_RESOURCE_ID
+    private var mLoadingViewResId = NULL_RESOURCE_ID
+    private var mNoNetworkViewResId = NULL_RESOURCE_ID
+    private var mContentViewResId = NULL_RESOURCE_ID
+
+    private var mViewStatus = -1
+
+    private val mOtherIds = ArrayList<Int>()
+
+    init {
+        val a: TypedArray =
+            context.obtainStyledAttributes(attrs, R.styleable.MultipleStatusView, defStyleAttr, 0)
+        mEmptyViewResId = a.getResourceId(
+            R.styleable.MultipleStatusView_emptyView,
+            R.layout.view_multiple_empty_view
+        )
+        mErrorViewResId = a.getResourceId(
+            R.styleable.MultipleStatusView_errorView,
+            R.layout.view_multiple_error_view
+        )
+        mLoadingViewResId = a.getResourceId(
+            R.styleable.MultipleStatusView_loadingView,
+            R.layout.view_multiple_loading_view
+        )
+        mNoNetworkViewResId = a.getResourceId(
+            R.styleable.MultipleStatusView_noNetworkView,
+            R.layout.view_multiple_no_network_view
+        )
+        mContentViewResId =
+            a.getResourceId(R.styleable.MultipleStatusView_contentView, NULL_RESOURCE_ID)
+        a.recycle()
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        showContent()
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        clear(mEmptyView, mLoadingView, mErrorView, mNoNetworkView)
+        if (mOtherIds.isNotEmpty()) {
+            mOtherIds.clear()
+        }
+        if (null != mOnRetryClickListener) {
+            mOnRetryClickListener = null
+        }
+        if (null != mViewStatusListener) {
+            mViewStatusListener = null
+        }
+    }
+
+    /**
+     * 获取当前状态
+     *
+     * @return 视图状态
+     */
+    fun getViewStatus() = mViewStatus
+
+    private var mOnRetryClickListener: ((view: View) -> Unit)? = null
+
+    /**
+     * 设置重试点击事件
+     *
+     * @param listener 重试点击事件
+     */
+    fun setOnRetryClickListener(listener: (view: View) -> Unit) {
+        this.mOnRetryClickListener = listener
+    }
+
+    /**
+     * 显示空视图
+     *
+     * @param view         自定义视图
+     * @param layoutParams 布局参数
+     */
+    fun showEmpty(
+        @LayoutRes layoutId: Int = mEmptyViewResId,
+        view: View = mEmptyView ?: View.inflate(context, layoutId, null),
+        layoutParams: ViewGroup.LayoutParams = DEFAULT_LAYOUT_PARAMS
+    ) {
+        changeViewStatus(STATUS_EMPTY)
+        if (null == mEmptyView) {
+            mEmptyView = view
+            val emptyRetryView: View? = mEmptyView!!.findViewById(R.id.empty_retry_view)
+            if (null != mOnRetryClickListener && null != emptyRetryView) {
+                emptyRetryView.setOnClickListener(mOnRetryClickListener)
+            }
+            mOtherIds.add(mEmptyView!!.id)
+            addView(mEmptyView, 0, layoutParams)
+        }
+        showViewById(mEmptyView!!.id)
+    }
+
+    /**
+     * 显示错误视图
+     *
+     * @param view         自定义视图
+     * @param layoutParams 布局参数
+     */
+    fun showError(
+        @LayoutRes layoutId: Int = mErrorViewResId,
+        view: View = mErrorView ?: View.inflate(context, layoutId, null),
+        layoutParams: ViewGroup.LayoutParams = DEFAULT_LAYOUT_PARAMS
+    ) {
+        changeViewStatus(STATUS_ERROR)
+        if (null == mErrorView) {
+            mErrorView = view
+            val errorRetryView: View? = mErrorView!!.findViewById(R.id.error_retry_view)
+            if (null != mOnRetryClickListener && null != errorRetryView) {
+                errorRetryView.setOnClickListener(mOnRetryClickListener)
+            }
+            mOtherIds.add(mErrorView!!.id)
+            addView(mErrorView, 0, layoutParams)
+        }
+        showViewById(mErrorView!!.id)
+    }
+
+    /**
+     * 显示加载中视图
+     *
+     * @param view         自定义视图
+     * @param layoutParams 布局参数
+     */
+    fun showLoading(
+        @LayoutRes layoutId: Int = mLoadingViewResId,
+        view: View = mLoadingView ?: View.inflate(context, layoutId, null),
+        layoutParams: ViewGroup.LayoutParams = DEFAULT_LAYOUT_PARAMS
+    ) {
+        changeViewStatus(STATUS_LOADING)
+        if (null == mLoadingView) {
+            mLoadingView = view
+            mOtherIds.add(mLoadingView!!.id)
+            addView(mLoadingView, 0, layoutParams)
+        }
+        showViewById(mLoadingView!!.id)
+    }
+
+    /**
+     * 显示无网络视图
+     *
+     * @param view         自定义视图
+     * @param layoutParams 布局参数
+     */
+    fun showNoNetwork(
+        @LayoutRes layoutId: Int = mNoNetworkViewResId,
+        view: View = mNoNetworkView ?: View.inflate(context, layoutId, null),
+        layoutParams: ViewGroup.LayoutParams = DEFAULT_LAYOUT_PARAMS
+    ) {
+        changeViewStatus(STATUS_NO_NETWORK)
+        if (null == mNoNetworkView) {
+            mNoNetworkView = view
+            val noNetworkRetryView: View? =
+                mNoNetworkView!!.findViewById(R.id.no_network_retry_view)
+            if (null != mOnRetryClickListener && null != noNetworkRetryView) {
+                noNetworkRetryView.setOnClickListener(mOnRetryClickListener)
+            }
+            mOtherIds.add(mNoNetworkView!!.id)
+            addView(mNoNetworkView, 0, layoutParams)
+        }
+        showViewById(mNoNetworkView!!.id)
+    }
+
+    /**
+     * 显示内容视图
+     *
+     * @param view         自定义视图
+     * @param layoutParams 布局参数
+     */
+    fun showContent(
+        @LayoutRes layoutId: Int = mContentViewResId,
+        view: View? = mContentView,
+        layoutParams: ViewGroup.LayoutParams = DEFAULT_LAYOUT_PARAMS
+    ) {
+        // ?: View.inflate(context, layoutId, null)
+
+        changeViewStatus(STATUS_CONTENT)
+
+        if (null == mContentView) {
+            if (NULL_RESOURCE_ID != layoutId) {
+                mContentView = View.inflate(context, layoutId, null)
+                addView(mContentView, 0, layoutParams)
+            } else {
+                showContentView()
+                return
+            }
+        }
+
+
+//        if (NULL_RESOURCE_ID == layoutId && null == mContentView) {
+////            mContentView = View.inflate(context, layoutId, null)
+////            addView(mContentView, 0, layoutParams)
+//        }
+
+
+//        if (NULL_RESOURCE_ID == layoutId && mContentView == null) {
+//            showContentView()
+//        } else {
+        clear(mContentView)
+        mContentView = view
+        addView(mContentView, 0, layoutParams)
+        showViewById(mContentView!!.id)
+//        }
+    }
+
+    private fun showViewById(viewId: Int) {
+        for (i in 0 until childCount) {
+            val view = getChildAt(i)
+            view.visibility = if (view.id == viewId) View.VISIBLE else View.GONE
+        }
+    }
+
+    private fun showContentView() {
+        for (i in 0 until childCount) {
+            val view = getChildAt(i)
+            view.visibility = if (mOtherIds.contains(view.id)) View.GONE else View.VISIBLE
+        }
+    }
+
+    private fun checkNull(obj: Any?, hint: String) {
+        if (null == obj) {
+            throw NullPointerException(hint)
+        }
+    }
+
+    private fun clear(vararg views: View?) {
+        try {
+            for (view in views) {
+                view?.let { removeView(it) }
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    // 视图状态改变接口
+    private var mViewStatusListener: ((oldViewStatus: Int, newViewStatus: Int) -> Unit)? = null
+
+    /**
+     * 设置视图状态改变监听事件
+     */
+    fun setOnViewStatusChangeListener(listener: (oldViewStatus: Int, newViewStatus: Int) -> Unit) {
+        this.mViewStatusListener = listener
+    }
+
+    /**
+     * 改变视图状态
+     *
+     * @param newViewStatus 新的视图状态
+     */
+    private fun changeViewStatus(newViewStatus: Int) {
+        if (mViewStatus == newViewStatus) {
+            return
+        }
+        mViewStatusListener?.invoke(mViewStatus, newViewStatus)
+        mViewStatus = newViewStatus
+    }
+
+}

+ 53 - 0
RcCore/src/main/java/com/rc/core/ui/widget/decoration/GridSpacingItemDecoration.java

@@ -0,0 +1,53 @@
+package com.rc.core.ui.widget.decoration;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * FileName: GridSpacingItemDecoration
+ * Description:
+ */
+public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final int spanCount; //列数
+    private final int horizontalSpacing; // 水平间隔
+    private final int verticalSpacing; // 垂直间隔
+    private final boolean includeEdge; //是否包含边缘
+
+    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
+        this(spanCount, spacing, spacing, includeEdge);
+    }
+
+    public GridSpacingItemDecoration(int spanCount, int horizontalSpacing, int verticalSpacing, boolean includeEdge) {
+        this.spanCount = spanCount;
+        this.horizontalSpacing = horizontalSpacing;
+        this.verticalSpacing = verticalSpacing;
+        this.includeEdge = includeEdge;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+
+        //这里是关键,需要根据你有几列来判断
+        int position = parent.getChildAdapterPosition(view); // item position
+        int column = position % spanCount; // item column
+
+        if (includeEdge) {
+            outRect.left = horizontalSpacing - column * horizontalSpacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
+            outRect.right = (column + 1) * horizontalSpacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
+
+            if (position < spanCount) { // top edge
+                outRect.top = verticalSpacing;
+            }
+            outRect.bottom = verticalSpacing; // item bottom
+        } else {
+            outRect.left = column * horizontalSpacing / spanCount; // column * ((1f / spanCount) * spacing)
+            outRect.right = horizontalSpacing - (column + 1) * horizontalSpacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
+            if (position >= spanCount) {
+                outRect.top = verticalSpacing; // item top
+            }
+        }
+    }
+}

+ 148 - 0
RcCore/src/main/java/com/rc/core/ui/widget/decoration/NoLastLineItemDecoration.java

@@ -0,0 +1,148 @@
+package com.rc.core.ui.widget.decoration;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class NoLastLineItemDecoration extends RecyclerView.ItemDecoration {
+    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+    public static final int VERTICAL = LinearLayout.VERTICAL;
+
+    private static final String TAG = "DividerItem";
+    private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
+
+    private Drawable mDivider;
+
+    /**
+     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
+     */
+    private int mOrientation;
+
+    private final Rect mBounds = new Rect();
+
+    public NoLastLineItemDecoration(Context context, int orientation) {
+        final TypedArray a = context.obtainStyledAttributes(ATTRS);
+        mDivider = a.getDrawable(0);
+        if (mDivider == null) {
+            Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+                    + "DividerItemDecoration. Please set that attribute all call setDrawable()");
+        }
+        a.recycle();
+        setOrientation(orientation);
+    }
+
+    /**
+     * Sets the orientation for this divider. This should be called if
+     * {@link RecyclerView.LayoutManager} changes orientation.
+     *
+     * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+     */
+    public void setOrientation(int orientation) {
+        if (orientation != HORIZONTAL && orientation != VERTICAL) {
+            throw new IllegalArgumentException(
+                    "Invalid orientation. It should be either HORIZONTAL or VERTICAL");
+        }
+        mOrientation = orientation;
+    }
+
+    /**
+     * Sets the {@link Drawable} for this divider.
+     *
+     * @param drawable Drawable that should be used as a divider.
+     */
+    public void setDrawable(@NonNull Drawable drawable) {
+        if (drawable == null) {
+            throw new IllegalArgumentException("Drawable cannot be null.");
+        }
+        mDivider = drawable;
+    }
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        if (parent.getLayoutManager() == null || mDivider == null) {
+            return;
+        }
+        if (mOrientation == VERTICAL) {
+            drawVertical(c, parent);
+        } else {
+            drawHorizontal(c, parent);
+        }
+    }
+
+    private void drawVertical(Canvas canvas, RecyclerView parent) {
+        canvas.save();
+        final int left;
+        final int right;
+        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
+        if (parent.getClipToPadding()) {
+            left = parent.getPaddingLeft();
+            right = parent.getWidth() - parent.getPaddingRight();
+            canvas.clipRect(left, parent.getPaddingTop(), right,
+                    parent.getHeight() - parent.getPaddingBottom());
+        } else {
+            left = 0;
+            right = parent.getWidth();
+        }
+
+        final int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            final View child = parent.getChildAt(i);
+            parent.getDecoratedBoundsWithMargins(child, mBounds);
+            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
+            final int top = bottom - mDivider.getIntrinsicHeight();
+            mDivider.setBounds(left, top, right, bottom);
+            mDivider.draw(canvas);
+        }
+        canvas.restore();
+    }
+
+    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
+        canvas.save();
+        final int top;
+        final int bottom;
+        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
+        if (parent.getClipToPadding()) {
+            top = parent.getPaddingTop();
+            bottom = parent.getHeight() - parent.getPaddingBottom();
+            canvas.clipRect(parent.getPaddingLeft(), top,
+                    parent.getWidth() - parent.getPaddingRight(), bottom);
+        } else {
+            top = 0;
+            bottom = parent.getHeight();
+        }
+
+        final int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            final View child = parent.getChildAt(i);
+            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
+            final int right = mBounds.right + Math.round(child.getTranslationX());
+            final int left = right - mDivider.getIntrinsicWidth();
+            mDivider.setBounds(left, top, right, bottom);
+            mDivider.draw(canvas);
+        }
+        canvas.restore();
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                               RecyclerView.State state) {
+        if (mDivider == null) {
+            outRect.set(0, 0, 0, 0);
+            return;
+        }
+        if (mOrientation == VERTICAL) {
+            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
+        } else {
+            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
+        }
+    }
+}
+

+ 195 - 0
RcCore/src/main/java/com/rc/core/util/ApkController.kt

@@ -0,0 +1,195 @@
+package com.rc.core.util
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Process
+import androidx.core.content.FileProvider
+import com.rc.core.ui.ActivityCollector
+import java.io.*
+import java.nio.charset.Charset
+import kotlin.system.exitProcess
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object ApkController {
+
+    fun installAPK(
+        apkFile: File,
+        appContext: Context,
+        applicationId: String
+    ) {
+        if (isRootPermission()) {
+            install(apkFile.path)
+//            startApp("com.dlc.exam", "com.dlc.exam.ui.SplashActivity")
+            startApp(appContext, "com.dlc.exam", "com.dlc.exam.ui.SplashActivity")
+            // silentInstall(appContext, apkFile.path)
+        } else {
+            installOnTask(apkFile, appContext, applicationId)
+        }
+    }
+
+    /**
+     * 判定app是否获取root权限
+     */
+    fun isRootPermission(): Boolean {
+        try {
+            val process = Runtime.getRuntime().exec("su")
+            val os = DataOutputStream(process.outputStream)
+            os.writeBytes("ls /data/data/\n")
+            os.writeBytes("exit\n")
+            os.flush()
+            val reader = BufferedReader(InputStreamReader(process.inputStream))
+            val builder = StringBuilder()
+            var line: String? = null
+            while (reader.readLine().also { line = it } != null) {
+                builder.append(line)
+                builder.append(System.getProperty("line.separator"))
+            }
+            val result = builder.toString()
+            if (result.contains("com.android.phone")) {
+                return true
+            }
+        } catch (e: IOException) {
+            return false
+        }
+        return false
+    }
+
+    /**
+     * 安装apk
+     */
+    fun install(apkPath: String): Boolean {
+        var result = false
+        var dataOutputStream: DataOutputStream? = null
+        var errorStream: BufferedReader? = null
+        try {
+            val process = Runtime.getRuntime().exec("su")
+            dataOutputStream = DataOutputStream(process.outputStream)
+            val command = "pm install -r $apkPath\n"
+            dataOutputStream.write(command.toByteArray(Charset.forName("utf-8")))
+            dataOutputStream.flush()
+            dataOutputStream.writeBytes("exit\n")
+            dataOutputStream.flush()
+            process.waitFor()
+            errorStream = BufferedReader(InputStreamReader(process.errorStream))
+            var msg = ""
+            var line: String
+            while (errorStream.readLine().also { line = it } != null) {
+                msg += line
+            }
+            if (!msg.contains("Failure")) {
+                result = true
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        } finally {
+            try {
+                dataOutputStream?.close()
+                errorStream?.close()
+            } catch (e: IOException) {
+            }
+        }
+        return result
+    }
+
+    fun startApp(appContext: Context, pkg: String, cls: String) {
+        val intent = Intent.makeRestartActivityTask(ComponentName(pkg, cls))
+        appContext.startActivity(intent)
+        exitProcess(0)
+    }
+
+    /**
+     * 启动app
+     */
+    fun startApp(packageName: String, activityName: String): Boolean {
+        var result = false
+        var dataOutputStream: DataOutputStream? = null
+        var errorStream: BufferedReader? = null
+        try {
+            val process = Runtime.getRuntime().exec("su")
+            dataOutputStream = DataOutputStream(process.outputStream)
+            val command = "sleep 120; am start -n $packageName/$activityName\n"
+            dataOutputStream.write(command.toByteArray(Charset.forName("utf-8")))
+            dataOutputStream.flush()
+            dataOutputStream.writeBytes("exit\n")
+            dataOutputStream.flush()
+            process.waitFor()
+            errorStream = BufferedReader(InputStreamReader(process.errorStream))
+            var msg = ""
+            var line: String
+            while (errorStream.readLine().also { line = it } != null) {
+                msg += line
+            }
+            if (!msg.contains("Failure")) {
+                result = true
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        } finally {
+            try {
+                dataOutputStream?.close()
+                errorStream?.close()
+            } catch (e: IOException) {
+            }
+        }
+        return result
+    }
+
+    /**
+     * 静默安装
+     *
+     * @param context
+     * @param apkPath
+     * @param autoStart 安装后是否自动启动
+     */
+    fun silentInstall(context: Context, apkPath: String, autoStart: Boolean = true) {
+        val intent = Intent().apply {
+            action = "android.intent.action.installslient"
+            putExtra("uri", apkPath)
+            putExtra("component", "com.dlc.laboratory/com.dlc.laboratory.ui.SplashActivity")
+            putExtra("enable", autoStart)
+        }
+        context.sendBroadcast(intent)
+    }
+
+    fun installOnTask(
+        apkFile: File,
+        appContext: Context,
+        applicationId: String
+    ) {
+        val intent = Intent()
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        intent.action = Intent.ACTION_VIEW
+
+        if (apkFile.exists()) {
+            // 提升目录读写权限,否则可能出现解析异常
+            DeviceUtils.promotePermission(apkFile.parentFile!!.path)
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                // 7.0以上的版本,特殊处理
+                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                // 参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
+                val uri = FileProvider.getUriForFile(
+                    appContext,
+                    "${applicationId}.fileprovider",
+                    apkFile
+                )
+                intent.setDataAndType(uri, "application/vnd.android.package-archive")
+            } else {
+                intent.setDataAndType(
+                    Uri.fromFile(apkFile),
+                    "application/vnd.android.package-archive"
+                )
+            }
+            appContext.startActivity(intent)
+            ActivityCollector.finishAll()
+            Process.killProcess(Process.myPid())
+        }
+    }
+
+}

+ 61 - 0
RcCore/src/main/java/com/rc/core/util/ApkUpdater.kt

@@ -0,0 +1,61 @@
+package com.rc.core.util
+
+import android.content.Context
+import com.rc.core.log.RcLog
+import com.rc.httpcore.net.DownloadListener
+import com.rc.httpcore.net.DownloadTask
+import java.io.File
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class ApkUpdater(
+    private val appContext: Context,
+    private val applicationId: String,
+    private val callback: DownloadCallback? = null
+) {
+
+    fun downloadApk(downloadUrl: String) {
+        val fileName = "laboratory-release.apk"
+        val apkFile = File(appContext.cacheDir, fileName)
+        if (apkFile.exists()) {
+            apkFile.delete()
+        }
+        val downloadTask = DownloadTask(
+            listener = object : DownloadListener {
+                override fun onProgress(progress: Int) {
+                    RcLog.info("download apk, progress:$progress")
+                    callback?.onProgress(progress)
+                }
+
+                override fun onSuccess() {
+                    callback?.onSuccess(apkFile.path)
+                    ApkController.installAPK(apkFile, appContext, applicationId)
+                }
+
+                override fun onFailed(errMsg: String?) {
+                    callback?.onFailed(errMsg)
+                }
+
+                override fun onPaused() {
+                }
+
+                override fun onCanceled() {
+                }
+
+            }, dlPath = appContext.cacheDir.path,
+            fileName = fileName,
+            headers = mapOf("Accept-Encoding" to "identity")
+        )
+        downloadTask.execute(downloadUrl)
+    }
+
+    interface DownloadCallback {
+        fun onProgress(progress: Int)
+        fun onFailed(errMsg: String?)
+        fun onSuccess(apkFile: String)
+    }
+
+}

+ 208 - 0
RcCore/src/main/java/com/rc/core/util/CrashHandler.kt

@@ -0,0 +1,208 @@
+package com.rc.core.util
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Environment
+import android.util.Log
+import java.io.*
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object CrashHandler : Thread.UncaughtExceptionHandler {
+
+    private const val TAG = "LAB_Crash"
+
+    private var mContext: Context? = null
+
+    // 系统默认的 UncaughtException 处理类
+    private var mDefaultHandler: Thread.UncaughtExceptionHandler? = null
+
+    // 用来存储设备信息和异常信息
+    private val infos = HashMap<String, String>()
+
+    // 用于格式化日期,作为日志文件名的一部分
+    private val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+
+    /**
+     * 初始化
+     */
+    fun initCrash(context: Context/*,  dstClass: Class<? out Activity>*/) {
+        mContext = context
+//        this.dstClass = dstClass;
+        // 获取系统默认的 UncaughtException 处理器
+        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+        // 设置该 CrashHandler 为程序的默认处理器
+        Thread.setDefaultUncaughtExceptionHandler(this)
+    }
+
+    override fun uncaughtException(thread: Thread, ex: Throwable) {
+        if (!handleException(ex) && mDefaultHandler != null) {
+            // 如果用户没有处理则让系统默认的异常处理器来处理
+            mDefaultHandler!!.uncaughtException(thread, ex)
+        } else {
+            try {
+                Thread.sleep(3000)
+            } catch (e: InterruptedException) {
+            }
+
+            // 退出程序,注释下面的重启启动程序代码
+//            SystemUtil.restartThroughIntentCompatMakeRestartActivityTask(
+//                ContextUtil.getContext(),
+//                dstClass
+//            );
+        }
+    }
+
+    /**
+     * 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
+     *
+     * @param ex
+     * @return true:如果处理了该异常信息;否则返回 false
+     */
+    private fun handleException(ex: Throwable?): Boolean {
+        if (ex == null) return false
+        Log.e(TAG, "", ex)
+
+        // 收集设备参数信息
+        mContext?.let {
+            collectDeviceInfo(it)
+        }
+
+        // 读取Error信息
+        readThrowableInfo(ex)?.let { error ->
+            mContext?.let {
+                // 保存日志文件
+                saveCrashInfo2File(error, it.cacheDir)
+            }
+        }
+        return true
+    }
+
+    /**
+     * 收集设备参数信息
+     * @param ctx
+     */
+    private fun collectDeviceInfo(ctx: Context) {
+        try {
+            ctx.packageManager.getPackageInfo(ctx.packageName, PackageManager.GET_ACTIVITIES)
+                .let { packageInfo ->
+                    infos["versionName"] = packageInfo.versionName ?: "null"
+                    infos["versionCode"] = packageInfo.versionCode.toString()
+                }
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.e(TAG, "an error occured when collect package info", e)
+        }
+
+        Build::class.java.declaredFields.forEach { field ->
+            try {
+                field.isAccessible = true
+                infos[field.name] = field.get(null).toString()
+                Log.d(TAG, field.name + " : " + field.get(null))
+            } catch (e: Exception) {
+                Log.e(TAG, "an error occured when collect crash info", e)
+            }
+        }
+    }
+
+    /**
+     * 保存错误信息到文件中
+     *
+     * @param error
+     * @return 返回文件名称, 便于将文件传送到服务器
+     */
+    private fun saveCrashInfo2File(error: String, cacheDir: File): String? {
+        val time = formatter.format(Date())
+        val sb = StringBuffer()
+        sb.append(time + "\n")
+        infos.entries.forEach { entry ->
+            sb.append("${entry.key} = ${entry.value} \n")
+        }
+
+        var fos: FileOutputStream? = null
+        try {
+            sb.append(error)
+            sb.append("\n\n\n")
+
+            saveErrorLogToSp(sb.toString())
+
+            val fileName = "error.log"
+            if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
+                val errorFile = File(cacheDir, fileName)
+                if (!errorFile.exists()) {
+                    errorFile.createNewFile()
+                }
+                fos = FileOutputStream(errorFile)
+                fos.write(sb.toString().toByteArray())
+                fos.close()
+            }
+
+            return fileName
+        } catch (e: Exception) {
+            Log.e(TAG, "an error occured while writing file...", e)
+        } finally {
+            try {
+                fos?.let {
+                    it.close()
+                    fos = null
+                }
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * 读取Throwable中的错误日志
+     *
+     * @param ex
+     * @return
+     */
+    private fun readThrowableInfo(ex: Throwable): String? {
+        var result: String? = null
+        var writer: Writer? = null
+        var printWriter: PrintWriter? = null
+        try {
+            writer = StringWriter()
+            printWriter = PrintWriter(writer)
+            ex.printStackTrace(printWriter)
+            var cause = ex.cause
+            while (cause != null) {
+                cause.printStackTrace(printWriter)
+                cause = cause.cause
+            }
+            result = writer.toString()
+        } catch (e: Exception) {
+            Log.e(TAG, "read throwable error....", e)
+        } finally {
+            printWriter?.let {
+                it.close()
+                printWriter = null
+            }
+            try {
+                writer?.let {
+                    it.close()
+                    writer = null
+                }
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+        return result
+    }
+
+    private fun saveErrorLogToSp(errorInfo: String) {
+        mContext?.let { context ->
+            val sp = context.getSharedPreferences("error-log", Context.MODE_PRIVATE)
+            sp.edit().putString("error", errorInfo).apply()
+        }
+    }
+
+}

+ 120 - 0
RcCore/src/main/java/com/rc/core/util/CrashHelper.kt

@@ -0,0 +1,120 @@
+package com.rc.core.util
+
+import android.os.Environment
+import android.util.Log
+import java.io.*
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object CrashHelper {
+
+    private const val TAG = "LAB_Crash"
+
+    /**
+     * 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
+     *
+     * @param ex
+     * @return true:如果处理了该异常信息;否则返回 false
+     */
+    fun handleException(ex: Throwable?): Boolean {
+        if (ex == null) return false
+        Log.e(TAG, "", ex)
+
+        // 读取Error信息
+        readThrowableInfo(ex)?.let { error ->
+            // 保存日志文件
+            saveCrashInfo2File(error, Environment.getExternalStorageDirectory())
+        }
+        return true
+    }
+
+    /**
+     * 读取Throwable中的错误日志
+     *
+     * @param ex
+     * @return
+     */
+    private fun readThrowableInfo(ex: Throwable): String? {
+        var result: String? = null
+        var writer: Writer? = null
+        var printWriter: PrintWriter? = null
+        try {
+            writer = StringWriter()
+            printWriter = PrintWriter(writer)
+            ex.printStackTrace(printWriter)
+            var cause = ex.cause
+            while (cause != null) {
+                cause.printStackTrace(printWriter)
+                cause = cause.cause
+            }
+            result = writer.toString()
+        } catch (e: Exception) {
+            Log.e(TAG, "read throwable error....", e)
+        } finally {
+            printWriter?.let {
+                it.close()
+                printWriter = null
+            }
+            try {
+                writer?.let {
+                    it.close()
+                    writer = null
+                }
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+        return result
+    }
+
+    /**
+     * 保存错误信息到文件中
+     *
+     * @param error
+     * @return 返回文件名称, 便于将文件传送到服务器
+     */
+    fun saveCrashInfo2File(error: String, cacheDir: File): String? {
+        val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+        val time = formatter.format(Calendar.getInstance().time)
+        val sb = StringBuffer()
+        sb.append(time + "\n")
+
+        var fos: FileOutputStream? = null
+        try {
+            sb.append(error)
+            sb.append("\n\n\n")
+
+            val fileName = "error-${time}.log"
+            if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
+                val errorFile = File(cacheDir, fileName)
+                if (!errorFile.exists()) {
+                    errorFile.createNewFile()
+                }
+                fos = FileOutputStream(errorFile)
+                fos.write(sb.toString().toByteArray())
+                fos.close()
+            }
+
+            return fileName
+        } catch (e: Exception) {
+            Log.e(TAG, "an error occured while writing file...", e)
+        } finally {
+            try {
+                fos?.let {
+                    it.close()
+                    fos = null
+                }
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+
+        return null
+    }
+
+}

+ 102 - 0
RcCore/src/main/java/com/rc/core/util/DateUtils.kt

@@ -0,0 +1,102 @@
+package com.rc.core.util
+
+import android.text.TextUtils
+import java.text.DateFormat
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object DateUtils {
+
+    /**
+     * 比较两个日期的年月日
+     * @param c1
+     * @param c2
+     * @return c1 > c2 返回 1;
+     * c1 = c2 返回 0;
+     * c1 < c2 返回 -1
+     */
+    fun compareToYMD(c1: Calendar?, c2: Calendar?): Int {
+        if (null == c1 || null == c2) {
+            return 0
+        }
+        clearTime(c1)
+        clearTime(c2)
+        return c1.compareTo(c2)
+    }
+
+    /**
+     * 重置时、分、秒、毫秒
+     */
+    private fun clearTime(calendar: Calendar) {
+        calendar[Calendar.HOUR_OF_DAY] = 0
+        calendar[Calendar.MINUTE] = 0
+        calendar[Calendar.SECOND] = 0
+        calendar[Calendar.MILLISECOND] = 0
+    }
+
+    /**
+     * yyyy-MM-dd 格式
+     */
+    fun formatDate(calendar: Calendar): String {
+        val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+        return format.format(calendar.time)
+    }
+
+    /**
+     * 发生错误时,默认返回当前时间
+     *
+     * @param dateStr yyyy-MM-dd 格式
+     */
+    fun parseDate(dateStr: String?): Calendar {
+        return parseDate(dateStr, Calendar.getInstance())
+    }
+
+    /**
+     * @param dateStr yyyy-MM-dd 格式
+     * @param defDate 发生错误时,返回的时间
+     */
+    fun parseDate(dateStr: String?, defDate: Calendar): Calendar {
+        if (TextUtils.isEmpty(dateStr)) {
+            return defDate
+        }
+        try {
+            val calendar = Calendar.getInstance()
+            val format: DateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+            val date = format.parse(dateStr)
+            if (null != date) {
+                calendar.time = date
+                return calendar
+            }
+        } catch (e: ParseException) {
+            e.printStackTrace()
+        }
+        return defDate
+    }
+
+    /**
+     * 格式化时间戳
+     *
+     * @param timestamp 时间戳,单位秒
+     * @return yyyy-MM-dd格式
+     */
+    fun formatTimestamp(timestamp: String?): String? {
+        if (null == timestamp) return null
+
+        return try {
+            val format: DateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+            val calendar = Calendar.getInstance()
+            calendar.timeInMillis = timestamp.toLong() * 1000
+            format.format(calendar.time)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            null
+        }
+    }
+
+}

+ 91 - 0
RcCore/src/main/java/com/rc/core/util/DeviceUtils.kt

@@ -0,0 +1,91 @@
+package com.rc.core.util
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import java.io.IOException
+import kotlin.math.roundToInt
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object DeviceUtils {
+
+    /**
+     * 获取系统SN序列号
+     */
+    @SuppressLint("PrivateApi")
+    fun getAndroidSN(defSN: String = "NULL"): String {
+        return try {
+            val sysClass = Class.forName("android.os.SystemProperties")
+            sysClass.getMethod("get", String::class.java, String::class.java)
+                .invoke(sysClass, "ro.serialno", defSN) as? String ?: defSN
+        } catch (e: Exception) {
+            e.printStackTrace()
+            defSN
+        }
+    }
+
+    /**
+     * 获取当前apk版本名称(versionName)
+     */
+    fun getVersionName(context: Context): String {
+        return try {
+            // 获取包信息
+            // 参1 包名 参2 获取额外信息的flag 不需要的话 写0
+            context.packageManager.getPackageInfo(
+                context.packageName, 0
+            ).versionName
+        } catch (e: PackageManager.NameNotFoundException) {
+            e.printStackTrace()
+            ""
+        }
+    }
+
+    /**
+     * 获取当前apk版本号(versionCode)
+     */
+    fun getVersionCode(context: Context): Int {
+        return try {
+            context.packageManager.getPackageInfo(context.packageName, 0).versionCode
+        } catch (e: PackageManager.NameNotFoundException) {
+            -1
+        }
+    }
+
+    /**
+     * 提升读写权限
+     *
+     * @param filePath 文件路径
+     */
+    fun promotePermission(filePath: String) {
+        val command = "chmod 777 $filePath"
+        val runtime = Runtime.getRuntime()
+        try {
+            runtime.exec(command)
+        } catch (e: IOException) {
+            e.printStackTrace()
+        }
+    }
+
+    /**
+     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
+     */
+    fun dip2px(context: Context, dpValue: Float): Int {
+        val scale = context.resources.displayMetrics.density
+        return (dpValue * scale + 0.5f).toInt()
+    }
+
+    fun spanCount(context: Context, gridExpectedSize: Int, offset: Int, horizontalSpacing: Int): Int {
+        val screenWidth = context.resources.displayMetrics.widthPixels - offset + horizontalSpacing
+        val expected = screenWidth.toFloat() / (gridExpectedSize.toFloat() + horizontalSpacing)
+        var spanCount = expected.roundToInt()
+        if (spanCount == 0) {
+            spanCount = 1
+        }
+        return spanCount
+    }
+
+}

+ 68 - 0
RcCore/src/main/java/com/rc/core/util/EscapeUnescape.java

@@ -0,0 +1,68 @@
+package com.rc.core.util;
+
+public class EscapeUnescape {
+
+    public static String escape(String src) {
+        int i;
+        char j;
+        StringBuffer tmp = new StringBuffer();
+        tmp.ensureCapacity(src.length() * 6);
+
+        for (i = 0; i < src.length(); i++) {
+
+            j = src.charAt(i);
+
+            if (Character.isDigit(j) || Character.isLowerCase(j) || Character.isUpperCase(j))
+                tmp.append(j);
+            else if (j < 256) {
+                tmp.append("%");
+                if (j < 16)
+                    tmp.append("0");
+                tmp.append(Integer.toString(j, 16));
+            } else {
+                tmp.append("%u");
+                tmp.append(Integer.toString(j, 16));
+            }
+        }
+        return tmp.toString();
+    }
+
+    public static String unescape(String src) {
+        StringBuffer tmp = new StringBuffer();
+        tmp.ensureCapacity(src.length());
+        int lastPos = 0, pos = 0;
+        char ch;
+        while (lastPos < src.length()) {
+            pos = src.indexOf("%", lastPos);
+            if (pos == lastPos) {
+                if (src.charAt(pos + 1) == 'u') {
+                    ch = (char) Integer.parseInt(src.substring(pos + 2, pos + 6), 16);
+                    tmp.append(ch);
+                    lastPos = pos + 6;
+                } else {
+                    ch = (char) Integer.parseInt(src.substring(pos + 1, pos + 3), 16);
+                    tmp.append(ch);
+                    lastPos = pos + 3;
+                }
+            } else {
+                if (pos == -1) {
+                    tmp.append(src.substring(lastPos));
+                    lastPos = src.length();
+                } else {
+                    tmp.append(src.substring(lastPos, pos));
+                    lastPos = pos;
+                }
+            }
+        }
+        return tmp.toString();
+    }
+
+//    public static void main(String[] args) {
+//        String tmp = "~!@#$%^&*()_+|\\=-,./?><;'][{}\"";
+//        System.out.println("testing escape : " + tmp);
+//        tmp = escape(tmp);
+//        System.out.println(tmp);
+//        System.out.println("testing unescape :" + tmp);
+//        System.out.println(unescape(tmp));
+//    }
+}

+ 13 - 0
RcCore/src/main/java/com/rc/core/util/Extension.kt

@@ -0,0 +1,13 @@
+package com.rc.core.util
+
+import android.os.Build
+import android.text.Html
+import android.text.Spanned
+
+fun String.fromHtml(): Spanned {
+    return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
+        Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
+    } else {
+        Html.fromHtml(this)
+    }
+}

+ 58 - 0
RcCore/src/main/java/com/rc/core/util/Format.kt

@@ -0,0 +1,58 @@
+package com.rc.core.util
+
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object Format {
+
+    /**
+     * 将时区时间格式化成日期字符串
+     *
+     * @param zonedDateTime 2021-03-30T14:18:41.000+08:00
+     */
+    fun formatZonedDateTime(zonedDateTime: String?, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
+        val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.getDefault())
+        val data: Date? = try {
+            dateFormat.parse(zonedDateTime)
+        } catch (e: Exception) {
+            null
+        }
+
+        return formatDate(data, pattern)
+    }
+
+    /**
+     * 将时间戳格式化成yyyy-MM-dd HH:mm:ss格式的日期字符串
+     *
+     * @param dateLong
+     * @return
+     */
+    fun formatTimestamp(dateLong: String?, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
+        val date: Date? = try {
+            dateLong?.let { Date(it.toLong()) }
+        } catch (e: Exception) {
+            null
+        }
+
+        return formatDate(date, pattern)
+    }
+
+    /**
+     * 按给定的格式格式化日期.
+     *
+     * @param date
+     * @param pattern : 格式化格式
+     * @return
+     */
+    fun formatDate(date: Date?, pattern: String = "yyyy-MM-dd"): String {
+        return date?.let {
+            SimpleDateFormat(pattern, Locale.getDefault()).format(it)
+        } ?: ""
+    }
+
+}

+ 162 - 0
RcCore/src/main/java/com/rc/core/util/MediaUtils.kt

@@ -0,0 +1,162 @@
+package com.rc.core.util
+
+import android.content.ContentValues
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import java.io.File
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object MediaUtils {
+
+    /**
+     * 创建一条图片地址uri,用于保存拍照后的照片
+     *
+     * @param context
+     * @param cameraFileName
+     * @param mimeType
+     * @return 图片的uri
+     */
+    fun createImageUri(
+        context: Context,
+        cameraFileName: String,
+        mimeType: String? = "image/jpeg"
+    ): Uri? {
+        // ContentValues是我们希望这条记录被创建时包含的数据信息
+        val values = ContentValues()
+
+        val suffix = cameraFileName.substring(cameraFileName.lastIndexOf("."))
+        val fileName = cameraFileName.replace(suffix, "")
+        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            values.put(MediaStore.Images.Media.DATE_TAKEN, "${System.currentTimeMillis()}")
+        }
+        values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
+
+        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
+        return if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                values.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/Camera")
+            }
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
+        } else {
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values)
+        }
+    }
+
+    /**
+     * 创建一条视频地址uri,用于保存录制的视频
+     *
+     * @param context
+     * @param cameraFileName
+     * @param mimeType
+     * @return 视频的uri
+     */
+    fun createVideoUri(
+        context: Context,
+        cameraFileName: String,
+        mimeType: String? = "video/mp4"
+    ): Uri? {
+        // ContentValues是我们希望这条记录被创建时包含的数据信息
+        val values = ContentValues()
+
+        val suffix = cameraFileName.substring(cameraFileName.lastIndexOf("."))
+        val fileName = cameraFileName.replace(suffix, "")
+        values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            values.put(MediaStore.Video.Media.DATE_TAKEN, "${System.currentTimeMillis()}")
+        }
+        values.put(MediaStore.Video.Media.MIME_TYPE, mimeType)
+
+        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
+        return if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES)
+            }
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)
+        } else {
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Video.Media.INTERNAL_CONTENT_URI, values)
+        }
+    }
+
+    /**
+     * delete camera PATH
+     *
+     * @param context Context
+     * @param cameraPath Camera url
+     */
+    fun deleteCamera(context: Context, cameraPath: String?) {
+        try {
+            if (null != cameraPath && cameraPath.startsWith("content://")) {
+                context.contentResolver.delete(Uri.parse(cameraPath), null, null)
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    fun deleteFile(filepath: String?) {
+        if (!filepath.isNullOrEmpty()) {
+            try {
+                File(filepath).delete()
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }
+    }
+
+    /**
+     * 创建文件
+     *
+     * @param context 上下文
+     * @param fileName 文件名
+     * @param outCameraDirectory 输出目录
+     * @return
+     */
+    fun createCameraFile(
+        context: Context,
+        fileName: String,
+        outCameraDirectory: String? = null
+    ): File {
+        val folderDir: File
+        if (outCameraDirectory.isNullOrEmpty()) {
+            // 外部没有自定义拍照存储路径使用默认
+            val rootDir: File
+            if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
+                rootDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+                folderDir = File(rootDir.absolutePath + File.separator + "Camera" + File.separator)
+            } else {
+                rootDir =
+                    context.applicationContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
+                folderDir = File(rootDir.absolutePath + File.separator)
+            }
+            if (!rootDir.exists()) {
+                rootDir.mkdirs()
+            }
+        } else {
+            // 自定义存储路径
+            folderDir = File(outCameraDirectory)
+            if (!Objects.requireNonNull(folderDir.parentFile).exists()) {
+                folderDir.parentFile?.mkdirs()
+            }
+        }
+        if (!folderDir.exists()) {
+            folderDir.mkdirs()
+        }
+
+        return File(folderDir, fileName)
+    }
+}

+ 128 - 0
RcCore/src/main/java/com/rc/core/util/ScreenAdapter.kt

@@ -0,0 +1,128 @@
+package com.rc.core.util
+
+import android.app.Activity
+import android.app.Application
+import android.content.ComponentCallbacks
+import android.content.Context
+import android.content.res.Configuration
+import android.util.DisplayMetrics
+
+/**
+ * 屏幕适配
+ *
+ * @author ReiChin_
+ */
+object ScreenAdapter {
+
+    private const val DEFAULT_WIDTH = 1080 // 默认宽度
+
+    private var appDensity = 0f
+
+    /**
+     * 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值
+     */
+    private var appScaledDensity = 0f
+
+    /**
+     * 状态栏高度
+     */
+    private var barHeight: Int = 0
+    private var appDisplayMetrics: DisplayMetrics? = null
+    private var densityScale = 1.0f
+
+    /**
+     * application 层调用,存储默认屏幕密度
+     */
+    fun initAppDensity(application: Application) {
+        // 获取application的DisplayMetrics
+        appDisplayMetrics = application.resources.displayMetrics
+        // 获取状态栏高度
+        barHeight = getStatusBarHeight(application)
+        if (appDensity == 0f) {
+            // 初始化的时候赋值
+            appDensity = appDisplayMetrics!!.density
+            appScaledDensity = appDisplayMetrics!!.scaledDensity
+            // 添加字体变化的监听
+            application.registerComponentCallbacks(object : ComponentCallbacks {
+
+                override fun onConfigurationChanged(newConfig: Configuration) {
+                    // 字体改变后,将appScaledDensity重新赋值
+                    if (newConfig.fontScale > 0) {
+                        appScaledDensity = application.resources.displayMetrics.scaledDensity
+                    }
+                }
+
+                override fun onLowMemory() {
+                }
+            })
+        }
+    }
+
+    /**
+     * 设置自定义的屏幕密度
+     */
+    fun setCustomDensity(activity: Activity) {
+        val targetDensity = appDisplayMetrics!!.widthPixels * 1.0f / DEFAULT_WIDTH
+
+        // 最后在这里将修改过后的值赋给系统参数,只修改Activity的density值
+        val activityDisplayMetrics = activity.resources.displayMetrics.apply {
+            density = targetDensity
+            scaledDensity = targetDensity * (appScaledDensity / appDensity)
+            densityDpi = (160 * targetDensity).toInt()
+        }
+
+        densityScale = appDensity / targetDensity
+        setBitmapDefaultDensity(activityDisplayMetrics.densityDpi)
+    }
+
+    /**
+     * 重置屏幕密度
+     */
+    fun resetAppDensity(activity: Activity) {
+        val activityDisplayMetrics = activity.resources.displayMetrics
+        activityDisplayMetrics.density = appDensity
+        activityDisplayMetrics.scaledDensity = appScaledDensity
+        activityDisplayMetrics.densityDpi = (appDensity * 160).toInt()
+
+        densityScale = 1.0f
+        setBitmapDefaultDensity(activityDisplayMetrics.densityDpi)
+    }
+
+    /**
+     * 获取状态栏高度
+     *
+     * @param context context
+     * @return 状态栏高度
+     */
+    private fun getStatusBarHeight(context: Context): Int {
+        val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
+        return if (resourceId > 0) {
+            context.resources.getDimensionPixelSize(resourceId)
+        } else 0
+    }
+
+    /**
+     * 设置 Bitmap 的默认屏幕密度
+     * 由于 Bitmap 的屏幕密度是读取配置的,导致修改未被启用
+     * 所有,放射方式强行修改
+     * @param defaultDensity 屏幕密度
+     */
+    private fun setBitmapDefaultDensity(defaultDensity: Int) {
+        try {
+            // 获取单个变量的值
+            val clazz = Class.forName("android.graphics.Bitmap")
+            val field = clazz.getDeclaredField("sDefaultDensity")
+            field.isAccessible = true
+            field.set(null, defaultDensity)
+            field.isAccessible = false
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    /**
+     * 屏幕密度缩放系数
+     */
+    fun getDensityScale() = densityScale
+
+}

+ 0 - 0
RcCore/src/main/java/com/rc/core/util/VideoFullScreenWebChromeClient.kt


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.