Browse Source

本地版 激活绑定检查流程初步实现

master
suliang 2 years ago
parent
commit
c954070df9
48 changed files with 1760 additions and 119 deletions
  1. 1
    0
      .idea/misc.xml
  2. 6
    3
      app/build.gradle
  3. 7
    3
      app/src/main/AndroidManifest.xml
  4. 1
    1
      app/src/main/java/com/xkl/cdl/adapter/AdapterCoursePackWithMemo.kt
  5. 1
    1
      app/src/main/java/com/xkl/cdl/adapter/DictionaryAdapter.kt
  6. 2
    0
      app/src/main/java/com/xkl/cdl/data/AppConstants.kt
  7. 1
    1
      app/src/main/java/com/xkl/cdl/data/bean/StatisticsCourse.kt
  8. 3
    1
      app/src/main/java/com/xkl/cdl/data/bean/course/Course.kt
  9. 10
    4
      app/src/main/java/com/xkl/cdl/data/bean/course/CoursePack.kt
  10. 31
    0
      app/src/main/java/com/xkl/cdl/data/bean/course/CoursePackBaseInfo.kt
  11. 12
    5
      app/src/main/java/com/xkl/cdl/data/binding/BindingAdapter.kt
  12. 13
    0
      app/src/main/java/com/xkl/cdl/data/manager/FilePathManager.kt
  13. 42
    3
      app/src/main/java/com/xkl/cdl/data/manager/UserInfoManager.kt
  14. 3
    3
      app/src/main/java/com/xkl/cdl/data/manager/db/DbCoursePackManager.kt
  15. 16
    0
      app/src/main/java/com/xkl/cdl/dialog/CommonDialog.kt
  16. 22
    15
      app/src/main/java/com/xkl/cdl/dialog/CommonDialogBean.kt
  17. 3
    1
      app/src/main/java/com/xkl/cdl/module/XKLApplication.kt
  18. 2
    2
      app/src/main/java/com/xkl/cdl/module/learn/LearnExamActivity.kt
  19. 2
    2
      app/src/main/java/com/xkl/cdl/module/learn/LearnWordActivity.kt
  20. 4
    4
      app/src/main/java/com/xkl/cdl/module/m_my/MyFragment.kt
  21. 2
    2
      app/src/main/java/com/xkl/cdl/module/m_my/SettingActivity.kt
  22. 54
    0
      app/src/main/java/com/xkl/cdl/module/splash/ActivateActivity.kt
  23. 57
    0
      app/src/main/java/com/xkl/cdl/module/splash/ActivateViewModel.kt
  24. 128
    47
      app/src/main/java/com/xkl/cdl/module/splash/SplashActivity.kt
  25. 278
    0
      app/src/main/java/com/xkl/cdl/module/splash/SplashViewModel.kt
  26. 13
    0
      app/src/main/java/com/xkl/cdl/net/ApiService.kt
  27. 102
    0
      app/src/main/java/com/xkl/cdl/net/RequestUtil.kt
  28. 207
    0
      app/src/main/java/com/xkl/cdl/util/IdentificationUtils.kt
  29. 60
    0
      app/src/main/res/layout/activity_activate.xml
  30. 2
    2
      app/src/main/res/layout/activity_course_main.xml
  31. 1
    1
      app/src/main/res/layout/activity_course_statistics_detail.xml
  32. 13
    0
      app/src/main/res/layout/activity_splash.xml
  33. 1
    1
      app/src/main/res/layout/item_statistics_course.xml
  34. 1
    1
      app/src/main/res/layout/main_item_coursepack.xml
  35. 3
    0
      app/src/main/res/values/strings.xml
  36. 7
    0
      build.gradle
  37. 8
    0
      lib/common/build.gradle
  38. 5
    4
      lib/common/src/main/AndroidManifest.xml
  39. 12
    10
      lib/common/src/main/java/com/suliang/common/util/AppGlobals.kt
  40. 81
    2
      lib/common/src/main/java/com/suliang/common/util/SpUtils.kt
  41. 31
    0
      lib/common/src/main/java/com/suliang/common/util/net/DownLoadFileListener.kt
  42. 193
    0
      lib/common/src/main/java/com/suliang/common/util/net/HttpUtil.kt
  43. 62
    0
      lib/common/src/main/java/com/suliang/common/util/net/IApiService.kt
  44. 71
    0
      lib/common/src/main/java/com/suliang/common/util/net/NetObserver.kt
  45. 13
    0
      lib/common/src/main/java/com/suliang/common/util/net/NetworkState.kt
  46. 72
    0
      lib/common/src/main/java/com/suliang/common/util/net/NetworkUtil.kt
  47. 32
    0
      lib/common/src/main/java/com/suliang/common/util/net/OkhttpResultCallback.kt
  48. 69
    0
      lib/common/src/main/java/com/suliang/common/util/net/RetryInterceptor.kt

+ 1
- 0
.idea/misc.xml View File

<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout-v23/include_main_learn_center_course_type_title.xml" value="0.4963768115942029" /> <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout-v23/include_main_learn_center_course_type_title.xml" value="0.4963768115942029" />
<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/_xpopup_ext_time_picker.xml" value="0.23632218844984804" /> <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/_xpopup_ext_time_picker.xml" value="0.23632218844984804" />
<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_about_us.xml" value="0.2373353596757852" /> <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_about_us.xml" value="0.2373353596757852" />
<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_activate.xml" value="0.36614583333333334" />
<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_cache_clear.xml" value="0.2373353596757852" /> <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_cache_clear.xml" value="0.2373353596757852" />
<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_course_main.xml" value="0.33" /> <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_course_main.xml" value="0.33" />
<entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_course_statistics_detail.xml" value="0.33" /> <entry key="..\:/Work/XKL/XKL/XklLocal/app/src/main/res/layout/activity_course_statistics_detail.xml" value="0.33" />

+ 6
- 3
app/build.gradle View File

implementation fileTree(include: ['*.jar', "*.aar"], dir: 'libs') implementation fileTree(include: ['*.jar', "*.aar"], dir: 'libs')
// implementation 'androidx.legacy:legacy-support-v4:1.0.0' // implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation project(path: ':lib:common') implementation project(path: ':lib:common')
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
// implementation 'androidx.appcompat:appcompat:1.2.0'
// implementation 'com.google.android.material:material:1.3.0'
// implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation project(path: ':videoplayer') implementation project(path: ':videoplayer')
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
// implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' // implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
// implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' // implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
// implementation 'androidx.appcompat:appcompat:1.2.0' // implementation 'androidx.appcompat:appcompat:1.2.0'

+ 7
- 3
app/src/main/AndroidManifest.xml View File

xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.xkl.cdl"> package="com.xkl.cdl">


<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />


<application <application
android:name=".module.XKLApplication" android:name=".module.XKLApplication"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.XklLocal" android:theme="@style/Theme.XklLocal"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
<activity
android:name=".module.splash.ActivateActivity"
android:exported="true" />
<activity <activity
android:name=".module.floating.DictionaryFloatingSearchActivity" android:name=".module.floating.DictionaryFloatingSearchActivity"
android:theme="@style/DialogActivityTheme"
android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:exported="true" />
android:theme="@style/DialogActivityTheme" />
<activity <activity
android:name=".module.m_my.CacheClearActivity" android:name=".module.m_my.CacheClearActivity"
android:exported="true" /> android:exported="true" />

+ 1
- 1
app/src/main/java/com/xkl/cdl/adapter/AdapterCoursePackWithMemo.kt View File

getItem(position).let { item -> getItem(position).let { item ->
(holder.binding as ItemMemoBinding).run { (holder.binding as ItemMemoBinding).run {
//图片 //图片
BindingAdapter.imageByteArray(imgCoursePackCover, item.coursePack.cover)
BindingAdapter.imageFilePath(imgCoursePackCover, item.coursePack.cover)
//文字 //文字
tvCoursePackName.text = item.coursePack.coursePackName tvCoursePackName.text = item.coursePack.coursePackName
//图标与角标文字 //图标与角标文字

+ 1
- 1
app/src/main/java/com/xkl/cdl/adapter/DictionaryAdapter.kt View File

tvValue.text = it.basic_explaination tvValue.text = it.basic_explaination
tvWord.text = it.word tvWord.text = it.word
ivHoruns.click { v -> //发音 ivHoruns.click { v -> //发音
AudioCache.getDictionaryVoice(it.id,UserInfoManager.getDefaultSoundWay())
AudioCache.getDictionaryVoice(it.id,UserInfoManager.instance.getDefaultSoundWay())
} }
//点击,进入详情 //点击,进入详情
root.click { v -> root.click { v ->

+ 2
- 0
app/src/main/java/com/xkl/cdl/data/AppConstants.kt View File

*/ */
object AppConstants { object AppConstants {


const val DEVICE_ID = "deviceId"
const val LICENSE_ID = "licenceId"


/** 项目: 英语 */ /** 项目: 英语 */
const val SUBJECT_ENGLISH = 3 const val SUBJECT_ENGLISH = 3

+ 1
- 1
app/src/main/java/com/xkl/cdl/data/bean/StatisticsCourse.kt View File

* create 2022/6/27 14:42 * create 2022/6/27 14:42
* Describe: 统计课程item * Describe: 统计课程item
*/ */
class StatisticsCourse(val course:Course,val cover: ByteArray) {
class StatisticsCourse(val course:Course,val cover: String) {
} }

+ 3
- 1
app/src/main/java/com/xkl/cdl/data/bean/course/Course.kt View File

package com.xkl.cdl.data.bean.course package com.xkl.cdl.data.bean.course


import java.io.Serializable

/** /**
* author suliang * author suliang
* create 2022/3/22 10:08 * create 2022/3/22 10:08
val courseType: Int, val courseType: Int,
val totalWords: Int, val totalWords: Int,
val dbPathName: String val dbPathName: String
) {
) : Serializable {


var courseLearnProgress : Double = 0.0 //课程学习进度 var courseLearnProgress : Double = 0.0 //课程学习进度
} }

+ 10
- 4
app/src/main/java/com/xkl/cdl/data/bean/course/CoursePack.kt View File

import androidx.databinding.BaseObservable import androidx.databinding.BaseObservable
import androidx.databinding.Bindable import androidx.databinding.Bindable
import com.xkl.cdl.BR import com.xkl.cdl.BR
import java.io.Serializable


/** /**
* author suliang * author suliang
data class CoursePack( data class CoursePack(
val coursePackId: Long, val coursePackId: Long,
val coursePackName: String, val coursePackName: String,
val cover: ByteArray,
val summary: String, val summary: String,
val subjectId: Int, val subjectId: Int,
val coursePackType: Int, val coursePackType: Int,
) : BaseObservable() {
) : BaseObservable(),Serializable {
var cover: String = "" //课程包图片文件地址
var downLoadZipUrl : String = "" //压缩包下载地址
var isDown = false //是否下载
//在CourseManger中的subjectWithCoursePackMap对应subject下的list中所在的位置 //在CourseManger中的subjectWithCoursePackMap对应subject下的list中所在的位置
var inCoursePackPosition : Int = 0 var inCoursePackPosition : Int = 0


if (coursePackId != other.coursePackId) return false if (coursePackId != other.coursePackId) return false
if (coursePackName != other.coursePackName) return false if (coursePackName != other.coursePackName) return false
if (!cover.contentEquals(other.cover)) return false
if (cover != other.cover) return false
if (summary != other.summary) return false if (summary != other.summary) return false
if (subjectId != other.subjectId) return false if (subjectId != other.subjectId) return false
if (coursePackType != other.coursePackType) return false if (coursePackType != other.coursePackType) return false
override fun hashCode() : Int { override fun hashCode() : Int {
var result = coursePackId.hashCode() var result = coursePackId.hashCode()
result = 31 * result + coursePackName.hashCode() result = 31 * result + coursePackName.hashCode()
result = 31 * result + cover.contentHashCode()
result = 31 * result + cover.hashCode()
result = 31 * result + summary.hashCode() result = 31 * result + summary.hashCode()
result = 31 * result + subjectId result = 31 * result + subjectId
result = 31 * result + coursePackType result = 31 * result + coursePackType

+ 31
- 0
app/src/main/java/com/xkl/cdl/data/bean/course/CoursePackBaseInfo.kt View File

package com.xkl.cdl.data.bean.course

import java.io.Serializable

/**
* author: suliang
* 2022/12/8 11:32
* describe : 用户绑定的课程信息
*/
@Deprecated("可以直接用CoursePack", replaceWith = ReplaceWith("CoursePack"),DeprecationLevel.ERROR)
class CoursePackBaseInfo : Serializable {
var subjectId : Int = 0 //项目id
var coursePackId: Long = 0 //课程包id
var coursePackName : String = "" //课程包名称
val coursePackType : Int = 0 //课程包类型
val summary : String = "" //简介
var downLoadZipUrl : String? = null //压缩包下载地址
//课程包下的课程id列表
var childrenCourses = mutableListOf<Long>()
var isDown = false //是否下载
}

+ 12
- 5
app/src/main/java/com/xkl/cdl/data/binding/BindingAdapter.kt View File

* @param imageByteArray ByteArray 字节数组 * @param imageByteArray ByteArray 字节数组
*/ */
//https://stackoverflow.com/questions/60264081/in-android-how-databinding-with-byte-array //https://stackoverflow.com/questions/60264081/in-android-how-databinding-with-byte-array
@BindingAdapter("imageByteArray")
@BindingAdapter("imgFilePath")
@JvmStatic @JvmStatic
fun imageByteArray(view:ImageView,imageByteArray:ByteArray){
fun imageFilePath(view:ImageView,imageFilePath:String){
// ImageLoader.loadImage(view,imageByteArray)
// view.setImageBitmap(BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.size))
ImageLoader.loadImage(view,imageFilePath)
}
@BindingAdapter("imgByteArray")
@JvmStatic
fun imageByteArray(view:ImageView,imageByteArray : ByteArray){
ImageLoader.loadImage(view,imageByteArray) ImageLoader.loadImage(view,imageByteArray)
// view.setImageBitmap(BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.size)) // view.setImageBitmap(BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.size))
} }


@BindingAdapter(value = ["imgBytes","blur"])
@BindingAdapter(value = ["imgFilePathBlur","blur"])
@JvmStatic @JvmStatic
fun imageByteArray(view:ImageView,imgByteArray:ByteArray,blur:Boolean){
fun imageByteArray(view:ImageView,imgFilePath:String,blur:Boolean){
ImageLoader.loadImage(ImageLoaderOption().apply { ImageLoader.loadImage(ImageLoaderOption().apply {
targetView = view targetView = view
byteArray = imgByteArray
url = imgFilePath
blurEanble = blur blurEanble = blur
}) })
} }

+ 13
- 0
app/src/main/java/com/xkl/cdl/data/manager/FilePathManager.kt View File

return File(getDbRootPath(), "dictionary.db") return File(getDbRootPath(), "dictionary.db")
} }
/* 获取课程包图片所在的目录
* @return File
*/
@JvmStatic
fun getIconRootPath(): String{
return FileUtil.getSaveDirPath("icon")
}
/** /**
* 获取课程包目录数据所在的地址 * 获取课程包目录数据所在的地址
* @return File * @return File
return File(getVoiceParent(),"voc") return File(getVoiceParent(),"voc")
} }
//zip下载包
fun getZipRootPath() : File {
return FileUtil.getSaveDirFile("zip")
}
} }


} }

+ 42
- 3
app/src/main/java/com/xkl/cdl/data/manager/UserInfoManager.kt View File

import com.suliang.common.util.file.FileUtil import com.suliang.common.util.file.FileUtil
import com.suliang.common.util.thread.AppExecutors import com.suliang.common.util.thread.AppExecutors
import com.xkl.cdl.data.AppConstants import com.xkl.cdl.data.AppConstants
import com.xkl.cdl.data.bean.course.CoursePack
import java.io.File import java.io.File


/** /**
* create 2022/4/8 17:47 * create 2022/4/8 17:47
* Describe: 用户数据管理 * Describe: 用户数据管理
*/ */
object UserInfoManager {

class UserInfoManager private constructor(){
companion object{
val instance : UserInfoManager by lazy(LazyThreadSafetyMode.SYNCHRONIZED){
UserInfoManager()
}
}
/** 获取默认发音方式 */ /** 获取默认发音方式 */
fun getDefaultSoundWay() : Int{ fun getDefaultSoundWay() : Int{
return SpUtils.instance.decode("defaultSoundWay",Int::class.java,AppConstants.SOUND_TYPE_UK) return SpUtils.instance.decode("defaultSoundWay",Int::class.java,AppConstants.SOUND_TYPE_UK)
} }
/** 获取用户头像 */ /** 获取用户头像 */
fun getUserHeadPortrait(): File? { fun getUserHeadPortrait(): File? {
return SpUtils.instance.decode("head_portrait",String::class.java,"").let {
return SpUtils.instance.decode("head_portrait",String::class.java,"").let {
if (it.isEmpty()){ if (it.isEmpty()){
return null return null
}else }else
SpUtils.instance.encode("nickname",nickname) SpUtils.instance.encode("nickname",nickname)
} }
/**获取唯一码*/
fun getUuid():String{
return SpUtils.instance.decode(AppConstants.DEVICE_ID,String::class.java,"")
}
/** 存放本地唯一码 */
fun putUuid(newUuid : String) {
SpUtils.instance.encode(AppConstants.DEVICE_ID,newUuid)
}
fun putLicence(licence : String) {
SpUtils.instance.encode(AppConstants.LICENSE_ID,licence)
}
fun getLicence() : String{
return SpUtils.instance.decode(AppConstants.LICENSE_ID,String::class.java,"")
}
/**
* 检查绑定的课程信息
*/
fun getBindCourse() : List<CoursePack>? {
return SpUtils.instance.decodeList("bindCourse")
}
/**
* 存放绑定的课程
*/
fun putBindCourse(coursePackList : List<CoursePack>) {
return SpUtils.instance.encodeList("bindCourse",coursePackList)
}
} }

+ 3
- 3
app/src/main/java/com/xkl/cdl/data/manager/db/DbCoursePackManager.kt View File

database = SQLiteDatabase.openDatabase(coursePackFile.path, "XUEKAOLE_COURSE_LIST_KEY", null, OPEN_READONLY) database = SQLiteDatabase.openDatabase(coursePackFile.path, "XUEKAOLE_COURSE_LIST_KEY", null, OPEN_READONLY)
} }


/***
/* *//***
* 查询用户绑定的课程包 * 查询用户绑定的课程包
* @param coursePackIds String 课程包id集合如: 1,2,3,4,5 最后不能有逗号 * @param coursePackIds String 课程包id集合如: 1,2,3,4,5 最后不能有逗号
* @return Boolean 查询是否成功 * @return Boolean 查询是否成功
*/
*//*
fun queryBindingCoursePack(coursePackIds: String): Boolean { fun queryBindingCoursePack(coursePackIds: String): Boolean {
//英语课程包 //英语课程包
val englishCoursePack = mutableListOf<CoursePack>() val englishCoursePack = mutableListOf<CoursePack>()
CourseManager.subjectWithCoursePackMap[AppConstants.SUBJECT_CHINESE] = chineseCoursePack.toList() CourseManager.subjectWithCoursePackMap[AppConstants.SUBJECT_CHINESE] = chineseCoursePack.toList()
database.close() database.close()
return true return true
}
}*/
} }

+ 16
- 0
app/src/main/java/com/xkl/cdl/dialog/CommonDialog.kt View File

titleText?.let { titleText?.let {
binding.tvTitle.setText(it) binding.tvTitle.setText(it)
binding.tvTitle.visibility = View.VISIBLE binding.tvTitle.visibility = View.VISIBLE
} ?: titleTextValue?.let {
binding.tvTitle.text = it
binding.tvTitle.visibility = View.VISIBLE
} }
contentColor?.let { contentColor?.let {
binding.tvContent.setTextColor(ContextCompat.getColor(requireContext(),it)) binding.tvContent.setTextColor(ContextCompat.getColor(requireContext(),it))
contentText?.let { contentText?.let {
binding.tvContent.setText(it) binding.tvContent.setText(it)
binding.tvContent.visibility = View.VISIBLE binding.tvContent.visibility = View.VISIBLE
} ?: contentTextValue?.let {
binding.tvContent.text = it
binding.tvContent.visibility = View.VISIBLE
} }
leftColor?.let { leftColor?.let {
binding.tvLeft.setTextColor(ContextCompat.getColor(requireContext(),it)) binding.tvLeft.setTextColor(ContextCompat.getColor(requireContext(),it))
binding.tvLeft.click { binding.tvLeft.click {
onCommonDialogButtonClickListener(this@CommonDialog,false) onCommonDialogButtonClickListener(this@CommonDialog,false)
} }
}?:leftTextValue?.let {
binding.tvLeft.text = it
binding.tvLeft.click {
onCommonDialogButtonClickListener(this@CommonDialog,false)
}
}?:let { }?:let {
binding.tvLeft.visibility = View.GONE binding.tvLeft.visibility = View.GONE
binding.vSplit.visibility = View.GONE binding.vSplit.visibility = View.GONE
binding.tvRight.click { binding.tvRight.click {
onCommonDialogButtonClickListener(this@CommonDialog,true) onCommonDialogButtonClickListener(this@CommonDialog,true)
} }
}?:rightTextValue?.let {
binding.tvRight.text = it
binding.tvRight.click {
onCommonDialogButtonClickListener(this@CommonDialog,true)
}
} }
imgFlag?.let { imgFlag?.let {

+ 22
- 15
app/src/main/java/com/xkl/cdl/dialog/CommonDialogBean.kt View File

* Describe: 通用弹窗设置实体 * Describe: 通用弹窗设置实体
*/ */
data class CommonDialogBean(@StringRes val titleText : Int? = null, data class CommonDialogBean(@StringRes val titleText : Int? = null,
@StringRes val contentText : Int? = null,
@StringRes val leftText : Int? = null,
@StringRes val rightText : Int? = null,
@StringRes val imgFlag : Int? = null,
@ColorRes val titleColor : Int? = null,
@ColorRes val contentColor : Int? = null,
@ColorRes val leftColor : Int? = null,
@ColorRes val rightColor : Int? = null) : Parcelable {
constructor(parcel : Parcel) : this(parcel.readValue(Int::class.java.classLoader) as? Int,
var titleTextValue : String? = null,
@StringRes val contentText : Int? = null,
var contentTextValue : String? = null,
@StringRes val leftText : Int? = null,
var leftTextValue : String? = null,
@StringRes val rightText : Int? = null,
var rightTextValue : String? = null,
@StringRes val imgFlag : Int? = null,
@ColorRes val titleColor : Int? = null,
@ColorRes val contentColor : Int? = null,
@ColorRes val leftColor : Int? = null,
@ColorRes val rightColor : Int? = null) : Parcelable {
constructor(parcel : Parcel) : this(parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readString(),
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readString(),
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readString(),
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readString(),
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Int::class.java.classLoader) as? Int,
) {
parcel.readValue(Int::class.java.classLoader) as? Int) {
} }
override fun writeToParcel(parcel : Parcel, flags : Int) { override fun writeToParcel(parcel : Parcel, flags : Int) {
parcel.writeValue(titleText) parcel.writeValue(titleText)
parcel.writeString(titleTextValue)
parcel.writeValue(contentText) parcel.writeValue(contentText)
parcel.writeString(contentTextValue)
parcel.writeValue(leftText) parcel.writeValue(leftText)
parcel.writeString(leftTextValue)
parcel.writeValue(rightText) parcel.writeValue(rightText)
parcel.writeString(rightTextValue)
parcel.writeValue(imgFlag) parcel.writeValue(imgFlag)
parcel.writeValue(titleColor) parcel.writeValue(titleColor)
parcel.writeValue(contentColor) parcel.writeValue(contentColor)
parcel.writeValue(leftColor) parcel.writeValue(leftColor)
parcel.writeValue(rightColor) parcel.writeValue(rightColor)
} }
override fun describeContents() : Int { override fun describeContents() : Int {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
} }
} }

+ 3
- 1
app/src/main/java/com/xkl/cdl/module/XKLApplication.kt View File

import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.xkl.cdl.module.floating.DictionaryFloatingWindowManager import com.xkl.cdl.module.floating.DictionaryFloatingWindowManager
import com.xkl.cdl.module.main.MainActivity import com.xkl.cdl.module.main.MainActivity
import com.xkl.cdl.util.IdentificationUtils
import io.reactivex.rxjava3.exceptions.UndeliverableException import io.reactivex.rxjava3.exceptions.UndeliverableException
import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// instance = this
SQLiteDatabase.loadLibs(this) SQLiteDatabase.loadLibs(this)
// LogUtil.e(UUID.randomUUID().toString().replace("-","")) // LogUtil.e(UUID.randomUUID().toString().replace("-",""))
//初始MMKV存储 //初始MMKV存储
// val rootDir = MMKV.initialize(this) // val rootDir = MMKV.initialize(this)

+ 2
- 2
app/src/main/java/com/xkl/cdl/module/learn/LearnExamActivity.kt View File

} }
else -> { else -> {
//默认发音 //默认发音
voiceSwitch.setSoundWay(UserInfoManager.getDefaultSoundWay())
voiceSwitch.setSoundWay(UserInfoManager.instance.getDefaultSoundWay())
voiceSwitch.soundWayChange.observe(this@LearnExamActivity) { voiceSwitch.soundWayChange.observe(this@LearnExamActivity) {
vm.defaultSoundWay = it vm.defaultSoundWay = it
UserInfoManager.putDefaultSoundWay(it)
UserInfoManager.instance.putDefaultSoundWay(it)
if (this@LearnExamActivity::spellAdapter.isInitialized) { if (this@LearnExamActivity::spellAdapter.isInitialized) {
spellAdapter.defaultSoundWay = it spellAdapter.defaultSoundWay = it
} }

+ 2
- 2
app/src/main/java/com/xkl/cdl/module/learn/LearnWordActivity.kt View File

vm.defaultSoundWay = AppConstants.SOUND_TYPE_CN vm.defaultSoundWay = AppConstants.SOUND_TYPE_CN
} }
else -> { else -> {
voiceSwitch.setSoundWay(UserInfoManager.getDefaultSoundWay())
voiceSwitch.setSoundWay(UserInfoManager.instance.getDefaultSoundWay())
voiceSwitch.soundWayChange.observe(this@LearnWordActivity) { voiceSwitch.soundWayChange.observe(this@LearnWordActivity) {
vm.defaultSoundWay = it vm.defaultSoundWay = it
UserInfoManager.putDefaultSoundWay(it)
UserInfoManager.instance.putDefaultSoundWay(it)
if (this@LearnWordActivity::spellAdapter.isInitialized) { if (this@LearnWordActivity::spellAdapter.isInitialized) {
spellAdapter.defaultSoundWay = it spellAdapter.defaultSoundWay = it
} }

+ 4
- 4
app/src/main/java/com/xkl/cdl/module/m_my/MyFragment.kt View File

override fun initFragment() { override fun initFragment() {
//加载头像 //加载头像
UserInfoManager.getUserHeadPortrait()?.let {
UserInfoManager.instance.getUserHeadPortrait()?.let {
ImageLoader.loadImage(ImageLoaderOption().apply { ImageLoader.loadImage(ImageLoaderOption().apply {
targetView = binding.headPortrait targetView = binding.headPortrait
placeholderResId = R.mipmap.img_default placeholderResId = R.mipmap.img_default
override fun onResult(photos : ArrayList<Photo>?, isOriginal : Boolean) { override fun onResult(photos : ArrayList<Photo>?, isOriginal : Boolean) {
photos?.let { photos?.let {
if (it.size > 0) { if (it.size > 0) {
UserInfoManager.putUserHeadPortrait(File(it[0].path))
UserInfoManager.instance.putUserHeadPortrait(File(it[0].path))
ImageLoader.loadImage(binding.headPortrait, it[0].uri) ImageLoader.loadImage(binding.headPortrait, it[0].uri)
} }
} }
} }
//昵称 //昵称
UserInfoManager.getNickname().let {
UserInfoManager.instance.getNickname().let {
if (it.isEmpty()) binding.tvNickname.hint = "点击可设置昵称" if (it.isEmpty()) binding.tvNickname.hint = "点击可设置昵称"
else binding.tvNickname.text = it else binding.tvNickname.text = it
} }
.asInputConfirm("修改昵称", null, binding.tvNickname.text.toString(), "限制为1-12个字符") { .asInputConfirm("修改昵称", null, binding.tvNickname.text.toString(), "限制为1-12个字符") {
//修改确认 //修改确认
if (it.isNotEmpty() && it.length <= 12) { if (it.isNotEmpty() && it.length <= 12) {
UserInfoManager.putNickname(it)
UserInfoManager.instance.putNickname(it)
binding.tvNickname.text = it binding.tvNickname.text = it
} else { } else {
showToast("输入格式错误") showToast("输入格式错误")

+ 2
- 2
app/src/main/java/com/xkl/cdl/module/m_my/SettingActivity.kt View File

binding.titleBar.onBackClick = {finish()} binding.titleBar.onBackClick = {finish()}
//默认发音方式 //默认发音方式
binding.voiceSwitch.setSoundWay(UserInfoManager.getDefaultSoundWay())
binding.voiceSwitch.setSoundWay(UserInfoManager.instance.getDefaultSoundWay())
//清理缓存图标 //清理缓存图标
val arrowRightDrawable = DrawableUti.changeSvgSizeAndColor(resources, R.drawable.ic_arrow_right, R.color.gray_2, 0.667f) val arrowRightDrawable = DrawableUti.changeSvgSizeAndColor(resources, R.drawable.ic_arrow_right, R.color.gray_2, 0.667f)
binding.tvClearCache.setCompoundDrawablesWithIntrinsicBounds(null,null,arrowRightDrawable,null) binding.tvClearCache.setCompoundDrawablesWithIntrinsicBounds(null,null,arrowRightDrawable,null)
binding.voiceSwitch.soundWayChange.observe(this){ binding.voiceSwitch.soundWayChange.observe(this){
UserInfoManager.putDefaultSoundWay(it)
UserInfoManager.instance.putDefaultSoundWay(it)
} }
binding.tvClearCache.click { binding.tvClearCache.click {

+ 54
- 0
app/src/main/java/com/xkl/cdl/module/splash/ActivateActivity.kt View File

package com.xkl.cdl.module.splash

import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.ViewModelProvider
import com.suliang.common.base.activity.BaseActivityVM
import com.suliang.common.base.activity.ToastEvent
import com.suliang.common.extension.click
import com.xkl.cdl.R
import com.xkl.cdl.databinding.ActivityActivateBinding
import com.xkl.cdl.dialog.CommonDialog
import com.xkl.cdl.dialog.CommonDialogBean

/**
* author: suliang
* 2022/12/5 15:17
* describe : 激活设备Activity
*/
class ActivateActivity : BaseActivityVM<ActivityActivateBinding,ActivateViewModel>() {
override fun initActivity(savedInstanceState : Bundle?) {
binding.etLicence.addTextChangedListener {
binding.btActive.isEnabled = !it.isNullOrEmpty() && !it.isNullOrBlank() //不为空、空格 则激活按钮可用
}
binding.btActive.click {
vm.active(binding.etLicence.text.toString())
}
}
override fun loadData() {
vm.activeMutable.observe(this){ requestResult ->
val commonDialogBean = CommonDialogBean(titleText = R.string.activate_result_title, rightText = R.string.sure )
when{
requestResult -> commonDialogBean.contentTextValue = "激活成功"
else -> commonDialogBean.contentTextValue = "激活失败,失败原因:\n ${vm.errorRequestMsg}"
}
CommonDialog.newInstance(commonDialogBean).apply {
onCommonDialogButtonClickListener = { dialog : CommonDialog, _ ->
dialog.dismissAllowingStateLoss()
if (requestResult) { //激活成功、关闭界面
finish() //关闭界面
}
}
}.show(supportFragmentManager, javaClass.name)
}
}
override fun initViewModel() : ActivateViewModel {
return ViewModelProvider(this)[ActivateViewModel::class.java]
}
}

+ 57
- 0
app/src/main/java/com/xkl/cdl/module/splash/ActivateViewModel.kt View File

package com.xkl.cdl.module.splash

import androidx.lifecycle.MutableLiveData
import com.suliang.common.base.viewmodel.BaseViewModel
import com.suliang.common.util.net.NetObserver
import com.suliang.common.util.thread.AppExecutors
import com.xkl.cdl.data.AppConstants
import com.xkl.cdl.data.manager.UserInfoManager
import com.xkl.cdl.net.RequestUtil
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import okhttp3.FormBody
import okhttp3.ResponseBody

/**
* author: suliang
* 2022/12/5 15:19
* describe : 激活VM
*/
class ActivateViewModel : BaseViewModel() {
/** 监听请求结果 */
val activeMutable = MutableLiveData<Boolean>()
var errorRequestMsg : String? = null
/**
* 激活
* @param toString licence
*/
fun active(licence : String) {
showHideLoading(true)
val deviceId = UserInfoManager.instance.getUuid()
val body = FormBody.Builder().add(AppConstants.DEVICE_ID, deviceId)
.add(AppConstants.LICENSE_ID,licence).build()
RequestUtil.postRequest<ResponseBody>("", body)
.subscribeOn(Schedulers.from(AppExecutors.io))
.observeOn(Schedulers.from(AppExecutors.mainThread))
.subscribe(object : NetObserver<ResponseBody>(){
override fun success(t : ResponseBody) {
//保存licence
UserInfoManager.instance.putLicence(licence)
activeMutable.value = true
showHideLoading(false)
}
override fun failure(errorMsg : String?) {
errorRequestMsg = errorMsg
showHideLoading(false)
}
})
}
}

+ 128
- 47
app/src/main/java/com/xkl/cdl/module/splash/SplashActivity.kt View File



import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import appApi.AppApi import appApi.AppApi
import com.googlecode.protobuf.format.JsonFormat
import com.suliang.common.base.activity.BaseActivity
import com.suliang.common.base.activity.BaseActivityVM
import com.suliang.common.util.LogUtil import com.suliang.common.util.LogUtil
import com.suliang.common.util.file.FileUtil import com.suliang.common.util.file.FileUtil
import com.suliang.common.util.thread.AppExecutors import com.suliang.common.util.thread.AppExecutors
import com.xkl.cdl.data.AppConstants import com.xkl.cdl.data.AppConstants
import com.xkl.cdl.data.manager.CourseManager import com.xkl.cdl.data.manager.CourseManager
import com.xkl.cdl.data.manager.FilePathManager import com.xkl.cdl.data.manager.FilePathManager
import com.xkl.cdl.data.manager.UserInfoManager
import com.xkl.cdl.data.manager.db.DbCoursePackManager import com.xkl.cdl.data.manager.db.DbCoursePackManager
import com.xkl.cdl.databinding.ActivitySplashBinding import com.xkl.cdl.databinding.ActivitySplashBinding
import com.xkl.cdl.databinding.DictionaryFloatingLayoutBinding
import com.xkl.cdl.dialog.CommonDialog
import com.xkl.cdl.dialog.CommonDialogBean
import com.xkl.cdl.module.XKLApplication import com.xkl.cdl.module.XKLApplication
import com.xkl.cdl.module.floating.DictionaryFloatingWindowManager
import com.xkl.cdl.module.main.MainActivity import com.xkl.cdl.module.main.MainActivity
import io.reactivex.rxjava3.core.Observable
import java.io.File
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess


@SuppressLint("CustomSplashScreen") @SuppressLint("CustomSplashScreen")
class SplashActivity : BaseActivity<ActivitySplashBinding>() {
class SplashActivity : BaseActivityVM<ActivitySplashBinding,SplashViewModel>() {
override fun onCreateOwn(savedInstanceState : Bundle?) { override fun onCreateOwn(savedInstanceState : Bundle?) {
if (!isTaskRoot) { if (!isTaskRoot) {
} }
// TODO: 激活返回跳转处理
override fun loadData() { override fun loadData() {
//检测唯一码、检测激活码
val uuid = UserInfoManager.instance.getUuid()
if (uuid.isEmpty()) {
//生成唯一码,存入本地
val newUuid = UUID.randomUUID().toString()
UserInfoManager.instance.putUuid(newUuid)
//跳转激活,绑定激活码
startActivity(ActivateActivity::class.java)
return
}
//检测激活码是否存在
if (UserInfoManager.instance.getLicence().isEmpty()) {
//激活码不存在,跳转到激活界面
startActivity(ActivateActivity::class.java)
return
}
showHideLoading(true)
AppExecutors.diskIO.execute {
//读取课程数据
//用户的sort信息
XKLApplication.mobileCache.courseSorted(AppConstants.SUBJECT_ENGLISH.toLong())?.let { value ->
val parseFrom = AppApi.CourseSortedResponse.parseFrom(value).toBuilder()
CourseManager.mSortInfoList.put(AppConstants.SUBJECT_ENGLISH, parseFrom.listBuilderList)
}
XKLApplication.mobileCache.courseSorted(AppConstants.SUBJECT_CHINESE.toLong())?.let { value ->
val parseFrom = AppApi.CourseSortedResponse.parseFrom(value).toBuilder()
CourseManager.mSortInfoList.put(AppConstants.SUBJECT_CHINESE, parseFrom.listBuilderList)
}
//复制词典 和 词汇量测试
val dictionary = FilePathManager.getDictionaryDbPath()
if (!dictionary.exists() || dictionary.length() != FileUtil.getAssetFileSize("dictionary.db")) {
LogUtil.e("复制dictionary.db")
FileUtil.copyAsset("dictionary.db", dictionary)
}
val vocabulary = FilePathManager.getVocabularyDbPath()
if (!vocabulary.exists() || vocabulary.length() != FileUtil.getAssetFileSize("vocabulary.db")) {
LogUtil.e("复制vocabulary.db")
FileUtil.copyAsset("vocabulary.db", vocabulary)
//检查用户下载的课程,拉取到新的直接替换,没有拉取到则使用缓存,如果缓存也没有则弹窗提示
binding.tvShowMsg.text = "检查绑定课程中......"
vm.loadBindCourse()
vm.bindCoursePackResult.observe(this){
if (vm.bindCoursePackList.isNullOrEmpty()) { //如果绑定的数据拉取为空
val commonDialogBean = CommonDialogBean(titleTextValue = "提示", contentTextValue = "未查询到绑定的课程\n请检查网络或联系业务员!",
rightTextValue = "重新拉取", leftTextValue = "退出")
CommonDialog.newInstance(commonDialogBean).apply {
onCommonDialogButtonClickListener = { dialog, isRightClick ->
dialog.dismissAllowingStateLoss()
if (isRightClick){
vm.loadBindCourse()
}else{ //退出应用
finish()
exitProcess(0)
}
}
}.show(supportFragmentManager,"binder")
}else{ //检查数据
binding.tvShowMsg.text = "开始下载课程......"
vm.checkCoursePack()
} }
// TODO: 2022/3/22 读取当前app绑定的课程数据,
// DbCoursePackManager().queryBindingCoursePack("262,261,264,136,547,615,516,411")
//暂时不用口语课程
DbCoursePackManager().queryBindingCoursePack("262,261,264,136,547,516,411")
//复制课程的数据库到对应位置
CourseManager.checkCourseDb()
//定时跳跃到住主界面
AppExecutors.scheduledExecutor.schedule({
AppExecutors.mainThread.execute {
showHideLoading(false)
startActivity(MainActivity::class.java)
finish()
}
}, 1, TimeUnit.SECONDS)
} }
/* 下载失败的提示 */
vm.downLoadResult.observe(this){
val commonDialogBean = CommonDialogBean(titleTextValue = "提示", contentTextValue = "课程下载失败,是否重新下载?",
rightTextValue = "继续", leftTextValue = "重新下载")
CommonDialog.newInstance(commonDialogBean).apply {
onCommonDialogButtonClickListener = { dialog, isRightClick ->
dialog.dismissAllowingStateLoss()
if (isRightClick){
vm.checkCoursePack()
}else{ //继续

}
}
}.show(supportFragmentManager,"download")
}
vm.mergeCoursePackResult.observe(this){
binding.tvShowMsg.text = "资源数据检测中......"
vm.copyResource()
}
vm.allOver.observe(this){
startActivity(MainActivity::class.java)
finish()
}
//
// showHideLoading(true)
//
// AppExecutors.diskIO.execute {
// //读取课程数据
// //用户的sort信息
// XKLApplication.mobileCache.courseSorted(AppConstants.SUBJECT_ENGLISH.toLong())?.let { value ->
// val parseFrom = AppApi.CourseSortedResponse.parseFrom(value).toBuilder()
// CourseManager.mSortInfoList.put(AppConstants.SUBJECT_ENGLISH, parseFrom.listBuilderList)
// }
//
// XKLApplication.mobileCache.courseSorted(AppConstants.SUBJECT_CHINESE.toLong())?.let { value ->
// val parseFrom = AppApi.CourseSortedResponse.parseFrom(value).toBuilder()
// CourseManager.mSortInfoList.put(AppConstants.SUBJECT_CHINESE, parseFrom.listBuilderList)
// }
//
// //复制词典 和 词汇量测试
// val dictionary = FilePathManager.getDictionaryDbPath()
// if (!dictionary.exists() || dictionary.length() != FileUtil.getAssetFileSize("dictionary.db")) {
// LogUtil.e("复制dictionary.db")
// FileUtil.copyAsset("dictionary.db", dictionary)
// }
// val vocabulary = FilePathManager.getVocabularyDbPath()
// if (!vocabulary.exists() || vocabulary.length() != FileUtil.getAssetFileSize("vocabulary.db")) {
// LogUtil.e("复制vocabulary.db")
// FileUtil.copyAsset("vocabulary.db", vocabulary)
// }
//
// // TODO: 2022/3/22 读取当前app绑定的课程数据,
//// DbCoursePackManager().queryBindingCoursePack("262,261,264,136,547,615,516,411")
// //暂时不用口语课程
// DbCoursePackManager().queryBindingCoursePack("262,261,264,136,547,516,411")
// //复制课程的数据库到对应位置
// CourseManager.checkCourseDb()
// //定时跳跃到住主界面
// AppExecutors.scheduledExecutor.schedule({
// AppExecutors.mainThread.execute {
// showHideLoading(false)
// startActivity(MainActivity::class.java)
// finish()
// }
// }, 1, TimeUnit.SECONDS)
// }
} }
override fun initViewModel() : SplashViewModel {
return ViewModelProvider(this)[SplashViewModel::class.java]
}
} }

+ 278
- 0
app/src/main/java/com/xkl/cdl/module/splash/SplashViewModel.kt View File

package com.xkl.cdl.module.splash

import androidx.lifecycle.MutableLiveData
import com.suliang.common.base.viewmodel.BaseViewModel
import com.suliang.common.util.LogUtil
import com.suliang.common.util.file.FileUtil
import com.suliang.common.util.net.DownLoadFileListener
import com.suliang.common.util.net.HttpUtil
import com.suliang.common.util.net.NetObserver
import com.suliang.common.util.thread.AppExecutors
import com.xkl.cdl.data.AppConstants
import com.xkl.cdl.data.bean.course.CoursePack
import com.xkl.cdl.data.manager.CourseManager
import com.xkl.cdl.data.manager.FilePathManager
import com.xkl.cdl.data.manager.UserInfoManager
import com.xkl.cdl.net.RequestUtil
import io.reactivex.rxjava3.schedulers.Schedulers
import okhttp3.FormBody
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipInputStream

/**
* author: suliang
* 2022/12/8 15:30
* describe : 启动检查的ViewModel
*/
class SplashViewModel : BaseViewModel() {
//绑定课程的结果
var bindCoursePackList : List<CoursePack>? = null
val bindCoursePackResult : MutableLiveData<Boolean> = MutableLiveData()
//下载课程与进度
val downLoadNameProgress : MutableLiveData<String> = MutableLiveData()
//下载课程失败
val downLoadResult : MutableLiveData<Boolean> = MutableLiveData()
//组合课程结束
val mergeCoursePackResult : MutableLiveData<Boolean> = MutableLiveData()
//数据处理结束livedata
val allOver : MutableLiveData<Boolean> = MutableLiveData()
/**
* 拉取绑定的课程
* @return
*/
fun loadBindCourse() {
val deviceId = UserInfoManager.instance.getUuid()
val licenceId = UserInfoManager.instance.getLicence()
val body = FormBody.Builder().add(AppConstants.DEVICE_ID, deviceId).add(AppConstants.LICENSE_ID, licenceId).build()
// TODO: 绑定课程的转换
RequestUtil.postRequest<List<CoursePack>>("", body)
.observeOn(Schedulers.from(AppExecutors.io))
.observeOn(Schedulers.from(AppExecutors.mainThread))
.subscribe(object : NetObserver<List<CoursePack>>() {
override fun success(t : List<CoursePack>) {
UserInfoManager.instance.putBindCourse(t)
bindCoursePackList = t
bindCoursePackResult.value = true
}
override fun failure(errorMsg : String?) {
bindCoursePackList = UserInfoManager.instance.getBindCourse()
bindCoursePackResult.value = false
}
})
}
/**
* 开始检查绑定的课程数据,是否进行了下载
*/
fun checkCoursePack() {
//需要下载的数量
var needCount = 0
AppExecutors.io.execute {
//判断该课程的数据的数据包是否存在
bindCoursePackList?.forEach { coursePackInfo : CoursePack ->
var isAdd = false
//课程id 判断数据库是否存在
coursePackInfo.childrenCourses.forEach { courseId ->
val courseID = coursePackInfo.childrenCourses[0]
//数据库不存在
val dbFile = File(FilePathManager.getDbRootPath(),
"${coursePackInfo.subjectId}/${coursePackInfo.coursePackId}/$courseID/course.db")
if (!dbFile.exists() && !isAdd) {
coursePackInfo.isDown = false
needCount++
isAdd = true
}
}
if (!isAdd) {
coursePackInfo.isDown = true
}
}
var downLoadOverSize = 0
val lock : String = ""
//默认下载成功,当有false时,说明下载有失败
var downLoadSuccess = true
//下载课程
bindCoursePackList?.forEach { coursePackBaseInfo ->
if (!coursePackBaseInfo.isDown) {
val saveFile = File(FilePathManager.getZipRootPath(),
coursePackBaseInfo.downLoadZipUrl!!.substringAfterLast("/"))
HttpUtil.instance.downLoadAsync(coursePackBaseInfo.downLoadZipUrl!!, saveFile, object : DownLoadFileListener {
val coursePack = coursePackBaseInfo
override fun downFileSize(fileSize : Long) {
}
override fun downFileProgress(progress : Int) {
downLoadNameProgress.postValue("${coursePack.coursePackName} : $progress%")
}
override fun downFileResult(saveFile : File?) {
synchronized(lock) {
downLoadOverSize++
if (saveFile == null) {
downLoadSuccess = false
}
}
//下载成功,解压到对应的文件
saveFile?.let { file ->
coursePack.let {
val inputStream = FileInputStream(file)
val zipInputStream = ZipInputStream(inputStream)
var count = 0
val buffer = ByteArray(5120)
var nextEntry = zipInputStream.nextEntry
val courseId = coursePack.childrenCourses[0]
//如果是作文,作文需要检查数据库,同时需要检查其下的图片、视频和关键帧
if (it.subjectId == AppConstants.SUBJECT_CHINESE && it.coursePackType == AppConstants.COURSEPACK_TYPE_CHINESE_COMPOSITION) {
// val assetFileName = it.dbPathName.substringBeforeLast(".") + ".zip"
// val inputStream = AppGlobals.application.resources.assets.open(assetFileName)
while (nextEntry != null) {
when {
nextEntry.name.endsWith((".icon.png")) -> File(FilePathManager.getIconRootPath(),
"${it.subjectId}/${coursePack.coursePackId}/icon")
nextEntry.name.endsWith(".mp4.png") -> File(FilePathManager.getMp4PngRootPath(),
"${it.subjectId}/${coursePack.coursePackId}/$courseId/${
nextEntry.name.substringAfterLast(
"/")
}")
nextEntry.name.endsWith(".mp4") -> File(FilePathManager.getMp4RootPath(),
"${it.subjectId}/${coursePack.coursePackId}/$courseId/${
nextEntry.name.substringAfterLast("/")
}")
nextEntry.name.endsWith(".png") -> File(FilePathManager.getPngRootPath(),
"${it.subjectId}/${coursePack.coursePackId}/$courseId/${
nextEntry.name.substringAfterLast("/")
}")
nextEntry.name.endsWith(".db") -> File(FilePathManager.getDbRootPath(),
"${it.subjectId}/${coursePack.coursePackId}/$courseId/course.db")
else -> null
}?.let { file ->
//文件不存在 ,复制当前文件进入该文件
if (!file.exists()) {
file.parentFile?.let { parentFile ->
if (!parentFile.exists()) parentFile.mkdirs()
}
val outputStream = BufferedOutputStream(FileOutputStream(file))
while ((zipInputStream.read(buffer).also { count = it }) != -1) {
outputStream.write(buffer, 0, count)
}
outputStream.close()
}
}
nextEntry = zipInputStream.nextEntry
}
} else { //其他的项目只需要复制和检查数据库
while (nextEntry != null) {
when {
nextEntry.name.endsWith(".db") -> File(FilePathManager.getDbRootPath(),
"${it.subjectId}/${coursePack.coursePackId}/$courseId/course.db")
nextEntry.name.endsWith((".icon.png")) -> File(FilePathManager.getIconRootPath(),
"${it.subjectId}/${coursePack.coursePackId}/icon")
else -> null
}?.let {
if (!it.exists()) {
it.parentFile?.let { parentFile ->
if (!parentFile.exists()) parentFile.mkdirs()
}
val outputStream = BufferedOutputStream(FileOutputStream(it))
while ((zipInputStream.read(buffer).also { count = it }) != -1) {
outputStream.write(buffer, 0, count)
}
outputStream.close()
}
}
nextEntry = zipInputStream.nextEntry
}
}
}
}
}
})
}
// 等待下载完成
while (downLoadOverSize != needCount) {
}
//判断下载是否成功
if (!downLoadSuccess) { //下载失败、弹窗提示
downLoadResult.postValue(false)
return@execute
}
//组合用户的课程包信息
//英语课程包
val englishCoursePack = mutableListOf<CoursePack>()
//语文课程包
val chineseCoursePack = mutableListOf<CoursePack>()
bindCoursePackList?.forEach {
if (it.isDown) {
it.cover = File(FilePathManager.getIconRootPath(), "${it.subjectId}/${it.coursePackId}/icon").path
//初始作文设置进度
if (it.subjectId == AppConstants.SUBJECT_CHINESE){
//语文初始需要给设置一下其进度,用于显示
run m@{
CourseManager.mSortInfoList[AppConstants.SUBJECT_CHINESE]?.forEach { sortInfo ->
if (it.coursePackId == sortInfo.packId && it.childrenCourses[0].courseId == sortInfo.courseId ){
it.childrenCourses[0].courseLearnProgress = sortInfo.s //设置进度
return@m
}
}
}
it.inCoursePackPosition = chineseCoursePack.size
chineseCoursePack.add(it)
}else{
it.inCoursePackPosition = englishCoursePack.size
englishCoursePack.add(it)
}
}
}
CourseManager.subjectWithCoursePackMap[AppConstants.SUBJECT_ENGLISH] = englishCoursePack.toList()
CourseManager.subjectWithCoursePackMap[AppConstants.SUBJECT_CHINESE] = chineseCoursePack.toList()
mergeCoursePackResult.postValue(true)
}
}
}
fun copyResource() {
AppExecutors.diskIO.run {
//复制词典 和 词汇量测试
val dictionary = FilePathManager.getDictionaryDbPath()
if (!dictionary.exists() || dictionary.length() != FileUtil.getAssetFileSize("dictionary.db")) {
LogUtil.e("复制dictionary.db")
FileUtil.copyAsset("dictionary.db", dictionary)
}
val vocabulary = FilePathManager.getVocabularyDbPath()
if (!vocabulary.exists() || vocabulary.length() != FileUtil.getAssetFileSize("vocabulary.db")) {
LogUtil.e("复制vocabulary.db")
FileUtil.copyAsset("vocabulary.db", vocabulary)
}
}
}
}

+ 13
- 0
app/src/main/java/com/xkl/cdl/net/ApiService.kt View File

package com.xkl.cdl.net

import com.suliang.common.util.net.IApiService
import okhttp3.ResponseBody

/**
* author: suliang
* 2022/12/7 16:36
* describe : api 接口
*/
interface ApiService : IApiService {

}

+ 102
- 0
app/src/main/java/com/xkl/cdl/net/RequestUtil.kt View File

package com.xkl.cdl.net

import com.suliang.common.util.net.HttpUtil
import io.reactivex.rxjava3.core.Observable
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Call

/**
* author: suliang
* 2022/12/7 16:35
* describe : 网络请求
*/
class RequestUtil {
companion object{
private val apiService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
HttpUtil.instance.getRetrofitService(ApiService::class.java)
}
/**
* 有参请求
* @param url 路径
* @param body 内容
* @return
*/
fun <T> postRequest(url : String, body : RequestBody) : Observable<T> {
return apiService.postRequest(url,body)
}
/* *//**
* 有参请求 带header
* @param url 路径
* @param body 内容
* @return
*//*
fun postRequestHasToken(url : String?, body : RequestBody) : ResponseBody {
// val token : String = SPUtils.getInstance().getString(SPUtils.USER_TOKEN)
return apiService.postRequest(url,token,body)
}*/
/**
* 有参请求 带header
* @param url 路径
* @param body 内容
* @return
*/
fun <T> postRequestHasToken(url : String, body : RequestBody, token : String) : Observable<T> {
return apiService.postRequest(url, token, body)
}
/**
* 无参请求
*
* @param url 路径
* @return
*/
fun <T> postRequest(url : String?) : Observable<T> {
return apiService.postRequest(url)
}
/**
* 无参请求,带header token
*
* @param url 路径
* @param token header token
* @return
*/
fun <T> postRequest(url : String, token : String) : Observable<T> {
return apiService.postRequest(url, token)
}
/**
* 上传头像
*/
fun <T> updateImage(token : String, requestBody : MultipartBody.Part) : Observable<T> {
return apiService.updateImage(token, requestBody)
}
/* *//**
* 无参请求,带header token
*
* @param url 路径
* @param token header token
* @return
*//*
fun postRequest(url : String?, token : String?) : Observable<ResponseBody?>? {
return RetrofitClient.getApiService().postRequest(url, token)
}
*//**
* 上传头像
*//*
fun updateImage(token : String?, requestBody : Part?) : Observable<ResponseBody?>? {
return RetrofitClient.getApiService().updateImage(token, requestBody)
}
*/
}
}

+ 207
- 0
app/src/main/java/com/xkl/cdl/util/IdentificationUtils.kt View File

package com.xkl.cdl.util

import android.content.Context
import android.os.Build
import android.provider.Settings
import android.telephony.TelephonyManager
import com.suliang.common.util.os.VersionUtil
import com.xkl.cdl.module.XKLApplication
import java.lang.StringBuilder
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and

/**
* author: suliang
* 2022/12/1 15:27
* describe : 获取唯一标识符
*/
class IdentificationUtils {
// companion object{
// /**
// * 获取设备的device Id
// * Android 系统为开发则提供的用于标识手机设备的串号
// * 问题:
// * 1 - 非手机设备获取不到 DEVICE_ID
// * 2 - 需要获取 READ_PHONE_STATE 权限
// * 3 - 有的手机设备该实现有漏洞,会返回垃圾,如: zeros 或者 asterisks 产品
// */
// @JvmStatic
// fun getDeviceId() : String{
// val telephoneService = XKLApplication.instance().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// val deviceId = telephoneService.deviceId
// return deviceId
// }
//
// /**
// * 获取 Mac 地址 : 使用手机 wifi 或 蓝牙的 MAC 地址作为唯一标示,不推荐
// * 问题:
// * 1 - 硬件限制:不是所有设备都有WIFI 和 蓝牙硬件,硬件不存在就获取不到
// * 2 - 如果Wifi没有打开过,是无法获取其Mac 地址,蓝牙只有在打开的时候才能获取倒其Mac地址
// */
// @JvmStatic
// fun getMacAddress(){
//
// }
//
// /**
// * Get sim serial number
// * 装有SIM 卡的 2.3 以上设备,可获取
// * 问题:
// * 1 - 对于CDMA设备,返回的是一个空值
// * 2 - 需要 READ_PHONE_STATE 权限
// * @return
// */
// @JvmStatic
// fun getSimSerialNumber() : String{
// val telephoneService = XKLApplication.instance().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// val simSerialNumber = telephoneService.simSerialNumber
// return simSerialNumber
// }
//
// /**
// * Get android id
// * 设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串形式保存下来,当设备被wipe后该值会被重置
// * 问题:
// * 1- 厂商定制的bug : 不同设备可能场所相同的 ANDROID_ID
// * 2- 厂商定制的bug : 有些设备返回的值位null
// * 3- 设备差异: 对于 CDMA设备来说,ANDROID_ID 与 DEVICE_ID 返回相同的值
// *
// * @return
// */
// @JvmStatic
// fun getAndroidId() : String {
// return Settings.System.getString(XKLApplication.instance().contentResolver,Settings.System.ANDROID_ID)
// }
//
// /**
// * Get serial number
// * 问题: 需要权限 READ_PHONE_STATE
// * @return
// */
// @JvmStatic
// fun getSerialNumber() : String{
// return Build.getSerial()
// }
//
// /**
// * 程序安装的id
// * 程序安装后第一次运行时会生成一个ID,该方式与设备标识不一样,不同的应用程序会产生不同的ID,同一个程序重新安装也会不同。
// * 这不是唯一ID,但可以保证每个用户的ID是不同的,可以用来跟踪应用的安装数量等
// * 自己生成
// * @return
// */
// @JvmStatic
// fun getInstallationId() {
//
// }
//
// /**
// * 获取唯一标识 方案一 UUID + SP保存
// * App首次使用的时候,创建UUID,并保存,以后再次使用时,直接获取
// * 优点: 数据唯一,不需要权限
// * 缺点: 会随App一起删除,重新安装UUID值会改变
// * @return
// */
// @JvmStatic
// fun getIdentification_1(): String{
//
// }
//
// /**
// * 获取唯一标识 方案二 UUID + SD保存
// * App首次使用的时候,创建UUID,并保存,以后再次使用时,直接获取
// * 优点: 数据唯一,不会随App一起删除
// * 缺点: 需要SD卡权限,防不住用户手动删除了SD卡的文件
// * @return
// */
// @JvmStatic
// fun getIdentification_2():String{
//
// }
//
/**
* 获取唯一标识 方案三 imei + android_id + serial + 硬件uuid(自生成)
* https://blog.csdn.net/qq_27061049/article/details/122559367
* @return
*/
// @JvmStatic
// fun getIdentification_3():String{
// val sbDeviceId = StringBuilder()
// val context = XKLApplication.instance()
// //1 获取 deviceId version>=M 时,不获取这样就不需要手动获取权限 READ_PHONE_STATE
// if (!VersionUtil.versionMorLater()){
// val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// val deviceId = telephonyManager.deviceId
// if (deviceId.isNotEmpty()){
// sbDeviceId.append(deviceId).append("|")
// }
// }
// //2 获取 AndroidId ,无须权限
// val androidId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
// if (androidId.isNotEmpty()){
// sbDeviceId.append(androidId).append("|")
// }
// //3 获取设备序列号 serial ,个别设备获取不到
// val serial = Build.SERIAL
// if (serial.isNotEmpty()){
// sbDeviceId.append(serial).append("|")
// }
// // 4 获取硬件uuid
// val dev = "202222" +
// Build.BOARD.length % 10 +
// Build.BRAND.length % 10 +
// Build.DEVICE.length % 10 +
// Build.HARDWARE.length % 10 +
// Build.ID.length % 10 +
// Build.MODEL.length % 10 +
// Build.PRODUCT.length % 10 +
// Build.SERIAL.length % 10
// val uuid = UUID(dev.hashCode().toLong(), Build.SERIAL.hashCode().toLong()).toString()
// if (uuid.isNotEmpty()){
// sbDeviceId.append(uuid)
// }
// //5 生成SHA1,统一 DEVICEID 长度
// if (sbDeviceId.isNotEmpty()){
// try {
// val messageDigest = MessageDigest.getInstance("SHA1")
// messageDigest.reset()
// messageDigest.update(sbDeviceId.toString().toByteArray(Charsets.UTF_8))
// val digest = messageDigest.digest()
// //转16进制字符串
// val sb = StringBuilder()
// var stmp : String
// digest.forEach { byte ->
// stmp = byte.toString(16)
// if (stmp.length == 1){
// sb.append("0")
// }
// sb.append(stmp)
// }
// val sha1 = sb.toString().uppercase(Locale.CHINA)
// if (sha1.isNotEmpty()){
// return sha1
// }
// }catch (ex: Exception){
// ex.printStackTrace()
// }
// }
//
// //如果以上硬件标识数据均无法获得,
// //则DeviceId默认使用系统随机数,这样保证DeviceId不为空
// val uid = UUID.randomUUID().toString().replace("-", "")
// return uid
// }
/**
* 使用UUID生成唯一标识,保存在设备中
* @return
*/
// fun getIdentification(): String {
// return UUID.randomUUID().toString()
// }
// }
}

+ 60
- 0
app/src/main/res/layout/activity_activate.xml View File

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".module.splash.ActivateActivity"
android:background="@color/white">

<com.suliang.common.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/title_bar_height"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:titleTextValue="@string/activate"/>

<TextView
android:id="@+id/tv_flag_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="激活码"
android:textSize="@dimen/normalSize"
android:textColor="@color/main_text_color"
android:layout_marginStart="@dimen/global_spacing"
android:layout_marginTop="40dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar"/>

<EditText
android:id="@+id/et_licence"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/tv_flag_1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_flag_1"
android:textSize="@dimen/normalSize"
android:textColor="@color/main_text_color"
android:layout_marginTop="@dimen/global_spacing"
android:layout_marginEnd="@dimen/global_spacing"
android:maxLines="1"
android:hint="请填入激活码"/>


<com.google.android.material.button.MaterialButton
android:id="@+id/bt_active"
style="@style/common_button_style"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_licence"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.3"
android:text="@string/activate"
android:enabled="false"
android:backgroundTint="@color/com_btn_enable_selector"
/>

</androidx.constraintlayout.widget.ConstraintLayout>

+ 2
- 2
app/src/main/res/layout/activity_course_main.xml View File

android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scaleType="fitXY" android:scaleType="fitXY"
bind:imgBytes="@{coursePack.cover}"
bind:imgFilePathBlur="@{coursePack.cover}"
bind:blur= "@{true}" bind:blur= "@{true}"
app:layout_constraintBottom_toBottomOf="@+id/view_placeholder_1" app:layout_constraintBottom_toBottomOf="@+id/view_placeholder_1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginStart="@dimen/global_spacing" android:layout_marginStart="@dimen/global_spacing"
android:scaleType="fitXY" android:scaleType="fitXY"
app:imageByteArray="@{coursePack.cover}"
app:imgFilePath="@{coursePack.cover}"
app:layout_constraintBottom_toTopOf="@+id/view_placeholder_1" app:layout_constraintBottom_toTopOf="@+id/view_placeholder_1"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar" app:layout_constraintTop_toBottomOf="@id/title_bar"

+ 1
- 1
app/src/main/res/layout/activity_course_statistics_detail.xml View File

android:layout_width="52dp" android:layout_width="52dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scaleType="fitXY" android:scaleType="fitXY"
app:imageByteArray="@{course.cover}"
app:imgFilePath="@{course.cover}"
app:layout_constraintDimensionRatio="52:72" app:layout_constraintDimensionRatio="52:72"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

+ 13
- 0
app/src/main/res/layout/activity_splash.xml View File

app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.6" /> app:layout_constraintVertical_bias="0.6" />



<TextView
android:id="@+id/tv_show_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/img"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="@dimen/normalSize"
android:textColor="@color/main_text_color"/>


</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

+ 1
- 1
app/src/main/res/layout/item_statistics_course.xml View File

android:scaleType="fitXY" android:scaleType="fitXY"
tools:src="@color/main_text_color" tools:src="@color/main_text_color"
app:shapeAppearance="@style/roundedCornerStyle" app:shapeAppearance="@style/roundedCornerStyle"
app:imageByteArray="@{course.cover}"/>
app:imgFilePath="@{course.cover}"/>


<androidx.constraintlayout.utils.widget.ImageFilterView <androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_arrow" android:id="@+id/iv_arrow"

+ 1
- 1
app/src/main/res/layout/main_item_coursepack.xml View File

android:layout_width="75dp" android:layout_width="75dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="fitXY" android:scaleType="fitXY"
app:imageByteArray="@{coursePack.cover}"
app:imgFilePath = "@{coursePack.cover}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

+ 3
- 0
app/src/main/res/values/strings.xml View File

<string name="floating_dictionary_tips">词典使用提示</string> <string name="floating_dictionary_tips">词典使用提示</string>
<string name="floating_dictionary_content">为了在学习中可以使用词典功能,需要手动打开并同意悬浮窗权限!</string> <string name="floating_dictionary_content">为了在学习中可以使用词典功能,需要手动打开并同意悬浮窗权限!</string>
<string name="floating_dictionary_right_button">去打开</string> <string name="floating_dictionary_right_button">去打开</string>
<string name="activate">激活</string>
<string name="activate_result_title">激活结果</string>
<string name="activate_result_success">激活成功</string>


</resources> </resources>

+ 7
- 0
build.gradle View File

Paging: "androidx.paging:paging-runtime:3.0.0-alpha08", Paging: "androidx.paging:paging-runtime:3.0.0-alpha08",
//room_paging //room_paging
// Room_Paging : "androidx.room:room-paging:${versions.lifecycle_version}" // Room_Paging : "androidx.room:room-paging:${versions.lifecycle_version}"
Okhttp : 'com.squareup.okhttp3:okhttp:4.10.0',
OkhttpLoggingIntercepter : 'com.squareup.okhttp3:logging-interceptor:4.10.0',
Retrofit : 'com.squareup.retrofit2:retrofit:2.9.0',
RetrofitProtobuf : 'com.squareup.retrofit2:converter-protobuf:2.9.0',
RetrofitScalars : 'com.squareup.retrofit2:converter-scalars:2.9.0',
RetrofitGson : 'com.squareup.retrofit2:converter-gson:2.9.0',
Rxjava : 'io.reactivex.rxjava3:rxjava:3.1.5'
] ]





+ 8
- 0
lib/common/build.gradle View File

api customDependencies.MMKV api customDependencies.MMKV
// liveEventBus // liveEventBus
api customDependencies.liveEventBus api customDependencies.liveEventBus
// okhttp
api customDependencies.Okhttp
api customDependencies.OkhttpLoggingIntercepter
api customDependencies.Retrofit
api customDependencies.RetrofitScalars
api customDependencies.RetrofitGson
api customDependencies.RetrofitProtobuf
api customDependencies.Rxjava







+ 5
- 4
lib/common/src/main/AndroidManifest.xml View File

xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.suliang.common"> package="com.suliang.common">


<!-- &lt;!&ndash;网络权限&ndash;&gt;-->
<!-- <uses-permission android:name="android.permission.INTERNET"/>-->
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>-->
<!-- &lt;!&ndash;本地外置存储权限&ndash;&gt;-->
<!--网络权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--本地外置存储权限-->
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>--> <!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>-->
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"--> <!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"-->
<!-- tools:ignore="ScopedStorage" />--> <!-- tools:ignore="ScopedStorage" />-->
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
--> -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:networkSecurityConfig="@xml/network_security_config"/> android:networkSecurityConfig="@xml/network_security_config"/>



+ 12
- 10
lib/common/src/main/java/com/suliang/common/util/AppGlobals.kt View File

/** /**
* 用于获取全局Applicaiton * 用于获取全局Applicaiton
*/ */
object AppGlobals {
val application : Application by lazy {
try {
val atClass = Class.forName("android.app.ActivityThread")
val currentApplicationMethod = atClass.getDeclaredMethod("currentApplication")
currentApplicationMethod.isAccessible = true
currentApplicationMethod.invoke(null) as Application
}catch (e:Exception){
e.printStackTrace()
throw RuntimeException("AppGlobals获取Application异常")
class AppGlobals {
companion object {
val application : Application by lazy {
try {
val atClass = Class.forName("android.app.ActivityThread")
val currentApplicationMethod = atClass.getDeclaredMethod("currentApplication")
currentApplicationMethod.isAccessible = true
currentApplicationMethod.invoke(null) as Application
} catch (e : Exception) {
e.printStackTrace()
throw RuntimeException("AppGlobals获取Application异常")
}
} }
} }
} }

+ 81
- 2
lib/common/src/main/java/com/suliang/common/util/SpUtils.kt View File

package com.suliang.common.util package com.suliang.common.util


import android.os.Parcelable import android.os.Parcelable
import android.util.Base64
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import java.io.*


/** /**
* author suliang * author suliang
*/ */
class SpUtils { class SpUtils {
companion object{ companion object{
val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
SpUtils() SpUtils()
fun <T> decode(key : String, c : Class<T> , defaultValue : T ) : T{ fun <T> decode(key : String, c : Class<T> , defaultValue : T ) : T{
return when(c){
return when(c){
Int::class.java -> kv.decodeInt(key,defaultValue as Int) Int::class.java -> kv.decodeInt(key,defaultValue as Int)
Long::class.java -> kv.decodeLong(key,defaultValue as Long) Long::class.java -> kv.decodeLong(key,defaultValue as Long)
Float::class.java -> kv.decodeFloat(key, defaultValue as Float) Float::class.java -> kv.decodeFloat(key, defaultValue as Float)
kv.clearAll() kv.clearAll()
} }
/**
* 存储list对象
* @param T Serializable子类对象
* @param key 存放key
* @param value 存放value
*/
fun <T : Serializable> encodeList(key : String, value : List<T>){
val v = objToBase64(value)
encode(key,v)
}
/**
* 获取list对象
* @param T Serializable子类对象
* @param key 获取key
* @return
*/
fun <T : Serializable> decodeList(key : String) : List<T>? {
val decode : String = decode(key, String::class.java, "")
return base64ToObj<List<T>>(decode)
}
/**
* 对象转字符串
* @param any 任意对象
* @return base64后的字符串
*/
private fun objToBase64(any : Any) : String{
var result = ""
val byteArrayOutputStream : ByteArrayOutputStream = ByteArrayOutputStream()
val oos : ObjectOutputStream = ObjectOutputStream(byteArrayOutputStream)
try {
oos.writeObject(any)
//将对象放到 outputStream中,将对象转换为byte数组并将其进行base64编码
val encode : ByteArray = Base64.encode(byteArrayOutputStream.toByteArray(), Base64.DEFAULT)
//转为字符串
result = String(encode)
} catch (e : Exception) {
e.printStackTrace()
} finally {
try {
byteArrayOutputStream.close()
oos.close()
} catch (e : Exception) {
e.printStackTrace()
}
}
return result
}
/**
* Base64转对象
* @param T 需要的对象
* @param base64 用于转换的字符串
* @return 转换后的对象
*/
private fun <T> base64ToObj(base64 :String) : T?{
val decode : ByteArray = Base64.decode(base64, Base64.DEFAULT)
val byteArrayInputStream : ByteArrayInputStream = ByteArrayInputStream(decode)
val ois : ObjectInputStream = ObjectInputStream(byteArrayInputStream)
var t : T? = null
try {
t = ois.readObject() as T
}catch (e: Exception){
e.printStackTrace()
} finally {
try {
byteArrayInputStream.close()
ois.close()
} catch (e : Exception) {
e.printStackTrace()
}
}
return t
}
} }

+ 31
- 0
lib/common/src/main/java/com/suliang/common/util/net/DownLoadFileListener.kt View File

package com.suliang.common.util.net

import java.io.File

/**
* author: suliang
* 2022/12/6 14:43
* describe : 下载监听
*/
interface DownLoadFileListener {
/**
* 下载文件的总长度
* @param fileSize
*/
fun downFileSize(fileSize : Long)
/***
* 下载进度
* @param progress 进度值,百分值
*/
fun downFileProgress(progress:Int)
/**
* 下载结果
* @param saveFile 成功为保存的文件路径 失败为空
*/
fun downFileResult(saveFile : File?)

}

+ 193
- 0
lib/common/src/main/java/com/suliang/common/util/net/HttpUtil.kt View File

package com.suliang.common.util.net

import android.widget.Toast
import com.suliang.common.util.AppGlobals
import com.suliang.common.util.file.FileUtil
import com.suliang.common.util.thread.MainThreadExecutor
import okhttp3.*
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.*
import java.util.concurrent.TimeUnit

/**
* author: suliang
* 2022/12/6 10:48
* describe : Okhttp封装类
*/
class HttpUtil private constructor() {
companion object {
val instance : HttpUtil by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { HttpUtil() }
private const val time_out = 3000L
private val okHttpClient : OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
OkHttpClient.Builder()
.connectTimeout(time_out, TimeUnit.SECONDS)
.readTimeout(time_out, TimeUnit.SECONDS)
.writeTimeout(time_out, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(RetryInterceptor(2)) //重试拦截器
.build()
}
private val retrofit : Retrofit by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Retrofit.Builder().baseUrl("")
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(ProtoConverterFactory.create())
.build()
}
}
fun getOkhttpClient() : OkHttpClient {
return okHttpClient
}
fun <T : IApiService> getRetrofitService (service : Class<T> ) : T {
return retrofit.create(service)
}
/**
* 同步请求
* @param request 请求request
* @return
*/
fun syncRequest(url : String, requestMap : MutableMap<String, String>) : Response {
val formBody = FormBody.Builder()
requestMap.forEach { (key, value) ->
formBody.add(key,value)
}
RequestBody
val request = Request.Builder().url(url).post(formBody.build()).build()
return okHttpClient.newCall(request).execute()
}
/**
* 异步请求
* @param request 请求request
* @param callback 请求回调
*/
fun asyncRequest(request : Request,callback: OkhttpResultCallback<Any>?){
callback?.onHandleStart()
okHttpClient.newCall(request).enqueue(object :Callback{
override fun onFailure(call : Call, e : IOException) {
callback?.onHandleFailure(call,e)
}
override fun onResponse(call : Call, response : Response) {
callback?.onResponse(call,response)
}
})
}
/**
* 异步下载
* @param urlPath 下载路径
* @param savePathFile 保存地址,带文件地址
* @param downLoadFileListener 下载监听
*/
fun downLoadAsync(urlPath : String, savePathFile : File, downLoadFileListener : DownLoadFileListener) {
if (!NetworkUtil.isConnected()) {
try {
MainThreadExecutor.run {
Toast.makeText(AppGlobals.application, "网络不可用", Toast.LENGTH_SHORT).show()
}
} catch (e : Exception) {
e.printStackTrace()
}
downLoadFileListener.downFileResult(null)
return
}
//下载
val newCall = okHttpClient.newCall(Request.Builder().url(urlPath).build())
newCall.enqueue(object : Callback {
override fun onFailure(call : Call, e : IOException) {
e.printStackTrace()
downLoadFileListener.downFileResult(null)
}
override fun onResponse(call : Call, response : Response) {
//下载失败
if (!response.isSuccessful) {
downLoadFileListener.downFileResult(null)
return
}
//下载成功
val body = response.body
var inputStream : InputStream? = null
var outputStream : OutputStream? = null
body?.let {
try {
val contentLength = it.contentLength() //总长度
downLoadFileListener.downFileSize(contentLength) //回调总长度
inputStream = it.byteStream()
outputStream = FileOutputStream(savePathFile)
var readLength : Int = 0
var writeLength = 0f
val bytes = ByteArray(1025 * 1025 * 5)
var progress = 0
while ((inputStream!!.read(bytes)).also {
readLength = it
} != -1) {
outputStream!!.write(bytes, 0, readLength)
writeLength += readLength
progress = (writeLength / contentLength * 100).toInt()
downLoadFileListener.downFileProgress(progress) //下载百分比
}
downLoadFileListener.downFileResult(savePathFile)
outputStream!!.flush()
} catch (e : Exception) {
e.printStackTrace()
downLoadFileListener.downFileResult(null)
}finally {
try {
outputStream?.close()
inputStream?.close()
}catch (e: Exception){
e.printStackTrace()
}
}
}
}
})
}
/**
* 取消全部请求队列
*/
fun cancelAll(){
okHttpClient.dispatcher.cancelAll()
}
/**
* 根据标记取消请求的队列和排队中的队列
*
* @param tag
*/
fun cancel(tag: String){
val dispatcher = okHttpClient.dispatcher
cancelCall(dispatcher.runningCalls(),tag)
cancelCall(dispatcher.queuedCalls(),tag)
}
private fun cancelCall(callList:List<Call>? , tag : String){
callList?.let { list ->
list.filter {
it.request().tag() == tag
}.forEach{ call ->
call.cancel()
}
}
}
}

+ 62
- 0
lib/common/src/main/java/com/suliang/common/util/net/IApiService.kt View File

package com.suliang.common.util.net

import io.reactivex.rxjava3.core.Observable
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.*

/**
* author: suliang
* 2022/12/7 16:36
* describe : api 接口
*/
interface IApiService {

@POST
@Headers("contentType : application/xkl_v2",
"accept : application/xkl_v2")
fun <T> postRequest(@Url url : String, @Body body: RequestBody) : Observable<T>
@POST
@Headers("Content-Type:application/xkl_v2", "accept:application/xkl_v2")
fun <T> postRequest(@Url url : String?,
@Header("Authorization") token : String,
@Body body : RequestBody) : Observable<T>
@POST
@Headers("accept:application/xkl_v2")
fun <T> postRequest(@Url url : String?) : Observable<T>
@POST
@Headers("accept:application/xkl_v2")
fun <T> postRequest(@Url url : String?, @Header("Authorization") token : String) : Observable<T>
/**
* 上传头像
*/
@Multipart
@POST("upload/upload") //base路径直接连接成为全路径
@Headers("accept:application/xkl_v2")
fun <T> updateImage(@Header("Authorization") token : String, @Part body : MultipartBody.Part) : Observable<T>
/* *//**
* 有参请求 带header
* @param url 路径
* @param body 内容
* @return
*//*
fun postRequestHasToken(url : String?, body : RequestBody?, token : String?) : Observable<ResponseBody?>? {
return RetrofitClient.getApiService().postRequest(url, token, body)
}*/
}

+ 71
- 0
lib/common/src/main/java/com/suliang/common/util/net/NetObserver.kt View File

package com.suliang.common.util.net

import io.reactivex.rxjava3.core.Observer
import io.reactivex.rxjava3.disposables.Disposable
import okhttp3.ResponseBody
import java.io.IOException
import java.nio.ByteBuffer

/**
* author: suliang
* 2022/12/8 10:00
* describe : Retrofit + Rxjava Observer
*/
abstract class NetObserver<T> : Observer<T>{
override fun onSubscribe(d : Disposable) {
}
override fun onNext(t : T) {
// try {
// val data : ByteArray = responseBody.bytes()
// val buffer : ByteBuffer = PbZstdUtil.parseHeaders(data)
// val stateCode : Int = PbZstdUtil.parseCode(buffer)
// val opCode : Int = PbZstdUtil.parseOpCode(buffer)
// val msgCodeKey = stateCode.toString() + "_" + opCode //对应状态码key
// L.e("OkHttp", msgCodeKey + LanguageManager.getInstance().getValue(msgCodeKey))
// if (stateCode == 200) { //成功
// //解压后数据
// val protobufData : ByteArray = PbZstdUtil.parseCompressData(buffer, data)
// onSuccess(msgCodeKey, protobufData)
// } else { //失败
// if (stateCode == 499) {
// on499Error(msgCodeKey, data, buffer)
// } else if (stateCode == 402 || stateCode == 405) {
// on402(msgCodeKey)
// } else {
// ToastUtils.showShort(LanguageManager.getInstance().getValue(msgCodeKey))
// onFailed(msgCodeKey)
// }
// }
// } catch (e : IOException) {
// e.printStackTrace()
// }
success(t)
}
override fun onError(e : Throwable) {
failure(e.message)
}
override fun onComplete() {
}
/**
* 成功
* @param t 返回的类型
*/
abstract fun success(t : T)
/**
* 失败
* @param errorMsg 失败原因
*/
abstract fun failure(errorMsg : String?)
}

+ 13
- 0
lib/common/src/main/java/com/suliang/common/util/net/NetworkState.kt View File

package com.suliang.common.util.net

/**
* author: suliang
* 2022/12/6 17:02
* describe : 网络状态
*/
enum class NetworkState {
NONE,
WIFI,
CELLULAR,
ETHERNET
}

+ 72
- 0
lib/common/src/main/java/com/suliang/common/util/net/NetworkUtil.kt View File

package com.suliang.common.util.net

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import com.suliang.common.base.LibApplication
import com.suliang.common.util.AppGlobals
import com.suliang.common.util.os.VersionUtil

/**
* author: suliang
* 2022/12/6 15:06
* describe : 网络工具
*/
class NetworkUtil {
companion object {
/**
* 判断当前设备是否有网络连接
* @return
*/
fun isConnected() : Boolean{
try {
val connectivityManager = AppGlobals.application.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager?
if (VersionUtil.versionMorLater()){
val activeNetwork = connectivityManager?.activeNetwork ?: return false
val networkCapabilities = connectivityManager?.getNetworkCapabilities(activeNetwork) ?: return false
return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}else{
val networkInfo = connectivityManager?.activeNetworkInfo?: return false
return networkInfo.isAvailable && networkInfo.isConnected
}
} catch (e : Exception) {
e.printStackTrace()
}
return false
}
fun getNetworkState() : NetworkState {
try {
val connectivityManager = AppGlobals.application.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager?
if (VersionUtil.versionMorLater()){
val activeNetwork = connectivityManager?.activeNetwork ?: return NetworkState.NONE
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return NetworkState.NONE
return when{
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkState.WIFI
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> NetworkState.CELLULAR
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> NetworkState.ETHERNET
else -> return NetworkState.NONE
}
}else{
return when(connectivityManager?.activeNetworkInfo?.type){
ConnectivityManager.TYPE_MOBILE -> NetworkState.CELLULAR
ConnectivityManager.TYPE_WIFI -> NetworkState.WIFI
ConnectivityManager.TYPE_ETHERNET -> NetworkState.ETHERNET
else -> NetworkState.NONE
}
}
}catch (e : Exception){
e.printStackTrace()
}
return NetworkState.NONE
}
}
}

+ 32
- 0
lib/common/src/main/java/com/suliang/common/util/net/OkhttpResultCallback.kt View File

package com.suliang.common.util.net

import okhttp3.Call
import okhttp3.Response
import java.lang.Exception

/**
* author: suliang
* 2022/12/7 10:47
* describe : Okhttp网络请求回调
*/
abstract class OkhttpResultCallback<T> {
fun onHandleStart(){}
fun onHandleFailure(call: Call? , exception : Exception?){}
fun onResponse(call : Call? , response : Response){}
fun onHandleProgress(byteRead : Long ,total : Long){}
open abstract fun onStart()
open abstract fun onHandleResponse(call : Call?, response : Response)
open abstract fun onProgress(byteRead : Long, total : Long)
abstract fun onSuccess(call : Call? ,response : T)

abstract fun onFinish()
}

+ 69
- 0
lib/common/src/main/java/com/suliang/common/util/net/RetryInterceptor.kt View File

package com.suliang.common.util.net

import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.lang.Exception
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import kotlin.Throws

/**
* author suliang
* create 2022/7/22 16:25
* Describe:
*/
internal class RetryInterceptor(var maxRetryCount : Int) : Interceptor {
var retryCount = 0
@Throws(IOException::class)
override fun intercept(chain : Interceptor.Chain) : Response {
retryCount = 0
return retry(chain)!!
}
@Throws(IOException::class)
private fun retry(chain : Interceptor.Chain) : Response? {
var response : Response? = null
try {
val request : Request = chain.request()
response = chain.proceed(request)
while (!response!!.isSuccessful && retryCount < maxRetryCount) {
retryCount++
closeResponse(response)
response = retry(chain)
}
} catch (e : SocketTimeoutException ) {
e.printStackTrace()
closeResponse(response)
if (retryCount < maxRetryCount) {
retryCount++
response = retry(chain)
} else if (retryCount == maxRetryCount) { //重连最后一次还是异常,则将异常抛出
throw e
}
} catch (e : UnknownHostException) {
e.printStackTrace()
closeResponse(response)
if (retryCount < maxRetryCount) {
retryCount++
response = retry(chain)
} else if (retryCount == maxRetryCount) {
throw e
}
}
return response
}
private fun closeResponse(response : Response?) {
if (response != null) {
try {
response.close()
} catch (e1 : Exception) {
e1.printStackTrace()
}
}
}
}

Loading…
Cancel
Save