[BadCase]BadCase业务移植到DevaTools模块

[BadCase]入口展示和入口隐藏回调添加

[BadCase]移除之前无用的逻辑

[BadCase]code
This commit is contained in:
renwj
2022-03-09 12:26:18 +08:00
parent 6a17ee9f68
commit a6267114d4
37 changed files with 1273 additions and 839 deletions

View File

@@ -4,6 +4,7 @@ plugins {
id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'com.alibaba.arouter'
id 'com.google.protobuf'
}
android {
@@ -39,21 +40,45 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += [
"-Xopt-in=kotlin.RequiresOptIn"
]
}
protobuf {
protoc {
artifact = rootProject.ext.dependencies.google_protoc
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {}
}
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation rootProject.ext.dependencies.kotlinstdlibjdk7
implementation rootProject.ext.dependencies.coroutinescore
implementation rootProject.ext.dependencies.arouter
kapt rootProject.ext.dependencies.aroutercompiler
implementation rootProject.ext.dependencies.mogologlib
implementation rootProject.ext.dependencies.mogochainbase
kapt rootProject.ext.dependencies.androidxroomcompiler
implementation rootProject.ext.dependencies.androidxroomruntime
implementation rootProject.ext.dependencies.androidxroomktx
implementation rootProject.ext.dependencies.androidx_datastore
implementation rootProject.ext.dependencies.google_proto_java
implementation rootProject.ext.dependencies.androidxappcompat
implementation rootProject.ext.dependencies.androidxconstraintlayout
implementation rootProject.ext.dependencies.androidxrecyclerview
if (Boolean.valueOf(USE_MAVEN_PACKAGE)) {
implementation rootProject.ext.dependencies.mogoserviceapi
implementation rootProject.ext.dependencies.modulecommon
implementation rootProject.ext.dependencies.mogo_core_utils
implementation rootProject.ext.dependencies.mogo_core_function_api
implementation rootProject.ext.dependencies.mogo_core_function_call

View File

@@ -2,8 +2,10 @@ package com.zhjt.mogo_core_function_devatools
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import com.alibaba.android.arouter.facade.annotation.Route
import com.mogo.cloud.passport.MoGoAiCloudClientConfig
import com.mogo.eagle.core.data.autopilot.AutoPilotRecordResult
import com.mogo.eagle.core.data.chain.ChainConstant.Companion.CHAIN_LINK_LOG_ADAS_INIT
import com.mogo.eagle.core.data.chain.ChainConstant.Companion.CHAIN_LINK_LOG_ADAS_MSG
import com.mogo.eagle.core.data.chain.ChainConstant.Companion.CHAIN_LINK_LOG_CONNECT_STATUS
@@ -18,6 +20,7 @@ import com.mogo.eagle.core.utilcode.util.DeviceUtils
import com.mogo.eagle.core.utilcode.util.Utils
import com.zhidao.loglib.fw.FileWriteManager
import com.zhidao.loglib.fw.FwBuild
import com.zhjt.mogo_core_function_devatools.badcase.BadCaseManager
import com.zhjt.mogo_core_function_devatools.logcatch.MogoLogCatchManager
import com.zhjt.service.chain.core.ChainTraceStarter
@@ -100,6 +103,14 @@ class DevaToolsProvider : IDevaToolsProvider {
FileWriteManager.getInstance().operateChainMap(fwBuildMap)
}
override fun initBadCase(view: View, onShow: (() -> Unit)?, onHide: (() -> Unit)?) {
BadCaseManager.init(view, onShow, onHide)
}
override fun onReceiveBadCaseRecord(record: AutoPilotRecordResult) {
BadCaseManager.onReceiveBadCaseRecord(record)
}
override fun onDestroy() {
MogoLogCatchManager.onDestroy()
}

View File

@@ -0,0 +1,281 @@
package com.zhjt.mogo_core_function_devatools.badcase
import android.transition.AutoTransition
import android.transition.TransitionManager
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Lifecycle.Event
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.mogo.cloud.passport.MoGoAiCloudClientConfig
import com.mogo.eagle.core.data.autopilot.AutoPilotRecordResult
import com.mogo.eagle.core.function.call.hmi.CallerHmiManager
import com.mogo.eagle.core.utilcode.kotlin.lifecycleOwner
import com.mogo.eagle.core.utilcode.kotlin.onClick
import com.mogo.eagle.core.utilcode.util.ToastUtils
import com.mogo.eagle.core.utilcode.util.Utils
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.mvp.BadCasePresenter
import com.zhjt.mogo_core_function_devatools.badcase.mvp.BadCaseView
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
internal object BadCaseManager : LifecycleEventObserver {
const val TAG = "BadCase"
/**
* 超过此时间case入口自动消失
*/
private val CASE_EXPIRE_DURATION: Long = TimeUnit.HOURS.toMillis(4)/* TimeUnit.SECONDS.toMillis(10) */
private var onShow: (() -> Unit)? = null
private var onHide: (() -> Unit)? = null
private var hideFloat: (() -> Unit)? = null
@Volatile
private var record: AutoPilotRecord? = null
@Volatile
private var viewHolder : WeakReference<View>? = null
@Volatile
private var dismissJob: Job? = null
@OptIn(ExperimentalCoroutinesApi::class)
private var channel: Channel<AutoPilotRecord> = Channel(Channel.RENDEZVOUS)
get() = if (field.isClosedForReceive || field.isClosedForSend) {
field = Channel(Channel.RENDEZVOUS)
field
} else {
field
}
private val presenter by lazy {
BadCasePresenter()
}
@Volatile
private var scope: LifecycleCoroutineScope? = null
get() = if (field == null) {
field = viewHolder?.get()?.lifecycleOwner?.lifecycleScope
field
} else {
field
}
fun init(view: View, onShow: (() -> Unit)?, onHide: (() -> Unit)?) {
this.viewHolder = WeakReference(view)
view.lifecycleOwner.lifecycle.addObserver(this)
this.onShow = onShow
this.onHide = onHide
register()
recoverBadCase()
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun register() {
scope?.launch(Dispatchers.Default) {
while (true) {
Log.d(TAG, "---- 开始监听BadCase事件 ----")
val old = record
if (old == null || old.consumed) {
Log.d(TAG, "---- 当前事件已消费 -- value: $old")
var receive = channel.receive()
var oldT = record?.toLongTime() ?: 0L
var newT = receive.toLongTime()
if (isValid(oldT, newT)) {
record = receive
Log.d(TAG, "---- 时间有效,开始展示入口 ---")
withContext(Dispatchers.Main) {
showBadCaseInternal(receive)
}
continue
}
Log.d(TAG, "---- 时间无效,移除管道中无用数据 ---")
presenter.deleteRecord(receive)
while (oldT != 0L && newT != 0L && (newT - oldT) >= CASE_EXPIRE_DURATION) {
oldT = newT
receive = channel.receive()
newT = receive.toLongTime()
presenter.deleteRecord(receive)
}
receive.takeIf { it.key != old?.key }?.also {
Log.d(TAG, "record: [$record] is displaying for rest ...")
record = receive
withContext(Dispatchers.Main) {
showBadCaseInternal(it)
}
}
} else {
Log.d(TAG, "record: [$old] hasn't been consumed~~~~")
withContext(Dispatchers.Main) {
showEntry()
}
delay(1000)
}
}
}
}
private fun recoverBadCase() {
scope?.launchWhenCreated {
val lastModified = presenter.getLastModified()
val list = withContext(Dispatchers.IO) {
try {
Log.d(TAG, " --- 1 ----")
Log.d(TAG, "恢复持久化的数据 - 最后修改时间:$lastModified")
presenter.getUnConsumedRecords().fold(mutableListOf<AutoPilotRecord>()) {
acc, record ->
if (isValid(lastModified, record.toLongTime())) {
acc.add(record)
} else {
presenter.deleteRecord(record)
}
acc
}
} catch (t: Throwable) {
emptyList()
}
}
if (list.isEmpty()) {
Log.d(TAG, "没有要恢复的数据")
} else {
list.forEach {
Log.d(TAG, "恢复的接管数据:$it")
channel.send(it)
}
}
}
}
private fun isValid(oldT: Long, newT: Long): Boolean {
return oldT == 0L || newT == 0L || (newT - oldT >= 0 && (newT - oldT) < CASE_EXPIRE_DURATION)
}
fun onReceiveBadCaseRecord(record: AutoPilotRecordResult) {
scope?.launch {
val newRecord = record.toRecord()
withContext(Dispatchers.IO) {
presenter.insertRecord(newRecord)
channel.send(newRecord)
}
}
}
private fun CoroutineScope.showBadCaseInternal(record: AutoPilotRecord) = launch {
viewHolder?.get()?.also {
presenter.updateLastModified(record.toLongTime())
showEntry()
it.onClick {
showBadCaseFloat(
onDismiss = {
hideFloat?.invoke()
hideFloat = null
},
onSelect = { reason ->
val uploadResult = presenter.upload(mutableMapOf<String, String>().also { itx ->
itx["carLicense"] = MoGoAiCloudClientConfig.getInstance().sn
itx["filename"] = record.fileName ?: ""
itx["filesize"] = record.total.toString()
itx["key"] = record.key ?: ""
itx["reason"] = reason.reason ?: ""
itx["duration"] = record.duration.toInt().toString()
itx["timestamp"] = record.timestamp
})
if (uploadResult == null || uploadResult.code != 200) {
ToastUtils.showShort("接管反馈失败")
} else {
ToastUtils.showShort("接管反馈成功")
record.consumed = true
withContext(Dispatchers.IO) {
presenter.deleteRecord(record)
}
hideEntry()
hideFloat?.invoke()
hideFloat = null
}
})
}
dismissAfterDelay()?.also { dismissJob = it }
}
}
private fun dismissAfterDelay(): Job? {
dismissJob?.takeIf { it.isActive }?.cancel()
return scope?.launch {
delay(CASE_EXPIRE_DURATION)
hideEntry()
record?.also {
it.consumed = true
withContext(Dispatchers.IO) {
presenter.deleteRecord(it)
}
}
}
}
private fun showEntry() {
viewHolder?.get()?.takeIf { it.visibility != View.VISIBLE }?.also {
it.toggle(true)
onShow?.invoke()
}
}
private fun hideEntry() {
viewHolder?.get()?.takeIf { it.visibility == View.VISIBLE }?.also {
it.toggle(false)
onHide?.invoke()
}
}
private fun showBadCaseFloat(onDismiss: () -> Unit, onSelect:suspend (reason: Reason) -> Unit) {
val context = viewHolder?.get()?.context ?: Utils.getApp()
BadCaseView(context).also {
it.register(record, onDismiss, onSelect)
hideFloat = CallerHmiManager.showBadCaseFloat(floatView = it)
}
}
override fun onStateChanged(source: LifecycleOwner, event: Event) {
if (event == ON_DESTROY) {
dismissJob?.takeIf { it.isActive }?.cancel()
onHide = null
onShow = null
hideFloat = null
}
}
}
fun <T: View> T.toggle(show: Boolean) {
val group = (parent as? ViewGroup) ?: return
val target = if (show) View.VISIBLE else View.GONE
takeIf { it.visibility != target }?.also {
TransitionManager.beginDelayedTransition(group, AutoTransition())
it.visibility = target
}
}
internal fun AutoPilotRecordResult.toRecord(): AutoPilotRecord = AutoPilotRecord().also {
it.id = this.id
it.stat = this.stat
it.key = this.key
it.note = this.note
it.type = this.type
it.total = this.total
it.fileName = this.fileName
it.duration = this.duration
it.diskFree = this.diskFree
it.consumed = false
}

View File

@@ -0,0 +1,19 @@
package com.zhjt.mogo_core_function_devatools.badcase.api
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.UploadResult
import retrofit2.Response
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
internal interface BadCaseApi {
@FormUrlEncoded
@POST("/yycp-vehicle-management-service/tool/badcase/add")
suspend fun post(@FieldMap map: Map<String, String>): Response<UploadResult>
@GET("/yycp-vehicle-management-service/tool/badcase/reasons")
suspend fun get(): Response<BadCaseResponse>
}

View File

@@ -0,0 +1,28 @@
package com.zhjt.mogo_core_function_devatools.badcase.api.entity
import androidx.annotation.Keep
import com.google.gson.annotations.Expose
@Keep
internal class BadCaseResponse {
var code: Int = -1
var data: List<Reason>? = null
var msg: String? = null
var success: Boolean = false
var total: Int = -1
@Expose(serialize = false, deserialize = false)
var isBuildIn: Boolean = false
@Keep
class Reason {
var id: String? = null
var reason: String? = null
/**
* 业务字段,不参与序列化和反序列化
*/
@Expose(deserialize = false, serialize = false)
var isChecked: Boolean = false
}
}

View File

@@ -0,0 +1,15 @@
package com.zhjt.mogo_core_function_devatools.badcase.api.entity
import androidx.annotation.Keep
@Keep
internal class UploadResult {
var code: Int = -1
var msg: String? = null
var data: Array<String>? = null
var success: Boolean = false
override fun toString(): String {
return "UploadResult(code=$code, msg=$msg, data=${data?.contentToString()}, success=$success)"
}
}

View File

@@ -0,0 +1,8 @@
package com.zhjt.mogo_core_function_devatools.badcase.consts
import com.mogo.commons.debug.DebugConfig
internal object BadCaseHost {
fun getHost(): String = if (DebugConfig.getNetMode() == DebugConfig.NET_MODE_RELEASE) "http://dzt.zhidaozhixing.com" else "http://front.zdjs-private-test.myghost.zhidaoauto.com"
}

View File

@@ -0,0 +1,54 @@
package com.zhjt.mogo_core_function_devatools.badcase.mvp
import android.util.Log
import com.zhjt.mogo_core_function_devatools.badcase.BadCaseManager
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.mvp.biz.IBadCasePresenter
import com.zhjt.mogo_core_function_devatools.badcase.repository.Repository
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
import kotlinx.coroutines.flow.Flow
internal class BadCasePresenter: IBadCasePresenter {
private val repository by lazy {
Repository()
}
override suspend fun loadBadCases() = repository.loadBadCases()
override suspend fun insertRecord(record: AutoPilotRecord) {
try {
repository.insert(record)
} catch (t: Throwable) {
Log.d(BadCaseManager.TAG, "-- 插入数据失败 -- msg: $t")
}
}
override suspend fun getUnConsumedRecords(): List<AutoPilotRecord> {
return try {
repository.getAllUnConsumedRecord() ?: emptyList()
} catch (t: Throwable) {
Log.d(BadCaseManager.TAG, "-- 获取所有未消费的数据失败 -- msg: $t")
emptyList()
}
}
override suspend fun deleteRecord(record: AutoPilotRecord) {
try {
repository.deleteRecord(record)
} catch (t: Throwable) {
Log.d(BadCaseManager.TAG, "-- 删除某条记录失败 -- msg: $t")
}
}
override suspend fun upload(map: Map<String, String>): UploadResult? = repository.upload(map)
override suspend fun updateLastModified(timestamp: Long) {
repository.uploadLastModified(timestamp)
}
override suspend fun getLastModified(): Long {
Log.d(BadCaseManager.TAG, " --- 2 ----")
return repository.getLastModified()
}
}

View File

@@ -0,0 +1,176 @@
package com.zhjt.mogo_core_function_devatools.badcase.mvp
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
import android.util.AttributeSet
import android.util.StateSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mogo.eagle.core.data.autopilot.AutoPilotRecordResult
import com.mogo.eagle.core.utilcode.kotlin.*
import com.zhjt.mogo_core_function_devatools.R
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
import kotlinx.android.synthetic.main.layout_badcase_collect.view.*
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
internal class BadCaseView: ConstraintLayout {
@Volatile
private var selectCase: Reason? = null
@Volatile
private var cases: List<Reason>? = null
private val presenter by lazy {
BadCasePresenter()
}
private var onDismiss: (() -> Unit)? = null
private var onSelect:(suspend (reason: Reason) -> Unit)? = null
private val scope by lazy {
lifecycleOwner.lifecycleScope
}
private var record: AutoPilotRecord? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
@SuppressLint("SetTextI18n") constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
LayoutInflater.from(context).inflate(R.layout.layout_badcase_collect, this, true)
background = ColorDrawable(Color.parseColor("#F0151D41"))
isClickable = true
layoutParams = ViewGroup.LayoutParams(960.toPixels().toInt(), 1528.toPixels().toInt())
close?.onClick {
onDismiss?.invoke()
}
cancel?.also {
it.background = shape(solid = Color.parseColor("#3B4577"), radius = 16)
it.onClick {
onDismiss?.invoke()
}
}
ok?.also {
val enabled = gradient(radius = 16.toPixels().toInt(), orientation = GradientDrawable.Orientation.LEFT_RIGHT, centerX = 0.06f, startColor = Color.rgb(35, 146, 252), endColor = Color.rgb(28, 75, 252))
val disabled = gradient(radius = 16.toPixels().toInt(), orientation = GradientDrawable.Orientation.LEFT_RIGHT, centerX = 0.06f, startColor = Color.rgb(24, 71, 129), endColor = Color.rgb(21, 46, 129))
it.background = object : StateListDrawable() {}.also { itx ->
itx.addState(intArrayOf(android.R.attr.state_enabled), enabled)
itx.addState(StateSet.WILD_CARD, disabled)
}
it.onClick {
selectCase?.run {
scope.launch {
onSelect?.invoke(this@run)
}
}
}
}
scope.launchWhenCreated {
time_of_take_over?.text = "接管时间:${SimpleDateFormat("yyyy.MM.dd HH:mm", Locale.getDefault()).format(record?.toLongTime() ?: System.currentTimeMillis())}"
showLoading()
presenter.loadBadCases().also {
cases = it
refresh(it)
}
hideLoading()
}
}
private fun refresh(causes: List<Reason>) {
cases = causes
rv_take_over?.let {
it.layoutManager = LinearLayoutManager(it.context, LinearLayoutManager.VERTICAL, false)
it.adapter = object : RecyclerView.Adapter<BadCaseViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BadCaseViewHolder = BadCaseViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_badcase_item, parent, false))
override fun onBindViewHolder(holder: BadCaseViewHolder, position: Int) {
val cases = cases
if (cases == null || cases.isEmpty()) {
return
}
if (position >= cases.size) {
return
}
val case = cases[position]
holder.bindData(case)
}
override fun getItemCount(): Int = cases?.size ?: 0
}
}
}
private fun showLoading() {
pb?.let {
it.visibility = View.VISIBLE
}
}
private fun hideLoading() {
pb?.let {
it.visibility = View.INVISIBLE
}
}
private inner class BadCaseViewHolder(item: View) : RecyclerView.ViewHolder(item) {
private val check: ImageView = item.findViewById(R.id.check)
private val reason: TextView = item.findViewById(R.id.reason)
init {
check.background = StateListDrawable().also {
it.addState(intArrayOf(android.R.attr.state_selected), ContextCompat.getDrawable(itemView.context, R.drawable.icon_ap_badcase_check))
it.addState(StateSet.WILD_CARD, ContextCompat.getDrawable(itemView.context, R.drawable.icon_ap_badcase_default))
}
}
@SuppressLint("NotifyDataSetChanged")
fun bindData(case: Reason) {
check.isSelected = case.isChecked
reason.text = case.reason ?: ""
if (case.isChecked) {
ok?.isSelected = true
}
itemView.onClick {
case.isChecked = !case.isChecked
selectCase = case
cancelOtherChecked(case)
ok?.isEnabled = hasCheckedItem()
rv_take_over?.adapter?.notifyDataSetChanged()
}
}
private fun hasCheckedItem(): Boolean = cases?.find { it.isChecked } != null
private fun cancelOtherChecked(case: Reason) {
val cases = cases
if (cases == null || cases.isEmpty()) {
return
}
cases.filterNot { it == case }.forEach {
it.isChecked = false
}
}
}
fun register(record: AutoPilotRecord?, onDismiss: () -> Unit, onSelect:suspend (reason: Reason) -> Unit) {
this.record = record
this.onDismiss = onDismiss
this.onSelect = onSelect
}
}

View File

@@ -0,0 +1,24 @@
package com.zhjt.mogo_core_function_devatools.badcase.mvp.biz
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
import kotlinx.coroutines.flow.Flow
internal interface IBadCasePresenter {
suspend fun loadBadCases(): List<Reason>
suspend fun updateLastModified(timestamp: Long)
suspend fun getLastModified(): Long
suspend fun upload(map: Map<String, String>): UploadResult?
suspend fun insertRecord(record: AutoPilotRecord)
suspend fun getUnConsumedRecords(): List<AutoPilotRecord>
suspend fun deleteRecord(record: AutoPilotRecord)
}

View File

@@ -0,0 +1,93 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository
import android.util.Log
import com.zhjt.mogo_core_function_devatools.badcase.BadCaseManager
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.BadCaseDbModel
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.BadCaseNetModel
import com.zhjt.mogo_core_function_devatools.badcase.repository.store.BadCaseStore
import kotlinx.coroutines.flow.Flow
internal class Repository {
private val net by lazy {
BadCaseNetModel()
}
private val db by lazy {
BadCaseDbModel()
}
private val store by lazy {
BadCaseStore
}
suspend fun loadBadCases(): List<Reason> {
return net.get()?.data?.takeIf { it.isNotEmpty() }?.also { store.updateRecords(it) } ?: store.records().takeIf { it.isNotEmpty() } ?: getBuildIn()
}
suspend fun uploadLastModified(timestamp: Long) {
store.updateLastModified(timestamp)
}
suspend fun getLastModified(): Long {
Log.d(BadCaseManager.TAG, " --- 3 ----")
return store.getLastModified()
}
private fun getBuildIn(): List<Reason> {
Log.d(BadCaseManager.TAG, "-- load cases from buildin -- 1 --")
val data = mutableListOf<Reason>()
data += Reason().also {
it.id = "1"
it.reason = "变道有干扰"
}
data += Reason().also {
it.id = "2"
it.reason = "遇红绿灯未停车"
}
data += Reason().also {
it.id = "3"
it.reason = "遇障碍物未停车"
}
data += Reason().also {
it.id = "4"
it.reason = "无法绕行"
}
data += Reason().also {
it.id = "5"
it.reason = "画龙"
}
data += Reason().also {
it.id = "6"
it.reason = "转弯过于靠近路侧"
}
data += Reason().also {
it.id = "7"
it.reason = "无故退出自动驾驶"
}
data += Reason().also {
it.id = "8"
it.reason = "其它"
}
return data
}
suspend fun upload(map: Map<String, String>): UploadResult? {
return net.upload(map)
}
suspend fun insert(record: AutoPilotRecord) {
db.dao().insertRecord(record)
}
suspend fun deleteRecord(record: AutoPilotRecord) {
db.dao().deleteRecord(record)
}
suspend fun getAllUnConsumedRecord(): List<AutoPilotRecord>? {
return db.dao().getAllUnConsumedRecords()
}
}

View File

@@ -0,0 +1,16 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.db
import androidx.room.Database
import androidx.room.RoomDatabase
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.dao.IBadCaseRecordDao
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
@Database(entities = [
AutoPilotRecord::class
],
version = 1,
exportSchema = false)
internal abstract class BadCaseDb : RoomDatabase() {
abstract fun dao(): IBadCaseRecordDao
}

View File

@@ -0,0 +1,12 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.db
import androidx.room.Room
import com.mogo.eagle.core.utilcode.util.Utils
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.dao.IBadCaseRecordDao
internal class BadCaseDbModel {
fun dao(): IBadCaseRecordDao {
return Room.databaseBuilder(Utils.getApp(), BadCaseDb::class.java, "bad-cases").build().dao()
}
}

View File

@@ -0,0 +1,19 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.db.dao
import androidx.room.*
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
@Dao
internal interface IBadCaseRecordDao {
@Transaction
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecord(record: AutoPilotRecord): Long
@Transaction
@Delete
suspend fun deleteRecord(record: AutoPilotRecord): Int
@Query("SELECT * FROM record ORDER BY timestamp ASC")
suspend fun getAllUnConsumedRecords(): List<AutoPilotRecord>?
}

View File

@@ -0,0 +1,89 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.text.SimpleDateFormat
import java.util.*
@Entity(tableName = "record")
internal class AutoPilotRecord {
/**
* 磁盘可用空间(M)
*/
@ColumnInfo(name = "disk_free")
var diskFree: Long = 0
/**
* 采集时长
*/
var duration: Double = 0.0
/**
* 保存的文件名
*/
@ColumnInfo(name = "file_name")
var fileName: String? = ""
/**
* 其他信息,包含错误信息等
*/
var note: String? = ""
/**
* 域控制器定义的bag key
*/
var key: String? = ""
/**
* 采集状态:
* 100 - 采集成功,自动结束
* 101 - 采集成功,收到结束指令
* 200 - 采集失败
* 201 - 采集中
* 300 - 开始采集
*/
var stat: Int = 0
/**
* 任务类型1-badcase采集任务,2-地图数据采集任务
*/
var type: Int = 0
/**
* 任务ID
*/
var id: Int = 0
/**
* 时间戳格式YYYY-MM-DD-hh-mm-ss
*/
@PrimaryKey
var timestamp: String = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date())
/**
* 此次采集数据总大小M
*/
var total: Long? = 0
/**
* 记录此条数据是否已消费
*/
@Volatile
var consumed: Boolean = false
override fun toString(): String {
return "AutoPilotRecord(diskFree=$diskFree, duration=$duration, fileName=$fileName, note=$note, key=$key, stat=$stat, type=$type, id=$id, timestamp='$timestamp', total=$total, consumed=$consumed)"
}
fun toLongTime(): Long = try {
SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).parse(this.timestamp)?.time ?: 0L
} catch (t: Throwable) {
0L
}
}

View File

@@ -0,0 +1,44 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.net
import android.util.Log
import com.mogo.eagle.core.network.MoGoRetrofitFactory
import com.zhjt.mogo_core_function_devatools.badcase.BadCaseManager
import com.zhjt.mogo_core_function_devatools.badcase.api.BadCaseApi
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.consts.BadCaseHost
internal class BadCaseNetModel {
suspend fun get(): BadCaseResponse? = try {
Log.d(BadCaseManager.TAG, "-- load cases from net -- 1 --")
MoGoRetrofitFactory
.getInstance(BadCaseHost.getHost())
.create(BadCaseApi::class.java)
.get()
.takeIf {
val body = it.body()
it.isSuccessful && body != null && (body.code == 0 || body.code == 200)
}
?.body()?.also {
Log.d(BadCaseManager.TAG, "-- load cases from net -- 2 --")
}
} catch (t: Throwable) {
Log.d(BadCaseManager.TAG, "-- load cases from net -- 3 --")
null
}
suspend fun upload(map: Map<String, String>): UploadResult? = try {
MoGoRetrofitFactory
.getInstance(BadCaseHost.getHost())
.create(BadCaseApi::class.java)
.post(map)
.takeIf {
val body = it.body()
return@takeIf it.isSuccessful && (body != null) && (body.code == 0 || body.code == 200)
}
?.body()
} catch (t: Throwable) {
null
}
}

View File

@@ -0,0 +1,109 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.store
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.Serializer
import com.mogo.eagle.core.utilcode.util.Utils
import com.zhjt.mogo_core_function_devatools.badcase.BadCaseManager
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.generated.BadCauses
import com.zhjt.mogo_core_function_devatools.badcase.generated.Cause
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlin.math.log
internal object BadCaseStore {
private val serializer = object : Serializer<BadCauses> {
override val defaultValue: BadCauses
get() = BadCauses.getDefaultInstance()
override suspend fun readFrom(input: InputStream): BadCauses = suspendCancellableCoroutine {
Log.d(BadCaseManager.TAG, "--- readFrom ---")
it.invokeOnCancellation {
Thread.currentThread().interrupt()
}
try {
it.resumeWith(Result.success(BadCauses.parseFrom(input)))
} catch (t: Throwable) {
it.resumeWith(Result.failure(t))
}
}
override suspend fun writeTo(t: BadCauses, output: OutputStream) = suspendCancellableCoroutine<Unit> {
it.invokeOnCancellation {
Thread.currentThread().interrupt()
}
try {
t.writeTo(output)
it.resumeWith(Result.success(Unit))
} catch (t: Throwable) {
it.resumeWith(Result.failure(t))
}
}
}
private val store: DataStore<BadCauses> by lazy {
DataStoreFactory.create(serializer = serializer) { File(Utils.getApp().filesDir, "bad_cases.pb") }
}
suspend fun updateRecords(reasons: List<Reason>): BadCauses {
Log.d(BadCaseManager.TAG, "--- updateRecords ---")
val data = mutableListOf<Cause>()
reasons.forEach { itx ->
data += Cause.newBuilder().let {
it.id = itx.id
it.reason = itx.reason
it.build()
}
}
return store.updateData { itx ->
itx.toBuilder().clearData().addAllData(data).build()
}
}
suspend fun updateLastModified(timestamp: Long): BadCauses {
Log.d(BadCaseManager.TAG, "--- updateLastModified ---")
return store.updateData { itx ->
itx.toBuilder().setLastModified(timestamp).build()
}
}
suspend fun getLastModified(): Long {
Log.d(BadCaseManager.TAG, " --- 4 ----")
return store
.data
.catch {
if (it is IOException) {
emit(BadCauses.getDefaultInstance())
}
}
.map {
it.lastModified
}.firstOrNull() ?: 0L
}
@OptIn(FlowPreview::class)
suspend fun records(): List<Reason> {
Log.d(BadCaseManager.TAG, "-- load cases from pb -- 1 -- ")
val causes = store.data.firstOrNull()
return causes?.dataList?.map {
Reason().also { itx ->
itx.id = it.id
itx.reason = it.reason
}
}?.fold(mutableListOf()) { acc, reason ->
acc += reason
acc
} ?: emptyList()
}
}

View File

@@ -0,0 +1,17 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.zhjt.mogo_core_function_devatools.badcase.generated";
option java_outer_classname = "BadCausesProto";
message BadCauses {
int64 lastModified = 1;
repeated Cause data = 2 ;
}
message Cause {
string id = 1;
string reason = 2;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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"
tools:background="#151D41F0"
tools:layout_height="match_parent"
tools:layout_width="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/close"
android:layout_width="107px"
android:layout_height="107px"
android:layout_marginTop="66px"
android:layout_marginEnd="40px"
android:src="@drawable/icon_close_nor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/blue_block"
android:layout_width="14px"
android:layout_height="50px"
android:layout_marginStart="80px"
android:layout_marginTop="92px"
android:background="#2966EC"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="19px"
android:text="请选择本次接管原因"
android:textColor="#ffffff"
android:textSize="42px"
app:layout_constraintBottom_toBottomOf="@+id/blue_block"
app:layout_constraintStart_toEndOf="@+id/blue_block"
app:layout_constraintTop_toTopOf="@+id/blue_block" />
<TextView
android:id="@+id/time_of_take_over"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="113px"
android:layout_marginTop="10px"
android:textColor="#A7B6F0"
android:textSize="32px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/blue_block"
tools:text="接管时间2021.12.21 14:32" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_take_over"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="72px"
android:overScrollMode="never"
app:layout_constraintBottom_toTopOf="@+id/ok"
android:layout_marginBottom="20px"
app:layout_constraintTop_toBottomOf="@+id/time_of_take_over" />
<TextView
android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="123px"
android:paddingTop="31px"
android:paddingEnd="123px"
android:paddingBottom="31px"
android:text="确认"
android:textColor="#ffffff"
android:textSize="42px"
android:layout_marginStart="100px"
android:layout_marginEnd="100px"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/cancel"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
android:enabled="false"
android:layout_marginBottom="150px"/>
<TextView
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="123px"
android:paddingTop="31px"
android:paddingEnd="123px"
android:paddingBottom="31px"
android:text="取消"
android:textColor="#ffffff"
android:textSize="42px"
android:layout_marginEnd="100px"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/ok"
android:layout_marginBottom="150px"/>
<ProgressBar
android:id="@+id/pb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.Holo.ProgressBar.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</merge>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/dp_10"
android:paddingTop="@dimen/dp_10"
android:gravity="center_vertical">
<ImageView
android:id="@+id/check"
android:layout_width="70px"
android:layout_height="70px"
android:layout_marginStart="113px"/>
<TextView
android:id="@+id/reason"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#ffffff"
android:layout_marginStart="30px"
android:layout_marginEnd="20px"
android:textSize="42px"
android:lines="1"
android:ellipsize="end"
tools:text="转弯过于靠近路侧转弯过于靠近路侧转弯过于靠近路侧转弯过于靠近路侧转弯过于靠近路侧转弯过于靠近路侧转弯过于靠近路侧转弯过于靠近路侧"/>
</LinearLayout>