AppVersionUtil.kt 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. package com.develop.common.utils
  2. import android.annotation.SuppressLint
  3. import android.app.Application
  4. import android.content.Context
  5. import android.net.Uri
  6. import android.os.PowerManager
  7. import android.provider.Settings
  8. import android.util.Log
  9. import com.azhon.appupdate.listener.OnDownloadListener
  10. import com.azhon.appupdate.util.LogUtil
  11. import com.blankj.utilcode.util.FileUtils
  12. import com.blankj.utilcode.util.ToastUtils
  13. import com.blankj.utilcode.util.ZipUtils
  14. import com.develop.base.ext.getNewTuya
  15. import com.develop.base.ext.getOtaFileMd5
  16. import com.develop.base.ext.getSN
  17. import com.develop.base.ext.getUpdateRecipeTime
  18. import com.develop.base.ext.globalApp
  19. import com.develop.base.ext.setOtaFileMd5
  20. import com.develop.base.ext.setRecipesList
  21. import com.develop.base.util.AppActivityManager
  22. import com.develop.base.util.FileKit
  23. import com.develop.base.util.GlobalToast
  24. import com.develop.base.util.MMkvUtils
  25. import com.develop.base.util.TopResumedAtyHolder
  26. import com.develop.common.R
  27. import com.develop.common.data_repo.FoodDataProvider
  28. import com.develop.common.data_repo.db.entity.DevRecipeCategory
  29. import com.develop.common.data_repo.db.entity.DevVersion
  30. import com.develop.common.data_repo.net.Api
  31. import com.develop.common.data_repo.net.converter.SerializationConverter.Companion.jsonDecoder
  32. import com.develop.common.data_repo.net.model.request.DeviceInfoBody
  33. import com.develop.common.data_repo.net.model.response.DevInfoResult
  34. import com.develop.common.data_repo.net.model.response.RecipeDataConfig
  35. import com.develop.common.dialog.CancelConfirmDialog
  36. import com.develop.common.dialog.CommonDialog
  37. import com.develop.common.dialog.RecipeUpdateDialog
  38. import com.develop.common.tag.CURRENT_USER_ID
  39. import com.develop.common.tag.SCREENSAVER
  40. import com.drake.net.Get
  41. import com.drake.net.Post
  42. import com.drake.net.component.Progress
  43. import com.drake.net.interfaces.ProgressListener
  44. import com.drake.net.utils.scopeNetLife
  45. import kotlinx.serialization.decodeFromString
  46. import org.greenrobot.eventbus.EventBus
  47. import java.io.File
  48. import java.util.*
  49. object AppVersionUtil {
  50. var numberList = mutableListOf<String>()
  51. var dialogRecipeUpdate = RecipeUpdateDialog()
  52. var sTime: Long = 0
  53. fun startRecord(){
  54. sTime = System.currentTimeMillis()
  55. Log.e("TAG 启动时间"," ------ startRecord :$sTime")
  56. }
  57. fun endRecord(msg :String){
  58. val cost = System.currentTimeMillis() - sTime
  59. Log.e("TAG 启动时间","msg : $msg ------ cost :$cost")
  60. }
  61. fun checkRecipeUpdate(shoNoUpdateDialog: Boolean = false) {
  62. if (getNewTuya()){
  63. //由于涂鸦云食谱,估不检测平台食谱
  64. return
  65. }
  66. TopResumedAtyHolder.getCurrentActivity()?.apply {
  67. scopeNetLife {
  68. try {
  69. val result = Post<DevInfoResult>(Api.DEV_INFO) {
  70. body = DeviceInfoBody.genDeviceInfoBody()
  71. }.await()
  72. if (result.apkUpdate) {
  73. val commonDialog = CommonDialog()
  74. commonDialog.msg = getString(com.develop.common.R.string.update_msg)
  75. commonDialog.title = getString(com.develop.common.R.string.update)
  76. commonDialog.hasOKBtn = false
  77. val cancelConfirmDialog = CancelConfirmDialog()
  78. cancelConfirmDialog.title =
  79. getString(com.develop.common.R.string.update_title)
  80. cancelConfirmDialog.showDialog(
  81. supportFragmentManager,
  82. "cancelConfirmDialog"
  83. )
  84. cancelConfirmDialog.onDialogClickListener =
  85. object : CancelConfirmDialog.OnDialogClickListener {
  86. override fun onConfirm() {
  87. commonDialog.showDialog(supportFragmentManager, "commonDialog")
  88. UpdateUtil.updateApp(
  89. this@apply,
  90. result.apkUrl,
  91. object : OnDownloadListener {
  92. override fun cancel() {
  93. runOnUiThread {
  94. commonDialog.removeSelf()
  95. }
  96. }
  97. override fun done(apk: File) {
  98. GlobalToast.showToast(getString(R.string.finish_download))
  99. UpdateUtil.installPackage(this@apply, apk)
  100. commonDialog.updateProgress(getString(R.string.installing))
  101. }
  102. override fun downloading(max: Int, progress: Int) {
  103. runOnUiThread {
  104. commonDialog.updateProgress(
  105. "${
  106. String.format(
  107. "%.0f",
  108. ((progress.toFloat() / max.toFloat()) * 100f)
  109. )
  110. }%"
  111. )
  112. }
  113. }
  114. override fun error(e: Throwable) {
  115. GlobalToast.showToast(getString(R.string.download_fail))
  116. runOnUiThread {
  117. commonDialog.removeSelf()
  118. }
  119. }
  120. override fun start() {
  121. // GlobalToast.showToast(getString(com.develop.common.R.string.start_download))
  122. }
  123. })
  124. }
  125. override fun onCancel() {
  126. }
  127. override fun onKey() {
  128. }
  129. }
  130. } else {
  131. val downloadDir = this@apply.externalCacheDir.toString()
  132. val downloadName = System.nanoTime().toString()
  133. val recipeUpdateTime = result.recipeUpdateTime
  134. val newRecipes = LinkedList(result.newRecipes)
  135. if (newRecipes.isEmpty() && shoNoUpdateDialog) {
  136. val dialog = RecipeUpdateDialog()
  137. dialog.onDialogClickListener =
  138. object : RecipeUpdateDialog.OnDialogClickListener {
  139. override fun onConfirm() {
  140. }
  141. override fun onCancel() {
  142. }
  143. }
  144. dialog.showNoUpdateTips(supportFragmentManager, "RECIPE_UPDATE_DIALOG")
  145. }
  146. if (newRecipes.isNotEmpty()) {
  147. if (dialogRecipeUpdate.isShow) {
  148. dialogRecipeUpdate.removeSelf()
  149. }
  150. dialogRecipeUpdate.onDialogClickListener =
  151. object : RecipeUpdateDialog.OnDialogClickListener {
  152. override fun onConfirm() {
  153. updateRecipe = !updateRecipe
  154. //清掉list的number
  155. numberList.clear()
  156. EventBus.getDefault().post(NoScreenEvent(true))
  157. downloadRecipes(
  158. newRecipes,
  159. dialogRecipeUpdate,
  160. newRecipes.size.toLong(),
  161. downloadDir,
  162. downloadName,
  163. recipeUpdateTime
  164. )
  165. }
  166. override fun onCancel() {
  167. }
  168. }
  169. var activityName =
  170. AppActivityManager.getInstance().topActivity.localClassName
  171. /**
  172. * 由于访问接口得时候处理数据,这时候切换了其他页面再切换语言导致,
  173. * 更新食谱的dialog的问题会空白,估做下面判断
  174. * */
  175. if (activityName.contains("ModeEntranceActivity") || activityName.contains(
  176. "ModeEntrance2Activity"
  177. ) || activityName.contains("AboutActivity")
  178. ) {
  179. dialogRecipeUpdate.showUpdateTips(
  180. supportFragmentManager,
  181. "RECIPE_UPDATE_DIALOG",
  182. newRecipes.size.toLong()
  183. )
  184. }
  185. }
  186. }
  187. } catch (e: java.lang.Exception) {
  188. e.printStackTrace()
  189. }
  190. }
  191. }
  192. }
  193. fun checkAboutRecipeUpdate(shoNoUpdateDialog: Boolean = false) {
  194. if (getNewTuya()){
  195. //由于涂鸦云食谱,估不检测平台食谱
  196. return
  197. }
  198. TopResumedAtyHolder.getCurrentActivity()?.apply {
  199. scopeNetLife {
  200. try {
  201. val result = Post<DevInfoResult>(Api.DEV_INFO) {
  202. body = DeviceInfoBody.genDeviceInfoBody()
  203. }.await()
  204. val downloadDir = this@apply.externalCacheDir.toString()
  205. val downloadName = System.nanoTime().toString()
  206. val recipeUpdateTime = result.recipeUpdateTime
  207. val newRecipes = LinkedList(result.newRecipes)
  208. if (newRecipes.isEmpty() && shoNoUpdateDialog) {
  209. val dialog = RecipeUpdateDialog()
  210. dialog.onDialogClickListener =
  211. object : RecipeUpdateDialog.OnDialogClickListener {
  212. override fun onConfirm() {
  213. }
  214. override fun onCancel() {
  215. }
  216. }
  217. dialog.showNoUpdateTips(supportFragmentManager, "RECIPE_UPDATE_DIALOG")
  218. }
  219. if (newRecipes.isNotEmpty()) {
  220. if (dialogRecipeUpdate.isShow) {
  221. dialogRecipeUpdate.removeSelf()
  222. }
  223. dialogRecipeUpdate.onDialogClickListener =
  224. object : RecipeUpdateDialog.OnDialogClickListener {
  225. override fun onConfirm() {
  226. updateRecipe = !updateRecipe
  227. //清掉list的number
  228. numberList.clear()
  229. EventBus.getDefault().post(NoScreenEvent(true))
  230. downloadRecipes(
  231. newRecipes,
  232. dialogRecipeUpdate,
  233. newRecipes.size.toLong(),
  234. downloadDir,
  235. downloadName,
  236. recipeUpdateTime
  237. )
  238. }
  239. override fun onCancel() {
  240. }
  241. }
  242. var activityName =
  243. AppActivityManager.getInstance().topActivity.localClassName
  244. /**
  245. * 由于访问接口得时候处理数据,这时候切换了其他页面再切换语言导致,
  246. * 更新食谱的dialog的问题会空白,估做下面判断
  247. * */
  248. if (activityName.contains("ModeEntranceActivity") || activityName.contains("ModeEntrance2Activity") || activityName.contains(
  249. "AboutActivity"
  250. )
  251. ) {
  252. dialogRecipeUpdate.showUpdateTips(
  253. supportFragmentManager,
  254. "RECIPE_UPDATE_DIALOG",
  255. newRecipes.size.toLong()
  256. )
  257. }
  258. }
  259. } catch (e: java.lang.Exception) {
  260. e.printStackTrace()
  261. }
  262. }
  263. }
  264. }
  265. var updateRecipe = false
  266. private fun saveRecipeUpdateTime(recipeUpdateTime: Long?) {
  267. val v =
  268. FoodDataProvider.getUserDatabase().devConfigDao().recipeVersion() ?: DevVersion(0, 0)
  269. v.recipeUpdateTime = recipeUpdateTime
  270. FoodDataProvider.getUserDatabase().devConfigDao().saveDevVersion(v)
  271. //存储下载成功食谱的编号
  272. var strList = GsonUtils.GsonString(numberList)
  273. setRecipesList(strList)
  274. }
  275. fun getRecipe(context: Context){
  276. Thread {
  277. var isUpdateFile = StringUtils.doesUpdateTxtFileExist()
  278. //之前根据zip包的md5 更新,现在做一个文本获取进行更新, 先判断是否存在,存在再处理,不存在就走之前的方法
  279. Log.d("TAG update", "isUpdateFile 文件: $isUpdateFile")
  280. if (isUpdateFile) {
  281. Log.d("TAG update", "isUpdateFile 文件存在 ")
  282. var fileUpdate = StringUtils.getUpdateTime()
  283. var roomUpdate = getUpdateRecipeTime()
  284. Log.d("TAG update", "fileUpdate : $fileUpdate")
  285. Log.d("TAG update", "roomUpdate : $roomUpdate")
  286. FoodDataProvider.setUpdateTime(fileUpdate)
  287. //当本地时间为空,或者file的文件时间比本地时间大的时候,证明食谱包更新了
  288. if (roomUpdate == "" || fileUpdate.toLong() > roomUpdate.toLong()) {
  289. Log.d("TAG update", "isUpdateFile1111111-------- 文件存在 ")
  290. //删除room数据库
  291. deleteRoomDb(context)
  292. //删除sd卡的东西
  293. FoodDataProvider.deleteAll()
  294. FoodDataProvider.prepareData(globalApp())
  295. } else {
  296. FoodDataProvider.prepareData(globalApp())
  297. }
  298. } else {
  299. val md5 = getOtaFileMd5()
  300. val sn = getSN()
  301. Log.d("TAG md5", "time :" + System.currentTimeMillis())
  302. //大概50秒才获取到
  303. // val zipMd2 = StringUtils.getFileMD5("system/media/cofa_cooking.zip")
  304. Log.d("TAG md5", "md5 :$md5")
  305. var zipMd5 = ""
  306. if (md5.isNotEmpty()) {
  307. zipMd5 = FileUtils.getFileMD5ToString("system/media/cofa_cooking.zip")
  308. }
  309. Log.d("TAG md5", "time2222 :" + System.currentTimeMillis())
  310. Log.d("TAG md5", "string :$zipMd5")
  311. //处理ota食谱更新问题
  312. if (sn.startsWith("010") && (md5.isEmpty() || md5 != zipMd5)) {
  313. /**
  314. * 如果MD5的值不存在,重新解压
  315. * 如果MD5的值不一样的,就代表食谱包已经重新更新
  316. * 需要先删除sd卡目录下的cofa文件,再重新解压
  317. * */
  318. deleteRoomDb(context)
  319. //删除sd卡的东西
  320. FoodDataProvider.deleteAll()
  321. FoodDataProvider.prepareData(globalApp())
  322. } else {
  323. FoodDataProvider.prepareData(globalApp())
  324. }
  325. //010 每一次都把md5的值set进去
  326. if (sn.startsWith("010")) {
  327. //优化启动程序
  328. if (zipMd5.isEmpty()) {
  329. zipMd5 = FileUtils.getFileMD5ToString("system/media/cofa_cooking.zip")
  330. }
  331. setOtaFileMd5(zipMd5)
  332. }
  333. }
  334. }.start()
  335. }
  336. fun deleteRoomDb(context: Context) {
  337. FoodDataProvider.getUserDatabase().userInfoDao().apply {
  338. removeAllOnlineRecipe(CURRENT_USER_ID)
  339. removeAllFavouriteRecipe(CURRENT_USER_ID)
  340. removeAllHistoryRecipe(CURRENT_USER_ID)
  341. removeAllOnUserTag(CURRENT_USER_ID)
  342. }
  343. FoodDataProvider.getUserDatabase().devConfigDao().apply {
  344. removeAllDevVersion()
  345. }
  346. Log.d("TAG 删除room", "删除 FoodDataProvider Dao文件")
  347. val applicationDirectory = context.cacheDir.parent?.let { File(it) }
  348. if (applicationDirectory?.exists() == true) {
  349. val files = applicationDirectory.listFiles() ?: emptyArray()
  350. for (file in files) {
  351. //databases目录放着room 的db文件,重新解压需要删除
  352. if (file.name.equals("databases")) {
  353. Log.d("TAG 删除room", "删除 databases的room-db文件")
  354. FileUtils.delete(file)
  355. }
  356. }
  357. }
  358. }
  359. @SuppressLint("InvalidWakeLockTag")
  360. private fun downloadRecipes(
  361. newRecipes: LinkedList<String>,
  362. recipeUpdateDialog: RecipeUpdateDialog,
  363. sum: Long,
  364. downloadDir: String,
  365. downloadName: String,
  366. recipeUpdateTime: Long?
  367. ) {
  368. //取消标识退出
  369. if (!updateRecipe) {
  370. return
  371. }
  372. //升级休眠
  373. val pm = globalApp().getSystemService(Application.POWER_SERVICE) as PowerManager
  374. val mWakeLock = pm.newWakeLock(
  375. PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ON_AFTER_RELEASE or PowerManager.ACQUIRE_CAUSES_WAKEUP,
  376. "tag"
  377. );
  378. mWakeLock?.setReferenceCounted(false);
  379. mWakeLock?.acquire(10 * 60 * 1000L /*10 minutes*/)
  380. TopResumedAtyHolder.getCurrentActivity()?.apply {
  381. if (newRecipes.isEmpty()) {
  382. saveRecipeUpdateTime(recipeUpdateTime)
  383. scopeNetLife {
  384. //判断是否还有新菜谱
  385. val result = Post<DevInfoResult>(Api.DEV_INFO) {
  386. body = DeviceInfoBody.genDeviceInfoBody()
  387. }.await()
  388. val ut = result.recipeUpdateTime
  389. val nr = LinkedList(result.newRecipes)
  390. if (!nr.isEmpty()) {
  391. downloadRecipes(
  392. nr, recipeUpdateDialog,
  393. nr.size.toLong(), downloadDir, downloadName, ut
  394. )
  395. return@scopeNetLife
  396. } else {
  397. recipeUpdateDialog.removeSelf()
  398. showRecipesUpdateDialog(recipeUpdateTime)
  399. }
  400. }
  401. return
  402. }
  403. val recipeUrl = newRecipes.pop()
  404. val recipeNumber = recipeUrl.split("@")[0]
  405. val fileUrl = recipeUrl.split("@")[1]
  406. recipeUpdateDialog.showUpdating(supportFragmentManager, "", sum - newRecipes.size, sum)
  407. scopeNetLife {
  408. val result = Get<File>(fileUrl) {
  409. setDownloadFileName(downloadName)
  410. setDownloadDir(downloadDir)
  411. addDownloadListener(object : ProgressListener() {
  412. override fun onProgress(p: Progress) {
  413. LogUtil.d("dddddd", p.toString())
  414. }
  415. })
  416. }.await()
  417. if (!result.exists()) {
  418. downloadRecipes(
  419. newRecipes,
  420. recipeUpdateDialog,
  421. sum,
  422. downloadDir,
  423. downloadName,
  424. recipeUpdateTime
  425. )
  426. } else {
  427. prepareResource(result, recipeNumber)
  428. downloadRecipes(
  429. newRecipes,
  430. recipeUpdateDialog,
  431. sum,
  432. downloadDir,
  433. downloadName,
  434. recipeUpdateTime
  435. )
  436. }
  437. }.catch {
  438. Log.e("TAG 下载失败11",it.message.toString())
  439. it.localizedMessage?.let { it1 -> Log.e("TAG 下载失败22", it1) }
  440. Log.e("TAG 下载失败33",it.cause?.message.toString())
  441. Log.e("TAG 下载失败444",it.cause?.localizedMessage.toString())
  442. // ToastUtils.showShort("下载失败 :" + it.toString())
  443. ToastUtils.showShort("Download failed")
  444. }
  445. }
  446. mWakeLock.release()
  447. }
  448. private fun prepareResource(file: File, recipeNumber: String) {
  449. try {
  450. val dst = ZipUtils.unzipFile(file, FoodDataProvider.getExtRecipeResourceDir())
  451. FileUtils.delete(file)
  452. if (dst.isNullOrEmpty()) {
  453. return
  454. }
  455. val jsonFile = FoodDataProvider.getResourceConfigJsonPath(recipeNumber)
  456. if (!jsonFile.exists()) {
  457. return
  458. }
  459. val jsonContent = FileKit.readFileToStringB(jsonFile)
  460. val contentData = jsonDecoder.decodeFromString<RecipeDataConfig>(jsonContent)
  461. contentData.resetAllCodes()
  462. FoodDataProvider.getDatabase().runInTransaction {
  463. FoodDataProvider.getDatabase().recipeDao().apply {
  464. var recipeList = contentData.devRecipes
  465. deleteDevRecipeAccessorys(recipeNumber)
  466. deleteDevRecipeCookingSteps(recipeNumber)
  467. deleteDevRecipeFoods(recipeNumber)
  468. deleteDevRecipeNutritions(recipeNumber)
  469. deleteDevRecipeRelTags(recipeNumber)
  470. deleteDevRecipePortionSizes(recipeNumber)
  471. recipeList.forEach {
  472. //加此判断是因为 食谱包有可能updateTime是空或者0 ,由于食谱那边有个new的排序,是需要用到updateTime,估写入的时候做判断
  473. if (it.updateTime==null ||it.updateTime==0L){
  474. it.updateTime = System.currentTimeMillis()
  475. }
  476. deleteNumAndLangRecipe(it.number, it.lang)
  477. }
  478. insertDevAccessorys(contentData.devAccessorys)
  479. insertHotTags(contentData.devHotTags)
  480. insertDevPortraits(contentData.devPortraits)
  481. insertDevRecipeAccessorys(contentData.devRecipeAccessorys)
  482. val categorys = queryAllCategory()
  483. val categoryMap = HashMap<String, DevRecipeCategory>()
  484. for (category in categorys) {
  485. categoryMap[category.number + ":" + category.lang] = category
  486. }
  487. for (devRecipeCategory in contentData.devRecipeCategorys) {
  488. if (categoryMap.containsKey(devRecipeCategory.number + ":" + devRecipeCategory.lang)) {
  489. devRecipeCategory.code =
  490. categoryMap[devRecipeCategory.number + ":" + devRecipeCategory.lang]?.code.toString()
  491. }
  492. }
  493. insertDevRecipeCategorys(contentData.devRecipeCategorys)
  494. insertDevRecipeCookingSteps(contentData.devRecipeCookingSteps)
  495. insertDevRecipeFoods(contentData.devRecipeFoods)
  496. insertDevRecipeNutritions(contentData.devRecipeNutritions)
  497. insertDevRecipePortionSizes(contentData.devRecipePortionSizes)
  498. insertDevRecipeRelTags(contentData.devRecipeRelTags)
  499. insertDevRecipeTags(contentData.devRecipeTags)
  500. insertDevRecipes(recipeList)
  501. //作为下载食谱的新标
  502. numberList.add(recipeNumber)
  503. }
  504. }
  505. } catch (e: Exception) {
  506. e.printStackTrace()
  507. }
  508. }
  509. private fun showRecipesUpdateDialog(recipeUpdateTime: Long?) {
  510. TopResumedAtyHolder.getCurrentActivity()?.apply {
  511. val minute = MMkvUtils.getInt(SCREENSAVER)
  512. if (minute != 0) {
  513. Settings.System.putInt(
  514. contentResolver,
  515. Settings.System.SCREEN_OFF_TIMEOUT,
  516. 1000 * 60 * minute
  517. )
  518. val uri: Uri = Settings.System
  519. .getUriFor(Settings.System.SCREEN_OFF_TIMEOUT)
  520. contentResolver.notifyChange(uri, null)
  521. }
  522. EventBus.getDefault().post(NoScreenEvent(false))
  523. RecipeUpdateDialog().apply {
  524. onDialogClickListener = object : RecipeUpdateDialog.OnDialogClickListener {
  525. override fun onConfirm() {
  526. saveRecipeUpdateTime(recipeUpdateTime)
  527. removeSelf()
  528. }
  529. override fun onCancel() {
  530. }
  531. }
  532. showSuccess(supportFragmentManager, "recipeUpdateDialog")
  533. }
  534. }
  535. }
  536. }
  537. class NoScreenEvent(var noScreen: Boolean)
  538. class TuyaSoEvent()