@@ -66,6 +66,7 @@ | |||
<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/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_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" /> |
@@ -128,10 +128,13 @@ dependencies { | |||
implementation fileTree(include: ['*.jar', "*.aar"], dir: 'libs') | |||
// implementation 'androidx.legacy:legacy-support-v4:1.0.0' | |||
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 '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-viewmodel-ktx:2.3.1' | |||
// implementation 'androidx.appcompat:appcompat:1.2.0' |
@@ -3,7 +3,8 @@ | |||
xmlns:tools="http://schemas.android.com/tools" | |||
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 | |||
android:name=".module.XKLApplication" | |||
@@ -14,11 +15,14 @@ | |||
android:supportsRtl="true" | |||
android:theme="@style/Theme.XklLocal" | |||
tools:ignore="LockedOrientationActivity"> | |||
<activity | |||
android:name=".module.splash.ActivateActivity" | |||
android:exported="true" /> | |||
<activity | |||
android:name=".module.floating.DictionaryFloatingSearchActivity" | |||
android:theme="@style/DialogActivityTheme" | |||
android:exported="true" | |||
android:launchMode="singleTask" | |||
android:exported="true" /> | |||
android:theme="@style/DialogActivityTheme" /> | |||
<activity | |||
android:name=".module.m_my.CacheClearActivity" | |||
android:exported="true" /> |
@@ -66,7 +66,7 @@ class AdapterCoursePackWithMemo(viewModel : MemoFragmentViewModel) : | |||
getItem(position).let { item -> | |||
(holder.binding as ItemMemoBinding).run { | |||
//图片 | |||
BindingAdapter.imageByteArray(imgCoursePackCover, item.coursePack.cover) | |||
BindingAdapter.imageFilePath(imgCoursePackCover, item.coursePack.cover) | |||
//文字 | |||
tvCoursePackName.text = item.coursePack.coursePackName | |||
//图标与角标文字 |
@@ -217,7 +217,7 @@ class DictionaryAdapter(vm : DictionaryViewModel) : BaseRVAdapterVM<DictionaryIt | |||
tvValue.text = it.basic_explaination | |||
tvWord.text = it.word | |||
ivHoruns.click { v -> //发音 | |||
AudioCache.getDictionaryVoice(it.id,UserInfoManager.getDefaultSoundWay()) | |||
AudioCache.getDictionaryVoice(it.id,UserInfoManager.instance.getDefaultSoundWay()) | |||
} | |||
//点击,进入详情 | |||
root.click { v -> |
@@ -7,6 +7,8 @@ package com.xkl.cdl.data | |||
*/ | |||
object AppConstants { | |||
const val DEVICE_ID = "deviceId" | |||
const val LICENSE_ID = "licenceId" | |||
/** 项目: 英语 */ | |||
const val SUBJECT_ENGLISH = 3 |
@@ -7,5 +7,5 @@ import com.xkl.cdl.data.bean.course.Course | |||
* create 2022/6/27 14:42 | |||
* Describe: 统计课程item | |||
*/ | |||
class StatisticsCourse(val course:Course,val cover: ByteArray) { | |||
class StatisticsCourse(val course:Course,val cover: String) { | |||
} |
@@ -1,5 +1,7 @@ | |||
package com.xkl.cdl.data.bean.course | |||
import java.io.Serializable | |||
/** | |||
* author suliang | |||
* create 2022/3/22 10:08 | |||
@@ -24,7 +26,7 @@ data class Course( | |||
val courseType: Int, | |||
val totalWords: Int, | |||
val dbPathName: String | |||
) { | |||
) : Serializable { | |||
var courseLearnProgress : Double = 0.0 //课程学习进度 | |||
} |
@@ -3,6 +3,7 @@ package com.xkl.cdl.data.bean.course | |||
import androidx.databinding.BaseObservable | |||
import androidx.databinding.Bindable | |||
import com.xkl.cdl.BR | |||
import java.io.Serializable | |||
/** | |||
* author suliang | |||
@@ -20,11 +21,16 @@ import com.xkl.cdl.BR | |||
data class CoursePack( | |||
val coursePackId: Long, | |||
val coursePackName: String, | |||
val cover: ByteArray, | |||
val summary: String, | |||
val subjectId: Int, | |||
val coursePackType: Int, | |||
) : BaseObservable() { | |||
) : BaseObservable(),Serializable { | |||
var cover: String = "" //课程包图片文件地址 | |||
var downLoadZipUrl : String = "" //压缩包下载地址 | |||
var isDown = false //是否下载 | |||
//在CourseManger中的subjectWithCoursePackMap对应subject下的list中所在的位置 | |||
var inCoursePackPosition : Int = 0 | |||
@@ -52,7 +58,7 @@ data class CoursePack( | |||
if (coursePackId != other.coursePackId) 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 (subjectId != other.subjectId) return false | |||
if (coursePackType != other.coursePackType) return false | |||
@@ -66,7 +72,7 @@ data class CoursePack( | |||
override fun hashCode() : Int { | |||
var result = coursePackId.hashCode() | |||
result = 31 * result + coursePackName.hashCode() | |||
result = 31 * result + cover.contentHashCode() | |||
result = 31 * result + cover.hashCode() | |||
result = 31 * result + summary.hashCode() | |||
result = 31 * result + subjectId | |||
result = 31 * result + coursePackType |
@@ -0,0 +1,31 @@ | |||
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 //是否下载 | |||
} |
@@ -26,19 +26,26 @@ object BindingAdapter { | |||
* @param imageByteArray ByteArray 字节数组 | |||
*/ | |||
//https://stackoverflow.com/questions/60264081/in-android-how-databinding-with-byte-array | |||
@BindingAdapter("imageByteArray") | |||
@BindingAdapter("imgFilePath") | |||
@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) | |||
// view.setImageBitmap(BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.size)) | |||
} | |||
@BindingAdapter(value = ["imgBytes","blur"]) | |||
@BindingAdapter(value = ["imgFilePathBlur","blur"]) | |||
@JvmStatic | |||
fun imageByteArray(view:ImageView,imgByteArray:ByteArray,blur:Boolean){ | |||
fun imageByteArray(view:ImageView,imgFilePath:String,blur:Boolean){ | |||
ImageLoader.loadImage(ImageLoaderOption().apply { | |||
targetView = view | |||
byteArray = imgByteArray | |||
url = imgFilePath | |||
blurEanble = blur | |||
}) | |||
} |
@@ -30,6 +30,14 @@ class FilePathManager { | |||
return File(getDbRootPath(), "dictionary.db") | |||
} | |||
/* 获取课程包图片所在的目录 | |||
* @return File | |||
*/ | |||
@JvmStatic | |||
fun getIconRootPath(): String{ | |||
return FileUtil.getSaveDirPath("icon") | |||
} | |||
/** | |||
* 获取课程包目录数据所在的地址 | |||
* @return File | |||
@@ -94,6 +102,11 @@ class FilePathManager { | |||
return File(getVoiceParent(),"voc") | |||
} | |||
//zip下载包 | |||
fun getZipRootPath() : File { | |||
return FileUtil.getSaveDirFile("zip") | |||
} | |||
} | |||
} |
@@ -4,6 +4,7 @@ import com.suliang.common.util.SpUtils | |||
import com.suliang.common.util.file.FileUtil | |||
import com.suliang.common.util.thread.AppExecutors | |||
import com.xkl.cdl.data.AppConstants | |||
import com.xkl.cdl.data.bean.course.CoursePack | |||
import java.io.File | |||
/** | |||
@@ -11,8 +12,15 @@ import java.io.File | |||
* create 2022/4/8 17:47 | |||
* Describe: 用户数据管理 | |||
*/ | |||
object UserInfoManager { | |||
class UserInfoManager private constructor(){ | |||
companion object{ | |||
val instance : UserInfoManager by lazy(LazyThreadSafetyMode.SYNCHRONIZED){ | |||
UserInfoManager() | |||
} | |||
} | |||
/** 获取默认发音方式 */ | |||
fun getDefaultSoundWay() : Int{ | |||
return SpUtils.instance.decode("defaultSoundWay",Int::class.java,AppConstants.SOUND_TYPE_UK) | |||
@@ -32,7 +40,7 @@ object UserInfoManager { | |||
} | |||
/** 获取用户头像 */ | |||
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()){ | |||
return null | |||
}else | |||
@@ -50,5 +58,36 @@ object UserInfoManager { | |||
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) | |||
} | |||
} |
@@ -28,11 +28,11 @@ class DbCoursePackManager { | |||
database = SQLiteDatabase.openDatabase(coursePackFile.path, "XUEKAOLE_COURSE_LIST_KEY", null, OPEN_READONLY) | |||
} | |||
/*** | |||
/* *//*** | |||
* 查询用户绑定的课程包 | |||
* @param coursePackIds String 课程包id集合如: 1,2,3,4,5 最后不能有逗号 | |||
* @return Boolean 查询是否成功 | |||
*/ | |||
*//* | |||
fun queryBindingCoursePack(coursePackIds: String): Boolean { | |||
//英语课程包 | |||
val englishCoursePack = mutableListOf<CoursePack>() | |||
@@ -97,5 +97,5 @@ class DbCoursePackManager { | |||
CourseManager.subjectWithCoursePackMap[AppConstants.SUBJECT_CHINESE] = chineseCoursePack.toList() | |||
database.close() | |||
return true | |||
} | |||
}*/ | |||
} |
@@ -53,6 +53,9 @@ class CommonDialog private constructor() : BaseDialogFragment<DialogCommonBindin | |||
titleText?.let { | |||
binding.tvTitle.setText(it) | |||
binding.tvTitle.visibility = View.VISIBLE | |||
} ?: titleTextValue?.let { | |||
binding.tvTitle.text = it | |||
binding.tvTitle.visibility = View.VISIBLE | |||
} | |||
contentColor?.let { | |||
binding.tvContent.setTextColor(ContextCompat.getColor(requireContext(),it)) | |||
@@ -60,6 +63,9 @@ class CommonDialog private constructor() : BaseDialogFragment<DialogCommonBindin | |||
contentText?.let { | |||
binding.tvContent.setText(it) | |||
binding.tvContent.visibility = View.VISIBLE | |||
} ?: contentTextValue?.let { | |||
binding.tvContent.text = it | |||
binding.tvContent.visibility = View.VISIBLE | |||
} | |||
leftColor?.let { | |||
binding.tvLeft.setTextColor(ContextCompat.getColor(requireContext(),it)) | |||
@@ -69,6 +75,11 @@ class CommonDialog private constructor() : BaseDialogFragment<DialogCommonBindin | |||
binding.tvLeft.click { | |||
onCommonDialogButtonClickListener(this@CommonDialog,false) | |||
} | |||
}?:leftTextValue?.let { | |||
binding.tvLeft.text = it | |||
binding.tvLeft.click { | |||
onCommonDialogButtonClickListener(this@CommonDialog,false) | |||
} | |||
}?:let { | |||
binding.tvLeft.visibility = View.GONE | |||
binding.vSplit.visibility = View.GONE | |||
@@ -81,6 +92,11 @@ class CommonDialog private constructor() : BaseDialogFragment<DialogCommonBindin | |||
binding.tvRight.click { | |||
onCommonDialogButtonClickListener(this@CommonDialog,true) | |||
} | |||
}?:rightTextValue?.let { | |||
binding.tvRight.text = it | |||
binding.tvRight.click { | |||
onCommonDialogButtonClickListener(this@CommonDialog,true) | |||
} | |||
} | |||
imgFlag?.let { |
@@ -11,37 +11,43 @@ import androidx.annotation.StringRes | |||
* Describe: 通用弹窗设置实体 | |||
*/ | |||
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) { | |||
} | |||
override fun writeToParcel(parcel : Parcel, flags : Int) { | |||
parcel.writeValue(titleText) | |||
parcel.writeString(titleTextValue) | |||
parcel.writeValue(contentText) | |||
parcel.writeString(contentTextValue) | |||
parcel.writeValue(leftText) | |||
parcel.writeString(leftTextValue) | |||
parcel.writeValue(rightText) | |||
parcel.writeString(rightTextValue) | |||
parcel.writeValue(imgFlag) | |||
parcel.writeValue(titleColor) | |||
parcel.writeValue(contentColor) | |||
parcel.writeValue(leftColor) | |||
parcel.writeValue(rightColor) | |||
} | |||
override fun describeContents() : Int { | |||
@@ -57,4 +63,5 @@ data class CommonDialogBean(@StringRes val titleText : Int? = null, | |||
return arrayOfNulls(size) | |||
} | |||
} | |||
} |
@@ -10,6 +10,7 @@ import com.suliang.common.util.file.FileUtil | |||
import com.tencent.mmkv.MMKV | |||
import com.xkl.cdl.module.floating.DictionaryFloatingWindowManager | |||
import com.xkl.cdl.module.main.MainActivity | |||
import com.xkl.cdl.util.IdentificationUtils | |||
import io.reactivex.rxjava3.exceptions.UndeliverableException | |||
import io.reactivex.rxjava3.functions.Consumer | |||
import io.reactivex.rxjava3.plugins.RxJavaPlugins | |||
@@ -40,8 +41,9 @@ class XKLApplication : LibApplication() { | |||
override fun onCreate() { | |||
super.onCreate() | |||
// instance = this | |||
SQLiteDatabase.loadLibs(this) | |||
// LogUtil.e(UUID.randomUUID().toString().replace("-","")) | |||
//初始MMKV存储 | |||
// val rootDir = MMKV.initialize(this) |
@@ -205,10 +205,10 @@ class LearnExamActivity : BaseActivityVM<ActivityLearnExamBinding, LearnExamView | |||
} | |||
else -> { | |||
//默认发音 | |||
voiceSwitch.setSoundWay(UserInfoManager.getDefaultSoundWay()) | |||
voiceSwitch.setSoundWay(UserInfoManager.instance.getDefaultSoundWay()) | |||
voiceSwitch.soundWayChange.observe(this@LearnExamActivity) { | |||
vm.defaultSoundWay = it | |||
UserInfoManager.putDefaultSoundWay(it) | |||
UserInfoManager.instance.putDefaultSoundWay(it) | |||
if (this@LearnExamActivity::spellAdapter.isInitialized) { | |||
spellAdapter.defaultSoundWay = it | |||
} |
@@ -126,10 +126,10 @@ class LearnWordActivity : BaseActivityVM<ActivityLearnWordBinding, LearnWordView | |||
vm.defaultSoundWay = AppConstants.SOUND_TYPE_CN | |||
} | |||
else -> { | |||
voiceSwitch.setSoundWay(UserInfoManager.getDefaultSoundWay()) | |||
voiceSwitch.setSoundWay(UserInfoManager.instance.getDefaultSoundWay()) | |||
voiceSwitch.soundWayChange.observe(this@LearnWordActivity) { | |||
vm.defaultSoundWay = it | |||
UserInfoManager.putDefaultSoundWay(it) | |||
UserInfoManager.instance.putDefaultSoundWay(it) | |||
if (this@LearnWordActivity::spellAdapter.isInitialized) { | |||
spellAdapter.defaultSoundWay = it | |||
} |
@@ -49,7 +49,7 @@ class MyFragment : BaseFragmentVM<FragmentMyBinding, MyViewModel>() { | |||
override fun initFragment() { | |||
//加载头像 | |||
UserInfoManager.getUserHeadPortrait()?.let { | |||
UserInfoManager.instance.getUserHeadPortrait()?.let { | |||
ImageLoader.loadImage(ImageLoaderOption().apply { | |||
targetView = binding.headPortrait | |||
placeholderResId = R.mipmap.img_default | |||
@@ -67,7 +67,7 @@ class MyFragment : BaseFragmentVM<FragmentMyBinding, MyViewModel>() { | |||
override fun onResult(photos : ArrayList<Photo>?, isOriginal : Boolean) { | |||
photos?.let { | |||
if (it.size > 0) { | |||
UserInfoManager.putUserHeadPortrait(File(it[0].path)) | |||
UserInfoManager.instance.putUserHeadPortrait(File(it[0].path)) | |||
ImageLoader.loadImage(binding.headPortrait, it[0].uri) | |||
} | |||
} | |||
@@ -80,7 +80,7 @@ class MyFragment : BaseFragmentVM<FragmentMyBinding, MyViewModel>() { | |||
} | |||
//昵称 | |||
UserInfoManager.getNickname().let { | |||
UserInfoManager.instance.getNickname().let { | |||
if (it.isEmpty()) binding.tvNickname.hint = "点击可设置昵称" | |||
else binding.tvNickname.text = it | |||
} | |||
@@ -94,7 +94,7 @@ class MyFragment : BaseFragmentVM<FragmentMyBinding, MyViewModel>() { | |||
.asInputConfirm("修改昵称", null, binding.tvNickname.text.toString(), "限制为1-12个字符") { | |||
//修改确认 | |||
if (it.isNotEmpty() && it.length <= 12) { | |||
UserInfoManager.putNickname(it) | |||
UserInfoManager.instance.putNickname(it) | |||
binding.tvNickname.text = it | |||
} else { | |||
showToast("输入格式错误") |
@@ -15,14 +15,14 @@ class SettingActivity : BaseActivity<ActivitySettingBinding>() { | |||
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) | |||
binding.tvClearCache.setCompoundDrawablesWithIntrinsicBounds(null,null,arrowRightDrawable,null) | |||
binding.voiceSwitch.soundWayChange.observe(this){ | |||
UserInfoManager.putDefaultSoundWay(it) | |||
UserInfoManager.instance.putDefaultSoundWay(it) | |||
} | |||
binding.tvClearCache.click { |
@@ -0,0 +1,54 @@ | |||
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] | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
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) | |||
} | |||
}) | |||
} | |||
} |
@@ -2,28 +2,28 @@ package com.xkl.cdl.module.splash | |||
import android.annotation.SuppressLint | |||
import android.os.Bundle | |||
import androidx.lifecycle.ViewModelProvider | |||
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.file.FileUtil | |||
import com.suliang.common.util.thread.AppExecutors | |||
import com.xkl.cdl.data.AppConstants | |||
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.data.manager.db.DbCoursePackManager | |||
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.floating.DictionaryFloatingWindowManager | |||
import com.xkl.cdl.module.main.MainActivity | |||
import io.reactivex.rxjava3.core.Observable | |||
import java.io.File | |||
import java.util.* | |||
import java.util.concurrent.TimeUnit | |||
import kotlin.system.exitProcess | |||
@SuppressLint("CustomSplashScreen") | |||
class SplashActivity : BaseActivity<ActivitySplashBinding>() { | |||
class SplashActivity : BaseActivityVM<ActivitySplashBinding,SplashViewModel>() { | |||
override fun onCreateOwn(savedInstanceState : Bundle?) { | |||
if (!isTaskRoot) { | |||
@@ -38,49 +38,130 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() { | |||
} | |||
// TODO: 激活返回跳转处理 | |||
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] | |||
} | |||
} |
@@ -0,0 +1,278 @@ | |||
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) | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
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 { | |||
} |
@@ -0,0 +1,102 @@ | |||
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) | |||
} | |||
*/ | |||
} | |||
} |
@@ -0,0 +1,207 @@ | |||
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() | |||
// } | |||
// } | |||
} |
@@ -0,0 +1,60 @@ | |||
<?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> |
@@ -24,7 +24,7 @@ | |||
android:layout_width="0dp" | |||
android:layout_height="0dp" | |||
android:scaleType="fitXY" | |||
bind:imgBytes="@{coursePack.cover}" | |||
bind:imgFilePathBlur="@{coursePack.cover}" | |||
bind:blur= "@{true}" | |||
app:layout_constraintBottom_toBottomOf="@+id/view_placeholder_1" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
@@ -67,7 +67,7 @@ | |||
android:layout_height="100dp" | |||
android:layout_marginStart="@dimen/global_spacing" | |||
android:scaleType="fitXY" | |||
app:imageByteArray="@{coursePack.cover}" | |||
app:imgFilePath="@{coursePack.cover}" | |||
app:layout_constraintBottom_toTopOf="@+id/view_placeholder_1" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toBottomOf="@id/title_bar" |
@@ -47,7 +47,7 @@ | |||
android:layout_width="52dp" | |||
android:layout_height="0dp" | |||
android:scaleType="fitXY" | |||
app:imageByteArray="@{course.cover}" | |||
app:imgFilePath="@{course.cover}" | |||
app:layout_constraintDimensionRatio="52:72" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toTopOf="parent" |
@@ -24,5 +24,18 @@ | |||
app:layout_constraintTop_toTopOf="parent" | |||
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> | |||
</layout> |
@@ -25,7 +25,7 @@ | |||
android:scaleType="fitXY" | |||
tools:src="@color/main_text_color" | |||
app:shapeAppearance="@style/roundedCornerStyle" | |||
app:imageByteArray="@{course.cover}"/> | |||
app:imgFilePath="@{course.cover}"/> | |||
<androidx.constraintlayout.utils.widget.ImageFilterView | |||
android:id="@+id/iv_arrow" |
@@ -28,7 +28,7 @@ | |||
android:layout_width="75dp" | |||
android:layout_height="match_parent" | |||
android:scaleType="fitXY" | |||
app:imageByteArray="@{coursePack.cover}" | |||
app:imgFilePath = "@{coursePack.cover}" | |||
app:layout_constraintBottom_toBottomOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toTopOf="parent" |
@@ -112,5 +112,8 @@ | |||
<string name="floating_dictionary_tips">词典使用提示</string> | |||
<string name="floating_dictionary_content">为了在学习中可以使用词典功能,需要手动打开并同意悬浮窗权限!</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> |
@@ -123,6 +123,13 @@ ext { | |||
Paging: "androidx.paging:paging-runtime:3.0.0-alpha08", | |||
//room_paging | |||
// 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' | |||
] | |||
@@ -71,6 +71,14 @@ dependencies { | |||
api customDependencies.MMKV | |||
// 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 | |||
@@ -3,10 +3,10 @@ | |||
xmlns:tools="http://schemas.android.com/tools" | |||
package="com.suliang.common"> | |||
<!-- <!–网络权限–>--> | |||
<!-- <uses-permission android:name="android.permission.INTERNET"/>--> | |||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>--> | |||
<!-- <!–本地外置存储权限–>--> | |||
<!--网络权限--> | |||
<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.WRITE_EXTERNAL_STORAGE"--> | |||
<!-- tools:ignore="ScopedStorage" />--> | |||
@@ -16,6 +16,7 @@ | |||
android:networkSecurityConfig="@xml/network_security_config" | |||
android:usesCleartextTraffic="true" | |||
--> | |||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |||
<application | |||
android:networkSecurityConfig="@xml/network_security_config"/> | |||
@@ -6,16 +6,18 @@ import java.lang.Exception | |||
/** | |||
* 用于获取全局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异常") | |||
} | |||
} | |||
} | |||
} |
@@ -1,7 +1,9 @@ | |||
package com.suliang.common.util | |||
import android.os.Parcelable | |||
import android.util.Base64 | |||
import com.tencent.mmkv.MMKV | |||
import java.io.* | |||
/** | |||
* author suliang | |||
@@ -10,7 +12,6 @@ import com.tencent.mmkv.MMKV | |||
*/ | |||
class SpUtils { | |||
companion object{ | |||
val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { | |||
SpUtils() | |||
@@ -45,7 +46,7 @@ class SpUtils { | |||
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) | |||
Long::class.java -> kv.decodeLong(key,defaultValue as Long) | |||
Float::class.java -> kv.decodeFloat(key, defaultValue as Float) | |||
@@ -75,4 +76,82 @@ class SpUtils { | |||
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 | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
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?) | |||
} |
@@ -0,0 +1,193 @@ | |||
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() | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
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) | |||
}*/ | |||
} |
@@ -0,0 +1,71 @@ | |||
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?) | |||
} |
@@ -0,0 +1,13 @@ | |||
package com.suliang.common.util.net | |||
/** | |||
* author: suliang | |||
* 2022/12/6 17:02 | |||
* describe : 网络状态 | |||
*/ | |||
enum class NetworkState { | |||
NONE, | |||
WIFI, | |||
CELLULAR, | |||
ETHERNET | |||
} |
@@ -0,0 +1,72 @@ | |||
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 | |||
} | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
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() | |||
} |
@@ -0,0 +1,69 @@ | |||
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() | |||
} | |||
} | |||
} | |||
} |