diff --git a/app/src/main/java/com/nova/brain/glass/model/data/CompositeLayupApiData.kt b/app/src/main/java/com/nova/brain/glass/model/data/CompositeLayupApiData.kt new file mode 100644 index 0000000..332635b --- /dev/null +++ b/app/src/main/java/com/nova/brain/glass/model/data/CompositeLayupApiData.kt @@ -0,0 +1,56 @@ +package com.nova.brain.glass.model.data + +data class CompositeLayupDetailItem( + val id: Long = 0, + val taskId: Long = 0, + val taskNo: String = "", + val stepSeq: Int = 0, + val detailInfo: String = "", + val detailResult: String = "", + val detailStatus: Int = 0, + val createdAt: String = "", + val updatedAt: String = "", + val createdBy: String = "", + val updatedBy: String = "", + val isDeleted: Long = 0 +) + +data class CompositeLayupTaskDetail( + val id: Long = 0, + val taskNo: String = "", + val taskName: String = "", + val taskType: Int = 0, + val taskStatus: Int = 0, + val taskSource: String = "", + val taskSteps: Int = 0, + val taskCurrentStep: Int = 0, + val taskFilePaths: String = "", + val centralTaskId: String = "", + val centralTaskResult: String = "", + val createdAt: String = "", + val updatedAt: String = "", + val createdBy: String = "", + val updatedBy: String = "", + val isDeleted: Long = 0, + val detailList: List? = null +) + +data class CompositeLayupTaskDetailResponse( + val code: Int = 0, + val message: String = "", + val data: CompositeLayupTaskDetail? = null, + val success: Boolean = false +) + +data class CompositeLayupRecognizeResult( + val finished: Boolean = false, + val success: Boolean = false, + val errorMessage: String = "" +) + +data class CompositeLayupRecognizeResponse( + val code: Int = 0, + val message: String = "", + val data: CompositeLayupRecognizeResult? = null, + val success: Boolean = false +) diff --git a/app/src/main/java/com/nova/brain/glass/repository/Service4.kt b/app/src/main/java/com/nova/brain/glass/repository/Service4.kt new file mode 100644 index 0000000..7b7a419 --- /dev/null +++ b/app/src/main/java/com/nova/brain/glass/repository/Service4.kt @@ -0,0 +1,26 @@ +package com.nova.brain.glass.repository + +import com.nova.brain.glass.model.data.CompositeLayupRecognizeResponse +import com.nova.brain.glass.model.data.CompositeLayupTaskDetailResponse +import io.reactivex.Observable +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.Part +import retrofit2.http.POST +import retrofit2.http.Query + +interface Service4 { + + @GET("/api/glass/workTask/queryTask") + fun queryTask(@Query("taskNo") taskNo: String): Observable + + @Multipart + @POST("/api/glass/workTask/ocrRecognize") + fun ocrRecognize( + @Part("taskNo") taskNo: RequestBody, + @Part("stepSeq") stepSeq: RequestBody, + @Part file: MultipartBody.Part + ): Observable +} diff --git a/app/src/main/java/com/nova/brain/glass/ui/CompositeLayupTaskActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/CompositeLayupTaskActivity.kt new file mode 100644 index 0000000..7596e98 --- /dev/null +++ b/app/src/main/java/com/nova/brain/glass/ui/CompositeLayupTaskActivity.kt @@ -0,0 +1,256 @@ +package com.nova.brain.glass.ui + +import android.os.Environment +import android.view.View +import androidx.lifecycle.ViewModelProvider +import com.nova.brain.glass.R +import com.nova.brain.glass.databinding.ActivityCompositeLayupTaskBinding +import com.nova.brain.glass.helper.GlassMediaServiceHelper +import com.nova.brain.glass.helper.OfflineCmdListener +import com.nova.brain.glass.helper.OfflineCmdServiceHelper +import com.nova.brain.glass.viewmodel.CompositeLayupRecognizeState +import com.nova.brain.glass.viewmodel.CompositeLayupTaskVM +import com.rokid.security.glass3.sdk.base.data.media.PhotoResolution +import com.rokid.security.system.server.media.callback.PhotoFileCallback +import com.xuqm.base.common.LogHelper +import com.xuqm.base.extensions.showMessage +import com.xuqm.base.ui.BaseActivity +import java.io.File +import java.util.UUID + +class CompositeLayupTaskActivity : BaseActivity() { + override fun getLayoutId(): Int = R.layout.activity_composite_layup_task + override fun fullscreen(): Boolean = true + + private enum class ScreenMode { + START, + STEP_SUCCESS, + FAILED, + FINISHED + } + + private val viewModel: CompositeLayupTaskVM by lazy { + ViewModelProvider(this)[CompositeLayupTaskVM::class.java] + } + private val taskNoFromIntent: String by lazy { + intent.getStringExtra(EXTRA_TASK_NO).orEmpty() + } + + private var isPhotoFallback = false + private var isCaptureInFlight = false + private var screenMode = ScreenMode.START + + private val listener = object : OfflineCmdListener { + override fun onOfflineCmd(cmd: String) { + runOnUiThread { + when (cmd) { + "退出", "返回", "退回" -> finish() + "开始", "开始任务" -> if (screenMode == ScreenMode.START) triggerCapture() + "下一步", "继续识别", "继续任务" -> { + if (screenMode == ScreenMode.STEP_SUCCESS) triggerCapture() + } + "重拍", "重新拍照", "重新拍摄" -> { + if (screenMode == ScreenMode.FAILED) triggerCapture() + } + "完成任务" -> if (screenMode == ScreenMode.FINISHED) finish() + } + } + } + } + + private val photoCallbackId = UUID.randomUUID().toString() + private val photoCallback = object : PhotoFileCallback.Stub() { + override fun onTakePhoto(path: String) { + LogHelper.d("CompositeLayupTask onTakePhoto: $path") + } + + override fun getCallbackId(): String = photoCallbackId + + override fun onTakePhotoV2(path: String?, width: Int, height: Int) { + LogHelper.d("CompositeLayupTask onTakePhotoV2 width=$width height=$height path=$path") + if (path == null) { + if (isPhotoFallback) { + isPhotoFallback = false + takePhoto() + } else { + isCaptureInFlight = false + runOnUiThread { + updateHint("相机异常,请重试") + } + "相机异常".showMessage() + } + return + } + isCaptureInFlight = false + runOnUiThread { + updateHint("OCR识别中,请稍后...") + viewModel.recognize(path) + } + } + } + + override fun initData() { + super.initData() + window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + bindClicks() + applyScreenMode(ScreenMode.START) + observeViewModel() + if (taskNoFromIntent.isBlank()) { + updateHint("任务编号缺失") + "任务编号缺失".showMessage() + } else { + viewModel.loadTaskDetail(taskNoFromIntent) + } + } + + private fun bindClicks() { + binding.btnAction.setOnClickListener { + when (screenMode) { + ScreenMode.START, ScreenMode.STEP_SUCCESS, ScreenMode.FAILED -> triggerCapture() + ScreenMode.FINISHED -> finish() + } + } + binding.btnSecondary.setOnClickListener { + if (screenMode == ScreenMode.FINISHED) { + finish() + } + } + } + + private fun observeViewModel() { + viewModel.taskDetail.observe(this) { detail -> + if (detail != null) { + binding.tvTaskNo.text = "工号:${detail.taskNo.ifBlank { taskNoFromIntent }}" + binding.tvTaskName.text = detail.taskName.ifBlank { "铺贴任务列表" } + updateProgressText() + } + } + viewModel.taskDetailError.observe(this) { msg -> + if (msg.isNotBlank()) { + updateHint(msg) + msg.showMessage() + } + } + viewModel.recognizeState.observe(this) { state -> + when (state) { + CompositeLayupRecognizeState.LOADING -> { + binding.btnAction.isClickable = false + updateHint("OCR识别中,请稍后...") + } + CompositeLayupRecognizeState.SUCCESS -> { + binding.btnAction.isClickable = true + handleRecognizeResult() + } + CompositeLayupRecognizeState.FAILED -> { + binding.btnAction.isClickable = true + val error = viewModel.recognizeError.value?.ifBlank { "OCR识别失败" } ?: "OCR识别失败" + applyScreenMode(ScreenMode.FAILED, error) + } + else -> binding.btnAction.isClickable = true + } + } + } + + private fun handleRecognizeResult() { + val result = viewModel.recognizeResult.value ?: return + when { + result.success && result.finished -> applyScreenMode(ScreenMode.FINISHED) + result.success -> applyScreenMode(ScreenMode.STEP_SUCCESS) + else -> applyScreenMode(ScreenMode.FAILED, result.errorMessage.ifBlank { "识别铺贴错误,请重新拍取" }) + } + viewModel.resetRecognizeState() + } + + private fun triggerCapture() { + if (isCaptureInFlight || taskNoFromIntent.isBlank()) { + return + } + isCaptureInFlight = true + binding.btnAction.isClickable = false + updateHint("拍照中,请稍后...") + takePhoto() + } + + private fun takePhoto() { + isPhotoFallback = true + val fileName = "composite_layup_${System.currentTimeMillis()}.png" + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + fileName + ) + GlassMediaServiceHelper.takePhoto(PhotoResolution.RESOLUTION_720P, file.absolutePath) + } + + private fun updateProgressText() { + binding.tvProgress.text = "任务进度:${viewModel.currentStepSeq}/${viewModel.totalSteps}" + binding.tvStepHint.text = "识别到当前层第${viewModel.currentStepSeq}层 / 共${viewModel.totalSteps}层" + } + + private fun applyScreenMode(mode: ScreenMode, errorMessage: String = "") { + screenMode = mode + updateProgressText() + binding.groupStart.visibility = if (mode == ScreenMode.START) View.VISIBLE else View.GONE + binding.groupResult.visibility = if (mode == ScreenMode.START) View.GONE else View.VISIBLE + binding.btnSecondary.visibility = if (mode == ScreenMode.FINISHED) View.VISIBLE else View.GONE + when (mode) { + ScreenMode.START -> { + binding.resultIcon.setImageResource(R.mipmap.ocr_photo) + binding.resultTitle.text = "请开始本层铺贴工作" + binding.resultSubtitle.text = "语音输入“开始任务”或点击按钮后拍照识别" + binding.btnAction.text = "开工" + updateHint("左滑右滑选择任务,单击开工,开始该任务。") + } + ScreenMode.STEP_SUCCESS -> { + binding.resultIcon.setImageResource(R.mipmap.ocr_true) + binding.resultTitle.text = "识别铺贴层与料号信息正确" + binding.resultSubtitle.text = "请开始下一层铺贴工作" + binding.btnAction.text = "下一步" + updateHint("当前层识别通过,可进入下一层识别。") + } + ScreenMode.FAILED -> { + binding.resultIcon.setImageResource(R.mipmap.ocr_false) + binding.resultTitle.text = errorMessage.ifBlank { "识别铺贴错误,请重新拍取" } + binding.resultSubtitle.text = "请重新拍照识别" + binding.btnAction.text = "重拍" + updateHint(errorMessage.ifBlank { "识别失败,请重试" }) + } + ScreenMode.FINISHED -> { + binding.resultIcon.setImageResource(R.mipmap.ocr_true) + binding.resultTitle.text = "恭喜完成全部铺贴逐层任务!" + binding.resultSubtitle.text = "请点击返回,回到铺贴任务列表开启下一任务" + binding.btnAction.text = "返回" + binding.btnSecondary.text = "完成任务" + updateHint("当前任务已全部完成。") + } + } + } + + private fun updateHint(text: String) { + binding.hint.text = text + } + + override fun onResume() { + super.onResume() + isCaptureInFlight = false + binding.btnAction.isClickable = true + GlassMediaServiceHelper.addPhotoCallback(photoCallback) + OfflineCmdServiceHelper.addListenerCompositeLayup() + OfflineCmdServiceHelper.addOnLineListener(listener) + } + + override fun onPause() { + super.onPause() + GlassMediaServiceHelper.removePhotoCallback(photoCallback) + OfflineCmdServiceHelper.removeListenerCompositeLayup() + OfflineCmdServiceHelper.removeOnLineListener(listener) + } + + override fun onDestroy() { + super.onDestroy() + window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + companion object { + const val EXTRA_TASK_NO = "extra_task_no" + } +} diff --git a/app/src/main/java/com/nova/brain/glass/ui/TaskListActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/TaskListActivity.kt index bd183d5..c961d3c 100644 --- a/app/src/main/java/com/nova/brain/glass/ui/TaskListActivity.kt +++ b/app/src/main/java/com/nova/brain/glass/ui/TaskListActivity.kt @@ -124,13 +124,13 @@ class TaskListActivity : } private fun routeToTask(item: TaskItem) { - when (item.taskType) { - "复材MES任务" -> startActivity( + when { + item.taskType == "复材MES任务" -> startActivity( Intent(this, FoActivity::class.java) .putExtra("aiDescription", item.displayDesc()) .putExtra("taskType", item.taskType) ) - "审核任务" -> startActivity( + item.taskType == "审核任务" -> startActivity( Intent(this, ReviewActivity::class.java) .putExtra("taskId", item.id) .putExtra("taskType", item.taskType) @@ -149,19 +149,19 @@ class TaskListActivity : ) ) ) - "天镜检验任务" -> startActivity( + item.taskType == "天镜检验任务" -> startActivity( Intent(this, SprayingActivity::class.java) .putExtra("taskId", item.params.firstNotBlank("taskId", "task_id", "id")) .putExtra("aiDescription", item.displayDesc()) .putExtra("taskType", item.taskType) ) - "检验任务", "产业大脑检验任务" -> startActivity( + item.taskType == "检验任务" || item.taskType == "产业大脑检验任务" -> startActivity( Intent(this, InspectionActivity::class.java) .putExtra("glassTaskId", item.params.firstNotBlank("taskId", "task_id", "id")) .putExtra("taskName", item.params?.get("taskName").orEmpty()) .putExtra("taskNumber", item.params?.get("taskNumber").orEmpty()) ) - "复材铺贴任务" -> startActivity( + item.taskType.contains("复材铺贴任务") -> startActivity( Intent(this, CompositeLayupTaskActivity::class.java) .putExtra( CompositeLayupTaskActivity.EXTRA_TASK_NO, diff --git a/app/src/main/java/com/nova/brain/glass/viewmodel/CompositeLayupTaskVM.kt b/app/src/main/java/com/nova/brain/glass/viewmodel/CompositeLayupTaskVM.kt new file mode 100644 index 0000000..1d89298 --- /dev/null +++ b/app/src/main/java/com/nova/brain/glass/viewmodel/CompositeLayupTaskVM.kt @@ -0,0 +1,118 @@ +package com.nova.brain.glass.viewmodel + +import androidx.lifecycle.MutableLiveData +import com.nova.brain.glass.MyApplication +import com.nova.brain.glass.model.data.CompositeLayupRecognizeResult +import com.nova.brain.glass.model.data.CompositeLayupTaskDetail +import com.nova.brain.glass.repository.Service4 +import com.xuqm.base.di.manager.HttpManager +import androidx.lifecycle.ViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File + +enum class CompositeLayupRecognizeState { IDLE, LOADING, SUCCESS, FAILED } + +class CompositeLayupTaskVM : ViewModel() { + + val taskDetail = MutableLiveData() + val taskDetailError = MutableLiveData() + val recognizeState = MutableLiveData(CompositeLayupRecognizeState.IDLE) + val recognizeResult = MutableLiveData() + val recognizeError = MutableLiveData() + + private val disposables = CompositeDisposable() + var taskNo: String = "" + var currentStepSeq: Int = 1 + private set + var totalSteps: Int = 1 + private set + + fun loadTaskDetail(taskNo: String) { + this.taskNo = taskNo + val disposable = HttpManager.getApi(MyApplication.appComponent4, Service4::class.java) + .queryTask(taskNo) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + if (response.success && response.data != null) { + bindTaskDetail(response.data) + } else { + taskDetailError.value = response.message.ifBlank { "获取任务详情失败" } + } + }, { e -> + taskDetailError.value = e.message ?: "获取任务详情失败" + }) + disposables.add(disposable) + } + + fun recognize(photoPath: String) { + val file = File(photoPath) + if (!file.exists()) { + recognizeState.value = CompositeLayupRecognizeState.FAILED + recognizeError.value = "图片不存在" + return + } + if (taskNo.isBlank()) { + recognizeState.value = CompositeLayupRecognizeState.FAILED + recognizeError.value = "任务编号为空" + return + } + recognizeState.value = CompositeLayupRecognizeState.LOADING + val taskNoBody = taskNo.toRequestBody("text/plain".toMediaTypeOrNull()) + val stepSeqBody = currentStepSeq.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val requestFile = file.asRequestBody("application/octet-stream".toMediaTypeOrNull()) + val filePart = MultipartBody.Part.createFormData("file", file.name, requestFile) + val disposable = HttpManager.getApi(MyApplication.appComponent4, Service4::class.java) + .ocrRecognize(taskNoBody, stepSeqBody, filePart) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + val result = response.data + if (response.success && result != null) { + recognizeResult.value = result + recognizeState.value = CompositeLayupRecognizeState.SUCCESS + if (result.success && !result.finished) { + currentStepSeq = (currentStepSeq + 1).coerceAtMost(totalSteps.coerceAtLeast(1)) + } + } else { + recognizeState.value = CompositeLayupRecognizeState.FAILED + recognizeError.value = response.message.ifBlank { "OCR识别失败" } + } + }, { e -> + recognizeState.value = CompositeLayupRecognizeState.FAILED + recognizeError.value = e.message ?: "OCR识别失败" + }) + disposables.add(disposable) + } + + fun resetRecognizeState() { + recognizeState.value = CompositeLayupRecognizeState.IDLE + recognizeResult.value = null + recognizeError.value = "" + } + + private fun bindTaskDetail(detail: CompositeLayupTaskDetail) { + taskDetail.value = detail + this.taskNo = detail.taskNo.ifBlank { taskNo } + totalSteps = detail.taskSteps.coerceAtLeast(detail.detailList?.size ?: 1).coerceAtLeast(1) + currentStepSeq = when { + detail.taskCurrentStep > 0 -> detail.taskCurrentStep + !detail.detailList.isNullOrEmpty() -> { + detail.detailList.firstOrNull { it.detailStatus != 9 }?.stepSeq + ?: detail.detailList.last().stepSeq.coerceAtLeast(1) + } + else -> 1 + }.coerceIn(1, totalSteps) + } + + override fun onCleared() { + super.onCleared() + disposables.clear() + } +} diff --git a/app/src/main/res/drawable/bg_composite_button_outline.xml b/app/src/main/res/drawable/bg_composite_button_outline.xml new file mode 100644 index 0000000..22a9321 --- /dev/null +++ b/app/src/main/res/drawable/bg_composite_button_outline.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_composite_button_solid.xml b/app/src/main/res/drawable/bg_composite_button_solid.xml new file mode 100644 index 0000000..b232bc4 --- /dev/null +++ b/app/src/main/res/drawable/bg_composite_button_solid.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_composite_circle.xml b/app/src/main/res/drawable/bg_composite_circle.xml new file mode 100644 index 0000000..2f22975 --- /dev/null +++ b/app/src/main/res/drawable/bg_composite_circle.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_composite_panel.xml b/app/src/main/res/drawable/bg_composite_panel.xml new file mode 100644 index 0000000..37c1d02 --- /dev/null +++ b/app/src/main/res/drawable/bg_composite_panel.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_composite_row_selected.xml b/app/src/main/res/drawable/bg_composite_row_selected.xml new file mode 100644 index 0000000..556b198 --- /dev/null +++ b/app/src/main/res/drawable/bg_composite_row_selected.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/activity_composite_layup_task.xml b/app/src/main/res/layout/activity_composite_layup_task.xml new file mode 100644 index 0000000..40d2772 --- /dev/null +++ b/app/src/main/res/layout/activity_composite_layup_task.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +