| <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" /> |
| 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' |
| 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" /> |
| 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 | ||||
| //图标与角标文字 | //图标与角标文字 |
| 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 -> |
| */ | */ | ||||
| object AppConstants { | object AppConstants { | ||||
| const val DEVICE_ID = "deviceId" | |||||
| const val LICENSE_ID = "licenceId" | |||||
| /** 项目: 英语 */ | /** 项目: 英语 */ | ||||
| const val SUBJECT_ENGLISH = 3 | const val SUBJECT_ENGLISH = 3 |
| * 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) { | |||||
| } | } |
| 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 //课程学习进度 | ||||
| } | } |
| 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 |
| 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 //是否下载 | |||||
| } |
| * @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 | ||||
| }) | }) | ||||
| } | } |
| 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") | |||||
| } | |||||
| } | } | ||||
| } | } |
| 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) | |||||
| } | |||||
| } | } |
| 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 | ||||
| } | |||||
| }*/ | |||||
| } | } |
| 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 { |
| * 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) | ||||
| } | } | ||||
| } | } | ||||
| } | } |
| 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) |
| } | } | ||||
| 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 | ||||
| } | } |
| 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 | ||||
| } | } |
| 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("输入格式错误") |
| 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 { |
| 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] | |||||
| } | |||||
| } |
| 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) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } |
| 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] | |||||
| } | |||||
| } | } |
| 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) | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| 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 { | |||||
| } |
| 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) | |||||
| } | |||||
| */ | |||||
| } | |||||
| } |
| 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() | |||||
| // } | |||||
| // } | |||||
| } |
| <?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> |
| 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" |
| 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" |
| 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> |
| 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" |
| 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" |
| <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> |
| 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' | |||||
| ] | ] | ||||
| 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 | |||||
| xmlns:tools="http://schemas.android.com/tools" | xmlns:tools="http://schemas.android.com/tools" | ||||
| package="com.suliang.common"> | 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.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"/> | ||||
| /** | /** | ||||
| * 用于获取全局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异常") | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } |
| 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 | |||||
| } | |||||
| } | } |
| 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?) | |||||
| } |
| 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() | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| 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) | |||||
| }*/ | |||||
| } |
| 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?) | |||||
| } |
| package com.suliang.common.util.net | |||||
| /** | |||||
| * author: suliang | |||||
| * 2022/12/6 17:02 | |||||
| * describe : 网络状态 | |||||
| */ | |||||
| enum class NetworkState { | |||||
| NONE, | |||||
| WIFI, | |||||
| CELLULAR, | |||||
| ETHERNET | |||||
| } |
| 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 | |||||
| } | |||||
| } | |||||
| } |
| 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() | |||||
| } |
| 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() | |||||
| } | |||||
| } | |||||
| } | |||||
| } |