SafetyCheckFragment.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. package xn.xxp.home.sign
  2. import android.content.Context
  3. import android.os.Bundle
  4. import android.os.Message
  5. import android.view.LayoutInflater
  6. import android.view.View
  7. import android.view.ViewGroup
  8. import androidx.appcompat.app.AlertDialog
  9. import androidx.lifecycle.ViewModelProvider
  10. import core.ui.fragment.RcBaseFragment
  11. import core.util.MediaUtils
  12. import core.util.OnWeakListener
  13. import core.util.WeakHandler
  14. import core.util.ifNullOrEmpty
  15. import http.client.ApiRepository
  16. import http.exception.AICheckException
  17. import http.exception.NetException
  18. import http.vo.request.CheckInAllReq
  19. import http.vo.request.PatrolSignInReq
  20. import http.vo.response.LaboratoryVo
  21. import io.fotoapparat.Fotoapparat
  22. import io.fotoapparat.facedetector.processor.FaceDetectorProcessor
  23. import io.fotoapparat.log.fileLogger
  24. import io.fotoapparat.log.logcat
  25. import io.fotoapparat.log.loggers
  26. import io.fotoapparat.selector.front
  27. import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
  28. import org.greenrobot.eventbus.EventBus
  29. import xn.xxp.R
  30. import xn.xxp.app.LabApp
  31. import xn.xxp.databinding.FragmentSafetyCheckBinding
  32. import xn.xxp.home.auth.SignType
  33. import xn.xxp.home.auth.fragment.CardAuthFragment
  34. import xn.xxp.mqtt.event.OnlineUserEvent
  35. import xn.xxp.room.RoomTool
  36. import xn.xxp.room.bean.DeviceConfig
  37. import xn.xxp.room.bean.LabConfig
  38. import xn.xxp.utils.AudioPlayer
  39. import xn.xxp.utils.Tool
  40. import java.io.File
  41. import java.text.SimpleDateFormat
  42. import java.util.*
  43. import java.util.concurrent.TimeUnit
  44. /**
  45. * 安全准入检测
  46. *
  47. * @author ReiChin_
  48. */
  49. class SafetyCheckFragment :
  50. RcBaseFragment<FragmentSafetyCheckBinding>(),
  51. OnWeakListener {
  52. companion object {
  53. // 5s后,开始拍照/录像
  54. private const val MAX_TIME = 5
  55. private const val WHAT_REPEAT = 1
  56. private const val WHAT_TIME_OUT = 2
  57. private const val WHAT_RECORDING = 3
  58. private const val WHAT_COUNT_DOWN = 4
  59. private const val REPEAT_TIME = 10 * 1000L
  60. private const val TIME_OUT = 30 * 1000L
  61. private const val RECORDING_HEART = 300L
  62. fun newInstance(id: String): SafetyCheckFragment {
  63. val args = Bundle()
  64. args.putString("id", id)
  65. val fragment = SafetyCheckFragment()
  66. fragment.arguments = args
  67. return fragment
  68. }
  69. }
  70. private var mCounter = 0
  71. private var mCurrentCheckItem: CheckItem? = null
  72. private var mSignTyp = SignType.ACCESS.code
  73. private var mJumpSafetyCheck = false
  74. private var isMatching = false
  75. private var mSignId: String? = null
  76. private lateinit var laboratoryVo: LaboratoryVo
  77. private lateinit var labConfig: LabConfig
  78. private lateinit var deviceConfig: DeviceConfig
  79. private val viewModel by lazy {
  80. ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()).get(
  81. SignViewModel::class.java
  82. )
  83. }
  84. private var mFragmentCallback: SignInFragmentCallback? = null
  85. private var mCountDown: ICountDown? = null
  86. private lateinit var mFotoapparat: Fotoapparat
  87. override fun onAttach(context: Context) {
  88. super.onAttach(context)
  89. mFragmentCallback = context as? SignInFragmentCallback
  90. mCountDown = context as? ICountDown
  91. }
  92. override fun onDetach() {
  93. super.onDetach()
  94. mFragmentCallback = null
  95. mCountDown = null
  96. }
  97. private fun takePicture() {
  98. isMatching = true
  99. val format = SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.getDefault())
  100. val fileName = "${format.format(Calendar.getInstance().time)}.jpg"
  101. val photoFile = File(requireActivity().getExternalFilesDir("photos"), fileName)
  102. mFotoapparat.takePicture()
  103. .saveToFile(photoFile)
  104. .whenAvailable {
  105. handlerTakePictureResult(photoFile)
  106. }
  107. }
  108. override fun onResume() {
  109. super.onResume()
  110. try {
  111. mFotoapparat.stop()
  112. } catch (e: Exception) {
  113. }
  114. viewBinding.cameraView.postDelayed({
  115. mFotoapparat.start()
  116. isMatching = false
  117. }, 500)
  118. }
  119. override fun onPause() {
  120. super.onPause()
  121. mFotoapparat.stop()
  122. }
  123. override fun createViewBinding(
  124. inflater: LayoutInflater,
  125. container: ViewGroup?
  126. ) = FragmentSafetyCheckBinding.inflate(inflater, container, false)
  127. override fun initViews(savedInstanceState: Bundle?) {
  128. laboratoryVo = LabApp.laboratory
  129. labConfig = RoomTool.getInstance().labConfigDao().labConfig
  130. deviceConfig = RoomTool.getInstance().deviceConfigDao().deviceConfig
  131. mSignId = arguments?.getString("id")
  132. viewModel.checkItemData.observe(viewLifecycleOwner) { data ->
  133. mCurrentCheckItem = data
  134. data?.let {
  135. preStartCheck()
  136. }
  137. }
  138. viewModel.signTypeData.observe(viewLifecycleOwner) { data ->
  139. mSignTyp = data ?: SignType.ACCESS.code
  140. }
  141. viewModel.signFaceData.observe(viewLifecycleOwner) { data ->
  142. data?.let {
  143. mSignId = data.id
  144. mCurrentCheckItem = CheckItem(data.code)
  145. preStartCheck()
  146. }
  147. }
  148. mFotoapparat = Fotoapparat.with(requireActivity())
  149. .into(viewBinding.cameraView)
  150. .lensPosition(
  151. front()
  152. )//front()前置 back 后置
  153. .frameProcessor(
  154. FaceDetectorProcessor.with(requireActivity())
  155. .listener { faces ->
  156. viewBinding.rectanglesView.setRectangles(faces)
  157. }.build()
  158. )
  159. .cameraErrorCallback { onError(it.message) }
  160. .logger(loggers(logcat(), fileLogger(requireActivity())))
  161. .build()
  162. // 开始检测
  163. viewBinding.startCheck.setOnClickListener {
  164. // 移除语音播报提示检测Message
  165. mCountDown?.onStopCountDown()
  166. mWeakHandler.removeMessages(WHAT_REPEAT)
  167. mWeakHandler.removeMessages(WHAT_TIME_OUT)
  168. // 按钮不可用
  169. viewBinding.startCheck.isEnabled = false
  170. // 倒计时5s,结束后,拍照开始
  171. mCounter = 0
  172. viewBinding.countDown.visibility = View.VISIBLE
  173. mWeakHandler.sendEmptyMessage(WHAT_COUNT_DOWN)
  174. }
  175. // 电子牌 + 巡查签到
  176. if (1 == labConfig.isStart && SignType.PATROL.code == mSignTyp) {
  177. preStartCheck()
  178. }
  179. // takePictureInterval()
  180. }
  181. private fun preStartCheck() {
  182. mCountDown?.onStartCountDown()
  183. dispatchCheckTip()
  184. mWeakHandler.sendEmptyMessageDelayed(WHAT_TIME_OUT, TIME_OUT)
  185. }
  186. private fun takePictureInterval() {
  187. val disposable = io.reactivex.rxjava3.core.Observable.interval(3, 3, TimeUnit.SECONDS)
  188. .observeOn(AndroidSchedulers.mainThread())
  189. .subscribe {
  190. if (isVisible && !isMatching) takePicture()
  191. }
  192. addDisposable(disposable)
  193. }
  194. private val mWeakHandler by lazy { WeakHandler(this) }
  195. override fun onWeakHandleMessage(msg: Message) {
  196. if (!isVisible) return
  197. when (msg.what) {
  198. WHAT_COUNT_DOWN -> handleCountDown(msg)
  199. WHAT_RECORDING -> handleRecording(msg)
  200. WHAT_REPEAT -> dispatchCheckTip()
  201. WHAT_TIME_OUT -> dispatchTimeOut()
  202. }
  203. }
  204. private fun handleCountDown(msg: Message) {
  205. val countDown = MAX_TIME - mCounter
  206. if (countDown <= 0) {
  207. viewBinding.countDown.visibility = View.GONE
  208. // 倒计时结束,开始拍照
  209. takePicture()
  210. } else {
  211. viewBinding.countDown.text = "$countDown"
  212. msg.target.sendEmptyMessageDelayed(WHAT_COUNT_DOWN, 1000)
  213. }
  214. mCounter++
  215. }
  216. private fun handleRecording(msg: Message) {
  217. val visibility = msg.obj as? Boolean ?: false
  218. viewBinding.recording.visibility =
  219. if (visibility) View.VISIBLE else View.INVISIBLE
  220. val message = msg.target.obtainMessage(WHAT_RECORDING, !visibility)
  221. msg.target.sendMessageDelayed(message, RECORDING_HEART)
  222. }
  223. private fun dispatchCheckTip() {
  224. // if (!isResumed) return
  225. val message = "穿戴安全准入检测,请确认处于检测区域后,点击开始检测。"
  226. AudioPlayer.getInstance().play(R.raw.audio_check_ready)
  227. mWeakHandler.sendEmptyMessageDelayed(WHAT_REPEAT, REPEAT_TIME)
  228. }
  229. private fun dispatchTimeOut() {
  230. mCountDown?.onStopCountDown()
  231. AudioPlayer.getInstance().play(R.raw.audio_failed_sign)
  232. SafetyCheckResultDialog(requireActivity(), ResultEnum.FAIL, "签到失败", 3000).apply {
  233. setCancelable(false)
  234. setOnDismissListener { requireActivity().finish() }
  235. show()
  236. }
  237. }
  238. private fun handlerTakePictureResult(photoFile: File) {
  239. if (!isVisible) return
  240. viewBinding.startCheck.isEnabled = true
  241. // 1) 显示loading
  242. val message = "检测中,请勿操作屏幕。"
  243. AudioPlayer.getInstance().play(R.raw.audio_checking)
  244. if (null == mCheckLoading) {
  245. mCheckLoading =
  246. SafetyCheckResultDialog(requireActivity(), ResultEnum.LOADING, message).apply {
  247. setCancelable(false)
  248. }
  249. }
  250. mCheckLoading?.show()
  251. // 2) 调用后台接口:穿戴安全准入检测
  252. callCheckApi(photoFile)
  253. }
  254. /*
  255. * 调用检测接口
  256. */
  257. private fun callCheckApi(photoFile: File) {
  258. if (null == labConfig || null == laboratoryVo) {
  259. showToast("暂无实验室信息,即将重新获取")
  260. Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
  261. return
  262. }
  263. val pram = CheckInAllReq().apply {
  264. subId = labConfig.labId.toString()
  265. subName = laboratoryVo.subName
  266. id = mSignId
  267. file = photoFile
  268. }
  269. val disposable = ApiRepository.checkInAll(SignType.PATROL.code == mSignTyp, pram)
  270. .subscribe({
  271. // 删除源文件
  272. MediaUtils.deleteFile(photoFile)
  273. // 检测通过:跳转到下一级检测
  274. dismissCheckLoadingDialog()
  275. dispatchCheckSuccess()
  276. }, { throwable ->
  277. // 删除源文件
  278. MediaUtils.deleteFile(photoFile)
  279. // 检测失败:弹出提示,重新检测
  280. dismissCheckLoadingDialog()
  281. if (!pretreatmentError(throwable, "穿戴检测")) {
  282. throwable.printStackTrace()
  283. showNetError(throwable)
  284. // 出现其它错误时,重新开始计时。
  285. preStartCheck()
  286. }
  287. })
  288. addDisposable(disposable)
  289. }
  290. private fun dismissCheckLoadingDialog() {
  291. mCheckLoading?.let {
  292. if (it.isShowing) it.dismiss()
  293. mCheckLoading = null
  294. }
  295. }
  296. private fun dispatchCheckSuccess() {
  297. val message = "穿戴安全准入检测完成。"
  298. val dialog = SafetyCheckResultDialog(
  299. requireActivity(),
  300. ResultEnum.SUCCESS,
  301. message
  302. ).apply {
  303. setCancelable(false)
  304. show()
  305. }
  306. AudioPlayer.getInstance().play(R.raw.audio_wear_check_completed) {
  307. dialog.dismiss()
  308. callSignInSubmitApi()
  309. }
  310. }
  311. private fun callSignInSubmitApi() {
  312. showLoading("签到中...")
  313. val disposable = if (SignType.PATROL.code == mSignTyp) {
  314. val param = PatrolSignInReq().apply {
  315. num = deviceConfig.devId
  316. userId = LabApp.userVo.userId
  317. subjectId = labConfig.labId.toString()
  318. }
  319. ApiRepository.signInWithPatrol(param)
  320. } else {
  321. val code = mCurrentCheckItem?.code ?: ""
  322. ApiRepository.signIn(mJumpSafetyCheck, code, mSignId!!)
  323. }
  324. .subscribe({ success ->
  325. dismissLoading()
  326. if (success) {
  327. EventBus.getDefault().post(OnlineUserEvent())
  328. val message = "签到成功"
  329. AudioPlayer.getInstance().play(R.raw.audio_success_sign)
  330. SafetyCheckResultDialog(
  331. requireActivity(),
  332. ResultEnum.SUCCESS,
  333. message,
  334. 3000
  335. ).apply {
  336. setCancelable(false)
  337. setOnDismissListener {
  338. Tool.INSTANCE.openDoor()
  339. requireActivity().finish()
  340. }
  341. show()
  342. }
  343. }
  344. }, { throwable ->
  345. dismissLoading()
  346. if (!pretreatmentSignInError(throwable)) {
  347. throwable.printStackTrace()
  348. showNetError(throwable)
  349. }
  350. })
  351. addDisposable(disposable)
  352. }
  353. private fun pretreatmentSignInError(throwable: Throwable): Boolean {
  354. if (throwable is NetException) {
  355. // 600- 签到&签出已超时,请重新刷卡重试!
  356. if ("600" == throwable.code) {
  357. dispatchError_600(throwable.message)
  358. return true
  359. } else {
  360. // 后台返回的其它errorCode
  361. val message = throwable.message.ifNullOrEmpty("签到失败,请联系实验室管理员。")
  362. AudioPlayer.getInstance().play(R.raw.audio_failed_sign_admin)
  363. SafetyCheckResultDialog(requireActivity(), ResultEnum.FAIL, message, 3000).apply {
  364. setCancelable(false)
  365. setOnDismissListener { requireActivity().finish() }
  366. show()
  367. }
  368. return true
  369. }
  370. }
  371. return false
  372. }
  373. private fun dispatchError_600(message: String?) {
  374. AlertDialog.Builder(requireActivity())
  375. .setTitle("提示")
  376. .setMessage(message)
  377. .setPositiveButton("确定") { _, _ ->
  378. val fragment =
  379. requireFragmentManager().findFragmentByTag(CardAuthFragment::class.java.simpleName)
  380. as? CardAuthFragment ?: CardAuthFragment()
  381. mFragmentCallback?.switchFragment(fragment)
  382. }.show()
  383. }
  384. private fun pretreatmentError(throwable: Throwable, text: String): Boolean {
  385. if (throwable is AICheckException) {
  386. // 600- 签到&签出已超时,请重新刷卡重试!
  387. if ("600" == throwable.code) {
  388. dispatchError_600(throwable.message)
  389. return true
  390. } else if ("700" == throwable.code) {
  391. // 700- 检测失败,但是可以跳过此检测项
  392. mJumpSafetyCheck = true
  393. dispatchCheckSuccess()
  394. return true
  395. } else {
  396. // 后台返回了其它code
  397. val front = if ("300" == throwable.code) decodeCheckCode(throwable.data) else null
  398. val message = "${front ?: ""}穿戴检测未通过,请再次点击开始检测。"
  399. AudioPlayer.getInstance().play(speakCheckFailed(throwable.data))
  400. SafetyCheckResultDialog(requireActivity(), ResultEnum.WARNING, message).show()
  401. return true
  402. }
  403. } else if (throwable is NetException) {
  404. val message = throwable.message.ifNullOrEmpty("${text}失败,请联系实验室管理员。")
  405. //AudioPlayerManager.speakVoice(R.raw.audio_failed_sign_admin)
  406. SafetyCheckResultDialog(requireActivity(), ResultEnum.FAIL, message, 3000).apply {
  407. setCancelable(false)
  408. show()
  409. }
  410. return true
  411. }
  412. return false
  413. }
  414. private fun speakCheckFailed(code: String?): Int {
  415. return when (code) {
  416. "1" -> R.raw.audio_wear_check_hmj_failed
  417. "2" -> R.raw.audio_wear_check_syf_failed
  418. "3" -> R.raw.audio_wear_check_st_failed
  419. else -> R.raw.audio_wear_check_failed
  420. }
  421. }
  422. private fun decodeCheckCode(code: String?): String? {
  423. val laboratoryVo = LabApp.laboratory
  424. if (null == laboratoryVo || laboratoryVo.inCheck.isNullOrEmpty()) return null
  425. return laboratoryVo.inCheck.find { it.code == code }?.name
  426. }
  427. private var mCheckLoading: SafetyCheckResultDialog? = null
  428. private fun onError(message: String?) {
  429. dismissLoading()
  430. message?.let { showToast(it) }
  431. viewBinding.startCheck.isEnabled = true
  432. // 拍照错误时,重新开始计时。
  433. mCountDown?.onStartCountDown()
  434. dispatchCheckTip()
  435. mWeakHandler.sendEmptyMessageDelayed(WHAT_TIME_OUT, TIME_OUT)
  436. }
  437. override fun onDestroyView() {
  438. mCountDown?.onStopCountDown()
  439. mWeakHandler.removeCallbacksAndMessages(null)
  440. super.onDestroyView()
  441. }
  442. }