SwipeCodeTwoActivity.kt 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package xn.hxp.ui.verify
  2. import android.content.IntentFilter
  3. import android.hardware.usb.UsbManager
  4. import android.os.Handler
  5. import android.os.Looper
  6. import android.view.KeyEvent
  7. import android.view.MotionEvent
  8. import android.view.View
  9. import android.widget.TextView
  10. import androidx.viewbinding.ViewBinding
  11. import com.blankj.utilcode.util.LogUtils
  12. import com.bumptech.glide.Glide
  13. import com.bumptech.glide.load.engine.DiskCacheStrategy
  14. import com.bumptech.glide.request.RequestOptions
  15. import com.rc.core.ui.activity.BaseActivity
  16. import com.rc.httpcore.HttpConfig
  17. import com.rc.httpcore.bean.UserValidationBean
  18. import com.rc.httpcore.client.ApiRepository
  19. import com.rc.httpcore.exception.NetException
  20. import org.greenrobot.eventbus.EventBus
  21. import org.greenrobot.eventbus.Subscribe
  22. import org.greenrobot.eventbus.ThreadMode
  23. import retrofit2.HttpException
  24. import xn.hxp.R
  25. import xn.hxp.app.ChemicalApp
  26. import xn.hxp.comm.Constants
  27. import xn.hxp.databinding.ActivitySwipeCodeTwoBinding
  28. import xn.hxp.receiver.OnSerialScanListener
  29. import xn.hxp.receiver.PortScanHelper
  30. import xn.hxp.receiver.UsbReceiver
  31. import xn.hxp.ui.DoubleDialogBean
  32. import xn.hxp.utils.AudioPlayer
  33. import xn.hxp.utils.HandlerUtil
  34. import xn.hxp.utils.SharedPreferencesHelper
  35. import xn.hxp.utils.UiManager
  36. import xn.hxp.weidith.CustomDialog
  37. import xn.hxp.weidith.DoublePeopleDialog
  38. import java.net.ConnectException
  39. import java.net.SocketTimeoutException
  40. //双人-刷卡认证
  41. class SwipeCodeTwoActivity : BaseActivity() {
  42. private var mUsbReceiver: UsbReceiver? = null // 刷卡广播注册
  43. private var mHandleScanEvent = false //当前是否已经获取过 usb返回的参数
  44. private var isLogin = false //是否是登陆人
  45. private var count = 1 //认证次数
  46. private var mDoorId: String? = null //柜门id
  47. private var mChemicalLevel: Int = 0 //管控类型
  48. private var mTag = 0 //0 新增入库 1 归还废弃空瓶等 2 待入库 3 领用认证需要换api 需要判断不同权限 进行验证通过
  49. private var mUserId: String? = null
  50. private val handlerUtil = HandlerUtil.getInstance()
  51. private var mVoiceCount = 1 //认证次数
  52. private val mDoubleDialogBean = mutableListOf<DoubleDialogBean>() //双人认证 需要显示的
  53. lateinit var viewBinding: ActivitySwipeCodeTwoBinding
  54. override fun setViewBinding(): ViewBinding {
  55. viewBinding = ActivitySwipeCodeTwoBinding.inflate(layoutInflater)
  56. return viewBinding
  57. }
  58. override fun onInit() {
  59. Constants.AUTHENTICATION = false
  60. //注册广播
  61. EventBus.getDefault().register(this)
  62. try {
  63. SharedPreferencesHelper.clearList(this)
  64. } catch (e: Exception) {
  65. }
  66. viewBinding.tvReturn.text = "返回${ChemicalApp.confs!!.backTime}s"
  67. mTag = intent.getIntExtra("mTag", 0)
  68. val intExtra = intent.getIntExtra("hides", 0)
  69. mDoorId = intent.getStringExtra("doorId")
  70. mChemicalLevel = intent.getIntExtra("chemicalLevel", 0)
  71. if (intExtra == 4) {
  72. viewBinding.lint.visibility = View.GONE
  73. viewBinding.lintTwo.visibility = View.GONE
  74. } else if (intExtra == 2) {
  75. viewBinding.face.visibility = View.GONE
  76. viewBinding.faceTwo.visibility = View.GONE
  77. }
  78. val map = mutableMapOf<String, Any>()
  79. //跳转扫码
  80. map["chemicalLevel"] = mChemicalLevel //管控类型
  81. map["doorId"] = "$mDoorId" //柜子id
  82. map["mTag"] = mTag
  83. map["hides"] = intExtra //隐藏扫码
  84. viewBinding.face.setOnClickListener {
  85. //需要双人认证 人脸
  86. UiManager.switcher(this, map, TwoPersonActivity::class.java)
  87. finish()
  88. }
  89. viewBinding.wxScan.setOnClickListener {
  90. UiManager.switcher(this, map, ScanCodeTwoActivity::class.java)
  91. finish()
  92. }
  93. viewBinding.faceTwo.setOnClickListener {
  94. //需要双人认证 人脸
  95. UiManager.switcher(this, map, TwoPersonActivity::class.java)
  96. finish()
  97. }
  98. viewBinding.wxScanTwo.setOnClickListener {
  99. UiManager.switcher(this, map, ScanCodeTwoActivity::class.java)
  100. finish()
  101. }
  102. viewBinding.tvReturn.setOnClickListener {
  103. finish()
  104. }
  105. viewBinding.deptName.text = "${ChemicalApp.confs!!.deptName}-${ChemicalApp.confs!!.roomNum}"
  106. Glide.with(this)
  107. .load("${HttpConfig.API_BASE_IMG_URL}${ChemicalApp.confs!!.circularLogo}")
  108. .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.AUTOMATIC))
  109. .into(viewBinding.image)
  110. AudioPlayer.getInstance().play(R.raw.diyiwei_shua_ka_renzheng)
  111. // customDialogView(0,"请第一个人进行认证")
  112. // 定义一个定时任务 双人-刷卡认证
  113. // val task = object : TimerTask() {
  114. // override fun run() {
  115. // if (mVoiceCount == 6) {
  116. // finish()
  117. // }
  118. // if (count == 1) {
  119. // MediaPlayerHelper.playRawMp3(
  120. // this@SwipeCodeTwoActivity,
  121. // R.raw.diyiwei_shua_ka_renzheng
  122. // )
  123. // } else {
  124. // MediaPlayerHelper.playRawMp3(
  125. // this@SwipeCodeTwoActivity,
  126. // R.raw.dierweishuakarenzheng
  127. // )
  128. // }
  129. // mVoiceCount++
  130. // }
  131. // }
  132. // 使用 schedule 方法执行任务,延迟 0 毫秒,每隔 10 秒执行一次
  133. // mTimer.schedule(task, 0, 10000)
  134. handlerUtil.startTask(task, 10000)
  135. }
  136. private val task = Runnable {
  137. if (mVoiceCount == 6) {
  138. finish()
  139. }
  140. if (count == 1) {
  141. AudioPlayer.getInstance().play(R.raw.diyiwei_shua_ka_renzheng)
  142. } else {
  143. AudioPlayer.getInstance().play(R.raw.dierweishuakarenzheng)
  144. }
  145. mVoiceCount++
  146. }
  147. override fun onResume() {
  148. super.onResume()
  149. mPortScanHelper.onResume()
  150. registerUsbBroadcast()
  151. }
  152. override fun onBackPressed() {
  153. super.onBackPressed()
  154. finish()
  155. }
  156. override fun cdTime(cd: Int) {
  157. viewBinding.tvReturn.text = "返回${cd}s"
  158. }
  159. private val mPortScanHelper by lazy {
  160. PortScanHelper(this, object : OnSerialScanListener {
  161. override fun dispatchScanEvent(type: OnSerialScanListener.ScanType, content: String) {
  162. if (!mHandleScanEvent) {
  163. if (content.isNotBlank()) {
  164. mHandleScanEvent = true
  165. handleScanEvent(content)
  166. }
  167. }
  168. }
  169. })
  170. }
  171. //刷卡usb链接
  172. private fun registerUsbBroadcast() {
  173. if (null == mUsbReceiver) {
  174. val filter = IntentFilter().apply {
  175. addAction(UsbReceiver.ACTION_USB_PERMISSION)
  176. addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
  177. addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
  178. addAction(UsbReceiver.ACTION_USB_STATE) // usb连接状态广播
  179. }
  180. mUsbReceiver = UsbReceiver()
  181. registerReceiver(mUsbReceiver, filter)
  182. }
  183. }
  184. //调用刷卡信息
  185. private fun handleScanEvent(cont: String) {
  186. //只有 0 1 1代表 已经需要第二次进行验证
  187. showLoading("验证中...")
  188. val mutableMap = mutableMapOf<String, String>()
  189. mutableMap["cardNum"] = "$cont"
  190. mutableMap["subId"] = "${ChemicalApp.subjectId}"
  191. mutableMap["doorId"] = "$mDoorId"
  192. val disposable = ApiRepository.useCardVerify(mutableMap)
  193. .subscribe({ data ->
  194. dismissLoading()
  195. if (count == 1) {
  196. mUserId = data.userId
  197. LogUtils.i("第a一个人 $mUserId ${data.userId}")
  198. } else {
  199. LogUtils.i("第b一个人 $mUserId ${data.userId}")
  200. if (mUserId.equals(data.userId) && count == 2) {
  201. customDialogView(0, "不能是同一人")
  202. mHandleScanEvent = false
  203. return@subscribe
  204. }
  205. }
  206. //0 新增入库 1 领用归还废弃空瓶等 2 待入库 需要判断不同权限 进行验证通过
  207. when (mTag) {
  208. 0 -> {
  209. if (count == 1) {
  210. if (data.userId == ChemicalApp.userData!!.userId) {
  211. count++
  212. mHandleScanEvent = false
  213. isLogin = true
  214. customDialog(data.userName, data.faceImg)
  215. var bean =
  216. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  217. mDoubleDialogBean.add(bean)
  218. } else {
  219. if (mChemicalLevel == 1) {
  220. //管控 校级管理员或院级管理
  221. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  222. count++
  223. mHandleScanEvent = false
  224. if (count == 2) {
  225. customDialog(data.userName, data.faceImg)
  226. var bean = DoubleDialogBean(
  227. data.userName,
  228. data.faceImg,
  229. data.userId
  230. )
  231. mDoubleDialogBean.add(bean)
  232. } else {
  233. var bean = DoubleDialogBean(
  234. data.userName,
  235. data.faceImg,
  236. data.userId
  237. )
  238. mDoubleDialogBean.add(bean)
  239. passThrough()
  240. }
  241. } else {
  242. mHandleScanEvent = false
  243. customDialogView(0, "无权限信息")
  244. }
  245. } else {
  246. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  247. count++
  248. mHandleScanEvent = false
  249. if (count == 2) {
  250. customDialog(data.userName, data.faceImg)
  251. var bean = DoubleDialogBean(
  252. data.userName,
  253. data.faceImg,
  254. data.userId
  255. )
  256. mDoubleDialogBean.add(bean)
  257. } else {
  258. var bean = DoubleDialogBean(
  259. data.userName,
  260. data.faceImg,
  261. data.userId
  262. )
  263. mDoubleDialogBean.add(bean)
  264. passThrough()
  265. }
  266. } else {
  267. mHandleScanEvent = false
  268. customDialogView(0, "无权限信息")
  269. }
  270. }
  271. }
  272. } else if (count == 2) {
  273. if (isLogin) {
  274. //已经有登陆人
  275. if (mChemicalLevel == 1) {
  276. //管控 校级管理员或院级管理
  277. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  278. var bean = DoubleDialogBean(
  279. data.userName,
  280. data.faceImg,
  281. data.userId
  282. )
  283. mDoubleDialogBean.add(bean)
  284. passThrough()
  285. } else {
  286. mHandleScanEvent = false
  287. customDialogView(0, "无权限信息")
  288. }
  289. } else {
  290. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  291. var bean = DoubleDialogBean(
  292. data.userName,
  293. data.faceImg,
  294. data.userId
  295. )
  296. mDoubleDialogBean.add(bean)
  297. passThrough()
  298. } else {
  299. mHandleScanEvent = false
  300. customDialogView(0, "无权限信息")
  301. }
  302. }
  303. } else {
  304. if (data.userId == ChemicalApp.userData!!.userId) {
  305. var bean =
  306. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  307. mDoubleDialogBean.add(bean)
  308. passThrough()
  309. } else {
  310. mHandleScanEvent = false
  311. customDialogView(0, "无权限信息")
  312. }
  313. }
  314. }
  315. }
  316. 1 -> { //归还业务 标签管理
  317. if (count == 1) {
  318. if (data.userId == ChemicalApp.userData!!.userId) {
  319. count++
  320. mHandleScanEvent = false
  321. isLogin = true
  322. customDialog(data.userName, data.faceImg)
  323. var bean =
  324. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  325. mDoubleDialogBean.add(bean)
  326. } else {
  327. obtainCertification(data)
  328. }
  329. } else if (count == 2) {
  330. if (isLogin) {
  331. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  332. var bean =
  333. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  334. mDoubleDialogBean.add(bean)
  335. passThrough()
  336. } else {
  337. mHandleScanEvent = false
  338. customDialogView(0, "认证失败")
  339. }
  340. } else {
  341. if (data.userId == ChemicalApp.userData!!.userId) {
  342. var bean =
  343. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  344. mDoubleDialogBean.add(bean)
  345. passThrough()
  346. } else {
  347. mHandleScanEvent = false
  348. customDialogView(0, "认证失败")
  349. }
  350. }
  351. }
  352. }
  353. 2 -> {//待入库
  354. if (count == 1) {
  355. if (data.userId == ChemicalApp.userData!!.userId) {
  356. count++
  357. mHandleScanEvent = false
  358. isLogin = true
  359. customDialog(data.userName, data.faceImg)
  360. var bean =
  361. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  362. mDoubleDialogBean.add(bean)
  363. } else {
  364. toBeStored(data)
  365. }
  366. } else if (count == 2) {
  367. if (isLogin) {
  368. if (mChemicalLevel == 1) {
  369. //管控
  370. if (data.safeUser == true || data.adminUser == true || data.cabinetAdmin == true) {
  371. var bean = DoubleDialogBean(
  372. data.userName,
  373. data.faceImg,
  374. data.userId
  375. )
  376. mDoubleDialogBean.add(bean)
  377. passThrough()
  378. } else {
  379. mHandleScanEvent = false
  380. customDialogView(0, "认证失败")
  381. }
  382. } else {
  383. //非管控 实验室负责人or安全负责人or柜锁管理员
  384. if (data.safeUser == true || data.adminUser == true || data.cabinetAdmin == true) {
  385. var bean = DoubleDialogBean(
  386. data.userName,
  387. data.faceImg,
  388. data.userId
  389. )
  390. mDoubleDialogBean.add(bean)
  391. passThrough()
  392. } else {
  393. mHandleScanEvent = false
  394. customDialogView(0, "认证失败")
  395. }
  396. }
  397. } else {
  398. if (data.userId == ChemicalApp.userData!!.userId) {
  399. var bean =
  400. DoubleDialogBean(data.userName, data.faceImg, data.userId)
  401. mDoubleDialogBean.add(bean)
  402. passThrough()
  403. } else {
  404. mHandleScanEvent = false
  405. customDialogView(0, "认证失败")
  406. }
  407. }
  408. }
  409. }
  410. }
  411. }, { throwable ->
  412. dismissLoading()
  413. // showNetError(throwable)
  414. mHandleScanEvent = false
  415. throwableView(throwable)
  416. })
  417. addDisposable(disposable)
  418. }
  419. private fun obtainCertification(data: UserValidationBean) {
  420. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  421. count++
  422. mHandleScanEvent = false
  423. customDialog(data.userName, data.faceImg)
  424. var bean = DoubleDialogBean(data.userName, data.faceImg, data.userId)
  425. mDoubleDialogBean.add(bean)
  426. } else {
  427. mHandleScanEvent = false
  428. customDialogView(0, "认证失败")
  429. }
  430. }
  431. private fun toBeStored(data: UserValidationBean) {
  432. if (mChemicalLevel == 1) {
  433. //管控
  434. if (data.adminUser == true || data.safeUser == true || data.cabinetAdmin == true) {
  435. count++
  436. mHandleScanEvent = false
  437. customDialog(data.userName, data.faceImg)
  438. var bean = DoubleDialogBean(data.userName, data.faceImg, data.userId)
  439. mDoubleDialogBean.add(bean)
  440. } else {
  441. mHandleScanEvent = false
  442. customDialogView(0, "认证失败")
  443. }
  444. } else {
  445. //非管控 实验室负责人or安全负责人or柜锁管理员
  446. if (data.safeUser == true || data.adminUser == true || data.cabinetAdmin == true) {
  447. count++
  448. mHandleScanEvent = false
  449. customDialog(data.userName, data.faceImg)
  450. var bean = DoubleDialogBean(data.userName, data.faceImg, data.userId)
  451. mDoubleDialogBean.add(bean)
  452. } else {
  453. mHandleScanEvent = false
  454. customDialogView(0, "认证失败")
  455. }
  456. }
  457. }
  458. private fun customDialog(userName: String, imgUrl: String?) {
  459. // customDialogView(0,"请第二个人进行认证")
  460. viewBinding.through.visibility = View.VISIBLE
  461. viewBinding.userName.text = "$userName"
  462. Glide.with(this)
  463. .load("${HttpConfig.API_BASE_IMG_URL}$imgUrl")
  464. .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.AUTOMATIC))
  465. .error(R.mipmap.icon_zhan_wei)
  466. .into(viewBinding.imgView)
  467. AudioPlayer.getInstance().play(R.raw.dierweishuakarenzheng)
  468. }
  469. //获取刷卡信息
  470. override fun dispatchKeyEvent(event: KeyEvent): Boolean {
  471. mPortScanHelper.dispatchKeyEvent(event)
  472. return super.dispatchKeyEvent(event)
  473. }
  474. //停止 销毁广播传递
  475. override fun onPause() {
  476. mPortScanHelper.onPause()
  477. super.onPause()
  478. }
  479. override fun onDestroy() {
  480. super.onDestroy()
  481. mPortScanHelper.onPause()
  482. // 停止定时更新
  483. unregisterReceiver(mUsbReceiver)
  484. try {
  485. handlerUtil.removeCallbacks(task)
  486. handlerUtil.stopAllTasks()
  487. handlerBack.removeCallbacks(countdownRunnable)
  488. handlerBack.removeCallbacksAndMessages(null)
  489. } catch (e: Exception) {
  490. }
  491. EventBus.getDefault().unregister(this) //关闭广播
  492. }
  493. override fun cdFinish() {
  494. finish()
  495. }
  496. //必须写这个方法 防止注册失败
  497. @Subscribe(threadMode = ThreadMode.MAIN)
  498. fun onUpdateEventEvent(event: KeyEvent) {
  499. }
  500. /**
  501. * 0 没有图标 1 绿色(成功) 2红色(失败)
  502. * 失败或者成功的弹框
  503. */
  504. private fun customDialogView(types: Int, msg: String) {
  505. if (!this.isFinishing && !this.isDestroyed) {
  506. val customDialog = CustomDialog(this, types, msg)
  507. customDialog.show()
  508. }
  509. }
  510. /**
  511. * 异常处理
  512. */
  513. private fun throwableView(throwable: Throwable) {
  514. when (throwable) {
  515. is NetException -> {
  516. if (throwable.message.isNullOrEmpty()) {
  517. "接口请求失败(${throwable.code})"
  518. } else {
  519. throwable.message!!
  520. }
  521. }
  522. is SocketTimeoutException -> "请求超时,请稍后重试"
  523. is ConnectException -> "无法连接服务器,请检查网络"
  524. is HttpException -> "服务器繁忙,请稍后重试"
  525. else -> null
  526. }?.let { customDialogView(2, "$it") }
  527. }
  528. private lateinit var dialogsAut: DoublePeopleDialog
  529. private var timeLeftInSeconds = 2
  530. private var mTvView: TextView? = null
  531. private val handlerBack = Handler(Looper.getMainLooper())
  532. //验证通过
  533. private fun passThrough() {
  534. try {
  535. handlerUtil.removeCallbacks(task)
  536. handlerUtil.stopAllTasks()
  537. } catch (e: Exception) {
  538. }
  539. // 在需要时关闭定时器
  540. try {
  541. // 取消定时器,停止所有任务的执行
  542. LogUtils.i("验证通过${mDoubleDialogBean[0].name}")
  543. LogUtils.i("验证通过${mDoubleDialogBean[1].name}")
  544. } catch (e: Exception) {
  545. }
  546. Constants.AUTHENTICATION = true
  547. // 存储集合到 SharedPreferences
  548. SharedPreferencesHelper.saveList(this, mDoubleDialogBean)
  549. dialogsAut =
  550. DoublePeopleDialog(this, mDoubleDialogBean, object : DoublePeopleDialog.IClickLit {
  551. override fun onUpView(tvView: TextView) {
  552. mTvView = tvView
  553. }
  554. })
  555. dialogsAut.show()
  556. // 开始倒计时
  557. handlerBack.post(countdownRunnable)
  558. // 获取对话框的 Window 对象
  559. dialogsAut?.window?.decorView?.setOnTouchListener { _, event ->
  560. // 判断是否点击了对话框外部空白区域
  561. if (event.action == MotionEvent.ACTION_DOWN) {
  562. val x = event.x
  563. val y = event.y
  564. val dialogView = dialogsAut?.window?.decorView
  565. if (dialogView != null && (x < 0 || x > dialogView.width || y < 0 || y > dialogView.height)) {
  566. // 在此处执行点击对话框外部空白区域时的操作
  567. // 例如关闭对话框
  568. dialogsAut.dismiss()
  569. finish()
  570. return@setOnTouchListener true
  571. }
  572. }
  573. return@setOnTouchListener false
  574. }
  575. AudioPlayer.getInstance().play(R.raw.shuangren_tongguo)
  576. }
  577. private val countdownRunnable = object : Runnable {
  578. override fun run() {
  579. if (timeLeftInSeconds > 0) {
  580. mTvView!!.text = "${timeLeftInSeconds}秒后自动返回首页"
  581. timeLeftInSeconds--
  582. handlerBack.postDelayed(this, 1000)
  583. } else {
  584. dialogsAut!!.dismiss()
  585. finish()
  586. }
  587. }
  588. }
  589. }