[FeedBack]反馈代码提交

[feedback]优化ui
This commit is contained in:
renwj
2022-03-17 14:58:59 +08:00
parent 11c6f2cf2b
commit 639950ccc5
50 changed files with 1334 additions and 149 deletions

View File

@@ -3,8 +3,8 @@ package com.mogo.functions.test
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.mogo.eagle.core.data.autopilot.AutoPilotRecordResult
import com.mogo.eagle.core.function.call.devatools.CallerDevaToolsManager
import com.mogo.eagle.core.function.hmi.ui.MoGoHmiFragment
import com.mogo.eagle.core.function.main.MainLauncherActivity
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@@ -14,6 +14,7 @@ import org.junit.runner.RunWith
import record_cache.RecordPanelOuterClass
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.random.Random
@@ -31,7 +32,7 @@ class AutoPilotBadCaseTest {
@ExperimentalCoroutinesApi
@Test
fun showBadCaseEntrance1(): Unit = runBlocking(Dispatchers.Main) {
delay(5000)
ensureMoGoHmiFragmentShow()
var index = 0
(1 until 50)
.map { it }
@@ -61,7 +62,7 @@ class AutoPilotBadCaseTest {
@ExperimentalCoroutinesApi
@Test
fun showBadCaseEntrance2(): Unit = runBlocking(Dispatchers.Main) {
delay(5000)
ensureMoGoHmiFragmentShow()
var index = 0
(1 until 50)
.map { it }
@@ -118,4 +119,29 @@ class AutoPilotBadCaseTest {
.collect()
delay(TimeUnit.HOURS.toMillis(2))
}
private suspend fun ensureMoGoHmiFragmentShow(): MoGoHmiFragment = suspendCancellableCoroutine {
launch.onActivity { itx ->
val executor = Executors.newSingleThreadScheduledExecutor()
executor.scheduleAtFixedRate({
var find =
itx.supportFragmentManager.fragments.find { it is MoGoHmiFragment } as? MoGoHmiFragment
while (find == null) {
find =
itx.supportFragmentManager.fragments.find { it is MoGoHmiFragment } as? MoGoHmiFragment
}
while (!find.isResumed) {
Thread.sleep(500)
}
it.resumeWith(Result.success(find))
try {
Thread.sleep(500)
executor.shutdownNow()
} catch (e: Throwable) {
e.printStackTrace()
}
}, 50, 500, TimeUnit.MILLISECONDS)
}
}
}

View File

@@ -28,7 +28,7 @@ ext {
androidxcardview : "androidx.cardview:cardview:1.0.0",
localbroadcastmanager : "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0",
// flexbox
flexbox : 'com.google.android:flexbox:2.0.1',
flexbox : 'com.google.android.flexbox:flexbox:3.0.0',
// 测试
junit : "junit:junit:4.12",
androidxjunit : "androidx.test.ext:junit:1.1.2",

View File

@@ -35,6 +35,7 @@ import com.zhidao.support.adas.high.AdasManager
import com.zhidao.support.adas.high.AdasOptions
import com.zhidao.support.adas.high.bean.IPCUpgradeInfo
import com.zhidao.support.adas.high.common.Constants
import com.zhidao.support.adas.high.common.Constants.IPC_CONNECTION_STATUS
import com.zhidao.support.adas.high.common.CupidLogUtils
import io.netty.channel.Channel
import java.util.concurrent.TimeUnit
@@ -332,4 +333,11 @@ class MoGoAutopilotProvider :
override fun getGlobalPath() {
AdasManager.getInstance().sendGlobalPathReq()
}
/**
* 车机与工控机是否连上了
*/
override fun isConnected(): Boolean {
return AdasManager.getInstance().ipcConnectionStatus == IPC_CONNECTION_STATUS.CONNECTED
}
}

View File

@@ -82,6 +82,7 @@ dependencies {
implementation rootProject.ext.dependencies.androidxappcompat
implementation rootProject.ext.dependencies.androidxconstraintlayout
implementation rootProject.ext.dependencies.androidxrecyclerview
implementation rootProject.ext.dependencies.flexbox
if (Boolean.valueOf(USE_MAVEN_PACKAGE)) {
implementation rootProject.ext.dependencies.mogoserviceapi

View File

@@ -8,6 +8,7 @@ import com.mogo.eagle.core.data.chain.ChainLogParam
import com.mogo.eagle.core.data.constants.MogoServicePaths
import com.mogo.eagle.core.function.api.devatools.IDevaToolsProvider
import com.zhjt.mogo_core_function_devatools.badcase.BadCaseManager
import com.zhjt.mogo_core_function_devatools.feedback.FeedbackManager
import com.zhjt.mogo_core_function_devatools.logcatch.MogoLogCatchManager
import com.zhjt.mogo_core_function_devatools.trace.TraceManager.Companion.traceManager
import record_cache.RecordPanelOuterClass
@@ -56,8 +57,11 @@ class DevaToolsProvider : IDevaToolsProvider {
BadCaseManager.onReceiveBadCaseRecord(record)
}
override fun showFeedbackWindow(ctx: Context) {
FeedbackManager.showFeedbackWindow(ctx)
}
override fun onDestroy() {
MogoLogCatchManager.onDestroy()
}
}

View File

@@ -4,6 +4,7 @@ import android.transition.AutoTransition
import android.transition.TransitionManager
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.lifecycle.Lifecycle.Event
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
import androidx.lifecycle.LifecycleCoroutineScope
@@ -16,9 +17,10 @@ import com.mogo.eagle.core.function.call.hmi.CallerHmiManager
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
import com.mogo.eagle.core.utilcode.kotlin.lifecycleOwner
import com.mogo.eagle.core.utilcode.kotlin.onClick
import com.mogo.eagle.core.utilcode.kotlin.toast
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.repository.net.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.biz.BadCasePresenter
import com.zhjt.mogo_core_function_devatools.badcase.biz.BadCaseView
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
@@ -36,8 +38,7 @@ internal object BadCaseManager : LifecycleEventObserver {
* 超过此时间case入口自动消失
*/
private val CASE_EXPIRE_DURATION: Long =
TimeUnit.HOURS.toMillis(4)/* TimeUnit.SECONDS.toMillis(10) */
private val CASE_EXPIRE_DURATION: Long = TimeUnit.HOURS.toMillis(4)/* TimeUnit.SECONDS.toMillis(10) */
private var onShow: (() -> Unit)? = null
private var onHide: (() -> Unit)? = null
@@ -190,20 +191,20 @@ internal object BadCaseManager : LifecycleEventObserver {
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
})
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
itx["channel"] = "0"
})
if (uploadResult == null || uploadResult.code != 200) {
ToastUtils.showShort("接管反馈失败")
it.context.toast("上报失败")
} else {
ToastUtils.showShort("接管反馈成功")
it.context.toast("上报成功")
record.consumed = true
withContext(Dispatchers.IO) {
presenter.deleteRecord(record)
@@ -246,14 +247,11 @@ internal object BadCaseManager : LifecycleEventObserver {
}
}
private fun showBadCaseFloat(
onDismiss: () -> Unit,
onSelect: suspend (reason: Reason) -> Unit
) {
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)
BadCaseView(context).also { itx ->
itx.register(record, onDismiss, onSelect)
hideFloat = CallerHmiManager.showFloatWindow("BadCaseFloat", floatView = itx, WindowManager.LayoutParams().also { it.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE})
}
}

View File

@@ -1,28 +0,0 @@
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

@@ -2,7 +2,7 @@ package com.zhjt.mogo_core_function_devatools.badcase.biz
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.repository.net.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.repository.Repository
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
@@ -12,7 +12,7 @@ internal class BadCasePresenter: IBadCasePresenter {
Repository()
}
override suspend fun loadBadCases() = repository.loadBadCases()
override suspend fun loadBadCases(isDriven: Boolean) = repository.loadBadCases(isDriven)
override suspend fun insertRecord(record: AutoPilotRecord) {
try {
@@ -49,4 +49,8 @@ internal class BadCasePresenter: IBadCasePresenter {
Log.d(BadCaseManager.TAG, " --- 2 ----")
return repository.getLastModified()
}
override suspend fun getTaskId(): Int {
return repository.getTaskId()
}
}

View File

@@ -21,7 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.mogo.eagle.core.utilcode.kotlin.*
import com.mogo.eagle.core.utilcode.rv.divider.CommonDividerItemDecoration
import com.zhjt.mogo_core_function_devatools.R
import com.zhjt.mogo_core_function_devatools.badcase.api.entity.BadCaseResponse
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.BadCaseResponse
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
@@ -58,7 +58,7 @@ internal class BadCaseView: ConstraintLayout {
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())
layoutParams = ViewGroup.LayoutParams(960.PX, 1528.PX)
close?.onClick {
onDismiss?.invoke()
}
@@ -69,8 +69,8 @@ internal class BadCaseView: ConstraintLayout {
}
}
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))
val enabled = gradient(radius = 16.PX, 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.PX, 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)
@@ -86,7 +86,7 @@ internal class BadCaseView: ConstraintLayout {
scope.launchWhenCreated {
time_of_take_over?.text = "接管时间: ${SimpleDateFormat("yyyy.MM.dd HH:mm", Locale.getDefault()).format(record?.toLongTime() ?: System.currentTimeMillis())}"
showLoading()
presenter.loadBadCases().also {
presenter.loadBadCases(true).also {
cases = it
refresh(it)
}
@@ -101,7 +101,7 @@ internal class BadCaseView: ConstraintLayout {
it.addItemDecoration(
CommonDividerItemDecoration
.Builder()
.verticalInnerSpace(50.toPixels().toInt())
.verticalInnerSpace(50.PX)
.build())
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))

View File

@@ -1,13 +1,13 @@
package com.zhjt.mogo_core_function_devatools.badcase.biz
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.repository.net.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
internal interface IBadCasePresenter {
suspend fun loadBadCases(): List<BadCaseResponse.Reason>
suspend fun loadBadCases(isDriven: Boolean): List<Reason>
suspend fun updateLastModified(timestamp: Long)
@@ -20,4 +20,6 @@ internal interface IBadCasePresenter {
suspend fun getUnConsumedRecords(): List<AutoPilotRecord>
suspend fun deleteRecord(record: AutoPilotRecord)
suspend fun getTaskId(): Int
}

View File

@@ -3,8 +3,8 @@ package com.zhjt.mogo_core_function_devatools.badcase.repository
import com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.Companion.M_DEVA
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
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.net.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.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
@@ -25,8 +25,8 @@ internal class Repository {
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 loadBadCases(isDriven: Boolean): List<Reason> {
return net.get()?.data?.takeIf { it.isNotEmpty() }?.also { store.updateRecords(it, isDriven) } ?: store.records(isDriven).takeIf { it.isNotEmpty() } ?: getBuildIn(isDriven)
}
suspend fun uploadLastModified(timestamp: Long) {
@@ -38,40 +38,44 @@ internal class Repository {
return store.getLastModified()
}
private fun getBuildIn(): List<Reason> {
private fun getBuildIn(isDriven: Boolean): List<Reason> {
CallerLogger.d("$M_DEVA${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 = "其它"
if (isDriven) {
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 = "其它"
}
} else {
TODO()
}
return data
}
@@ -91,4 +95,8 @@ internal class Repository {
suspend fun getAllUnConsumedRecord(): List<AutoPilotRecord>? {
return db.dao().getAllUnConsumedRecords()
}
suspend fun getTaskId(): Int {
return store.getTaskIdAndIncrement()
}
}

View File

@@ -4,9 +4,9 @@ import com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.Companion.M_
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
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.repository.net.api.BadCaseApi
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.BadCaseResponse
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.badcase.consts.BadCaseHost
internal class BadCaseNetModel {

View File

@@ -1,7 +1,7 @@
package com.zhjt.mogo_core_function_devatools.badcase.api
package com.zhjt.mogo_core_function_devatools.badcase.repository.net.api
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.repository.net.api.entity.BadCaseResponse
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.UploadResult
import retrofit2.Response
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded

View File

@@ -0,0 +1,45 @@
package com.zhjt.mogo_core_function_devatools.badcase.repository.net.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
override fun equals(other: Any?): Boolean {
if (javaClass != other?.javaClass) return false
other as Reason
if (id != other.id) return false
if (reason != other.reason) return false
if (isChecked != other.isChecked) return false
return true
}
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + (reason?.hashCode() ?: 0)
result = 31 * result + isChecked.hashCode()
return result
}
}
}

View File

@@ -1,4 +1,4 @@
package com.zhjt.mogo_core_function_devatools.badcase.api.entity
package com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity
import androidx.annotation.Keep

View File

@@ -7,7 +7,7 @@ import com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.Companion.M_
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
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.Reason
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.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.FlowPreview
@@ -55,15 +55,10 @@ internal object BadCaseStore {
private val store: DataStore<BadCauses> by lazy {
DataStoreFactory.create(serializer = serializer) {
File(
Utils.getApp().filesDir,
"bad_cases.pb"
)
}
DataStoreFactory.create(serializer = serializer) { File(Utils.getApp().filesDir, "bad_cases.pb") }
}
suspend fun updateRecords(reasons: List<Reason>): BadCauses {
suspend fun updateRecords(reasons: List<Reason>, isDriven: Boolean): BadCauses {
CallerLogger.d("$M_DEVA${BadCaseManager.TAG}", "--- updateRecords ---")
val data = mutableListOf<Cause>()
reasons.forEach { itx ->
@@ -74,7 +69,11 @@ internal object BadCaseStore {
}
}
return store.updateData { itx ->
itx.toBuilder().clearData().addAllData(data).build()
if (isDriven) {
itx.toBuilder().clearDrivenData().addAllDrivenData(data).build()
} else {
itx.toBuilder().clearDrivingData().addAllDrivingData(data).build()
}
}
}
@@ -101,10 +100,10 @@ internal object BadCaseStore {
}
@OptIn(FlowPreview::class)
suspend fun records(): List<Reason> {
CallerLogger.d("$M_DEVA${BadCaseManager.TAG}", "-- load cases from pb -- 1 -- ")
suspend fun records(isDriven: Boolean): List<Reason> {
val causes = store.data.firstOrNull()
return causes?.dataList?.map {
val list = if (isDriven) causes?.drivenDataList else causes?.drivingDataList
return list?.map {
Reason().also { itx ->
itx.id = it.id
itx.reason = it.reason
@@ -114,4 +113,18 @@ internal object BadCaseStore {
acc
} ?: emptyList()
}
suspend fun getTaskIdAndIncrement(): Int {
val causes = store.data.firstOrNull()
val ret = causes?.taskId ?: 0
return ret.also {
store.updateData { itx ->
var old = ret
if (old == Int.MAX_VALUE) {
old = 0
}
itx.toBuilder().setTaskId(old + 1).build()
}
}
}
}

View File

@@ -0,0 +1,210 @@
package com.zhjt.mogo_core_function_devatools.feedback
import android.content.Context
import android.text.TextUtils
import android.util.Log
import android.view.WindowManager
import android.widget.TextView
import com.mogo.cloud.passport.MoGoAiCloudClientConfig
import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotIdentifyListener
import com.mogo.eagle.core.function.call.autopilot.CallerAutoPilotManager
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotIdentifyListenerManager
import com.mogo.eagle.core.function.call.hmi.CallerHmiManager
import com.mogo.eagle.core.utilcode.kotlin.onDetach
import com.mogo.eagle.core.utilcode.kotlin.safeCancel
import com.mogo.eagle.core.utilcode.kotlin.scope
import com.mogo.eagle.core.utilcode.kotlin.toast
import com.mogo.eagle.core.utilcode.mogo.toast.TipToast
import com.mogo.eagle.core.utilcode.util.ThreadUtils
import com.zhjt.mogo_core_function_devatools.R
import com.zhjt.mogo_core_function_devatools.badcase.repository.db.entity.AutoPilotRecord
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.badcase.toRecord
import com.zhjt.mogo_core_function_devatools.feedback.biz.FeedBackView
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback.BadCase
import com.zhjt.mogo_core_function_devatools.feedback.biz.impl.FeedbackPresenter
import com.zhjt.mogo_core_function_devatools.feedback.callback.IFeedbackCallback
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import record_cache.RecordPanelOuterClass
import java.lang.IllegalStateException
import kotlin.Result.Companion
internal object FeedbackManager {
const val TAG = "feedback"
private var hideFloat: (() -> Unit)? = null
private val presenter by lazy { FeedbackPresenter() }
@OptIn(ExperimentalCoroutinesApi::class)
private var autoPilotCallback = Channel<AutoPilotRecord>(Channel.RENDEZVOUS)
@Synchronized
get() = if (field.isClosedForReceive || field.isClosedForSend) {
field = Channel(Channel.RENDEZVOUS)
field
} else field
fun showFeedbackWindow(ctx: Context) {
CallerHmiManager.showFloatWindow("FeedbackWindow", FeedBackView(ctx).also { itx ->
itx.registerCallback(object : IFeedbackCallback {
override fun onClose() {
hideFloat?.invoke()
}
override fun onBadCaseItemClicked(reason: Reason) {
val oldData = itx.adapter.data ?: return
if (reason.isChecked) {
return
}
reason.isChecked = true
val badCase = oldData.firstOrNull() as? BadCase
badCase?.reasons?.filterNot { it.id == reason.id }?.forEach {
it.isChecked = false
}
itx.adapter.notifyItemChanged(0)
}
override fun onStartBadCaseRecord(record: TextView) {
if (!CallerAutoPilotManager.isConnected()) {
TipToast.shortTip("请检查车机与域控制器连接是否正常")
return
}
val data = itx.adapter.data ?: return
val badCase = data.firstOrNull() as? BadCase ?: return
val checked = badCase.reasons.find { it.isChecked }
if (checked == null) {
TipToast.shortTip("请选择一个Case")
return
}
record.scope.launch {
val taskId = presenter.getBadCaseTaskId()
val listener = object : IMoGoAutopilotIdentifyListener {
override fun onAutopilotRecordResult(recordPanel: RecordPanelOuterClass.RecordPanel) {
super.onAutopilotRecordResult(recordPanel)
val newRecord = recordPanel.toRecord() ?: return
Log.d(TAG, "-- 收到工控机录制任务回调 -- $recordPanel")
if (newRecord.type == 1 && newRecord.id == taskId) {
if (newRecord.stat == 100 || newRecord.stat == 101) {
Log.d(TAG, "录制Bag完成, 触发结束录制全量日志 ...")
stopRecordLog(newRecord)
launch {
send(newRecord)
}
}
if (newRecord.stat == 300) {
Log.d(TAG, "录制Bag开始, 触发录制全量日志 ...")
startRecordLog(newRecord)
}
}
}
}
CallerAutopilotIdentifyListenerManager.addListener("Feedback", listener)
record.onDetach {
CallerAutopilotIdentifyListenerManager.removeListener("Feedback")
hideFloat = null
}
record.text = "结束录制"
record.setTag(R.id.feed_back_badcase_tag, 1)
record.setTag(R.id.feed_back_badcase_taskid_tag, taskId)
recordBag(1, taskId, 20)
Log.d(TAG, "延时20秒开始....")
delay(20000) //延时20秒
Log.d(TAG, "延时20秒结束....")
stopRecordBag(1, taskId)
upload(record.context, badCase, checked)
}.also {
record.setTag(R.id.feed_back_badcase_job, it)
}
}
override fun onStopBadCaseRecord(record: TextView) {
val data = itx.adapter.data ?: return
val badCase = data.firstOrNull() as? BadCase ?: return
val checked = badCase.reasons.find { it.isChecked } ?: throw IllegalStateException("这种状态不存在")
val taskId = (record.getTag(R.id.feed_back_badcase_taskid_tag) as? Int) ?: throw IllegalStateException("TaskId 不存在")
val oldJob = record.getTag(R.id.feed_back_badcase_job) as? Job
record.scope.launch {
stopRecordBag(1, taskId)
oldJob?.safeCancel()
upload(record.context, badCase, checked)
}
}
})
}, WindowManager.LayoutParams().also { it.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN }).also { hideFloat = it }
}
private fun startRecordLog(record: AutoPilotRecord) {
//val fileName = record.fileName
//TODO zhongchao 添加全量日志开始录制
}
private fun stopRecordLog(newRecord: AutoPilotRecord) {
//val fileName = record.fileName
//TODO zhongchao 添加全量日志结束录制
}
private suspend fun upload(ctx: Context, badCase: BadCase, checked: Reason) {
val result = receive()
val remark = badCase.remark.text
presenter.upload(mutableMapOf<String, String>().also { itx ->
itx["carLicense"] = MoGoAiCloudClientConfig.getInstance().sn
itx["filename"] = result.fileName ?: ""
itx["filesize"] = result.total.toString()
itx["key"] = result.key ?: ""
itx["reason"] = checked.reason ?: ""
itx["duration"] = result.duration.toInt().toString()
itx["timestamp"] = result.timestamp
itx["channel"] = "1"
if (!TextUtils.isEmpty(remark)) {
itx["remark"] = remark.toString()
}
}).also {
if (it == null || it.code != 200) {
ctx.toast("上报失败")
} else {
ctx.toast("上报成功")
hideFloat?.invoke()
}
}
}
private suspend fun send(record: AutoPilotRecord) = autoPilotCallback.send(record)
private suspend fun receive(): AutoPilotRecord = autoPilotCallback.receive()
private suspend fun recordBag(type: Int, id: Int, duration: Int) = suspendCancellableCoroutine<Unit> {
val future = ThreadUtils.getIoPool().submit {
try {
CallerAutoPilotManager.recordPackage(type, id, duration)
it.resumeWith(Result.success(Unit))
} catch (t: Throwable) {
it.resumeWith(Companion.failure(t))
}
}
it.invokeOnCancellation {
try {
future.cancel(true)
} catch (t: Throwable) {
t.printStackTrace()
}
}
}
private suspend fun stopRecordBag(type: Int, id: Int) = suspendCancellableCoroutine<Unit> {
val future = ThreadUtils.getIoPool().submit {
try {
CallerAutoPilotManager.stopRecord(type, id)
it.resumeWith(Result.success(Unit))
} catch (t: Throwable) {
it.resumeWith(Companion.failure(t))
}
}
it.invokeOnCancellation {
try {
future.cancel(true)
} catch (t: Throwable) {
t.printStackTrace()
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.mogo.eagle.core.utilcode.kotlin.*
import com.zhjt.mogo_core_function_devatools.R
import com.zhjt.mogo_core_function_devatools.feedback.biz.adapter.FeedbackAdapter
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback
import com.zhjt.mogo_core_function_devatools.feedback.biz.impl.FeedbackPresenter
import com.zhjt.mogo_core_function_devatools.feedback.callback.IFeedbackCallback
import kotlinx.android.synthetic.main.layout_fb.view.*
internal class FeedBackView : ConstraintLayout {
private var cb: IFeedbackCallback? = null
private val presenter by lazy {
FeedbackPresenter()
}
private val scope by lazy {
lifecycleOwner.lifecycleScope
}
internal val adapter by lazy {
FeedbackAdapter<Feedback>()
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
LayoutInflater.from(context).inflate(R.layout.layout_fb, this, true).also {
observe(arrayOf(ON_DESTROY)) { itx ->
if (itx == ON_DESTROY) {
cb = null
}
}
}
layoutParams = ViewGroup.LayoutParams(960.PX, 1528.PX)
close.onClick {
cb?.onClose()
}
top_mask?.background = gradient(orientation = GradientDrawable.Orientation.TOP_BOTTOM, startColor = Color.parseColor("#151D41"), endColor = Color.parseColor("#05151D41"))
background = ColorDrawable(Color.parseColor("#F0151D41"))
rv?.also {
it.fixGestureConflictForViews(listOf(R.id.et))
it.itemAnimator?.run {
changeDuration = 0
addDuration = 0
moveDuration = 0
removeDuration = 0
}
it.adapter = adapter
it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
loadFeedbackAndRefresh()
}
}
private fun loadFeedbackAndRefresh() {
scope.launchWhenCreated {
showLoading()
presenter.loadFeedBacks().also {
adapter.data = it
}
hideLoading()
}
}
private fun showLoading() {
pb?.visibility = View.VISIBLE
}
private fun hideLoading() {
pb?.visibility = View.GONE
}
fun registerCallback(cb: IFeedbackCallback) {
this.cb = cb
this.adapter.setCallback(cb)
}
}

View File

@@ -0,0 +1,14 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback
internal interface IFeedbackPresenter {
suspend fun loadFeedBacks(): List<Feedback>
suspend fun getBadCaseTaskId(): Int
suspend fun upload(params: Map<String, String>): UploadResult?
}

View File

@@ -0,0 +1,63 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz.adapter
import android.util.Log
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.zhjt.mogo_core_function_devatools.feedback.FeedbackManager
import com.zhjt.mogo_core_function_devatools.feedback.biz.adapter.vh.BadCaseFBViewHolder
import com.zhjt.mogo_core_function_devatools.feedback.biz.adapter.vh.base.FeedbackViewHolder
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback.BadCase
import com.zhjt.mogo_core_function_devatools.feedback.biz.diff.FeedbackDiffCallback
import com.zhjt.mogo_core_function_devatools.feedback.callback.IFeedbackCallback
internal class FeedbackAdapter<T: Feedback>: RecyclerView.Adapter<FeedbackViewHolder<T>>() {
companion object {
const val ITEM_TYPE_BAD_CASE = 0x0101
}
private var cb: IFeedbackCallback? = null
var data: List<T>? = null
@Synchronized
set(value) {
val result = DiffUtil.calculateDiff(FeedbackDiffCallback(field, value))
result.dispatchUpdatesTo(this)
field = value
}
override fun getItemViewType(position: Int): Int {
val data = data ?: return super.getItemViewType(position)
when(val item = data[position]) {
is BadCase -> {
Log.d(FeedbackManager.TAG, "item->$item")
return ITEM_TYPE_BAD_CASE
}
}
return super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedbackViewHolder<T> {
when (viewType) {
ITEM_TYPE_BAD_CASE -> {
return BadCaseFBViewHolder(cb, parent) as FeedbackViewHolder<T>
}
else -> {
throw IllegalStateException("不支持ViewType: $viewType")
}
}
}
override fun onBindViewHolder(holder: FeedbackViewHolder<T>, position: Int) {
val item = data?.get(position) ?: return
holder.onBind(item, position)
}
override fun getItemCount(): Int = data?.size ?: 0
fun setCallback(cb: IFeedbackCallback) {
this.cb = cb
}
}

View File

@@ -0,0 +1,155 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz.adapter.vh
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable.Orientation.LEFT_RIGHT
import android.graphics.drawable.StateListDrawable
import android.text.Selection
import android.text.TextUtils
import android.text.TextUtils.TruncateAt.END
import android.util.StateSet
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.google.android.flexbox.FlexboxLayout
import com.mogo.eagle.core.utilcode.kotlin.*
import com.mogo.eagle.core.utilcode.util.ToastUtils
import com.zhjt.mogo_core_function_devatools.R
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.BadCaseResponse.Reason
import com.zhjt.mogo_core_function_devatools.feedback.biz.adapter.vh.base.FeedbackViewHolder
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback.BadCase
import com.zhjt.mogo_core_function_devatools.feedback.callback.IFeedbackCallback
@SuppressLint("SetTextI18n")
internal class BadCaseFBViewHolder(cb: IFeedbackCallback?, parent: ViewGroup): FeedbackViewHolder<BadCase>(cb,
LayoutInflater
.from(parent.context)
.inflate(R.layout.layout_fb_badcase, parent, false)) {
private val flex by lazy {
itemView.findViewById<FlexboxLayout>(R.id.flex)
}
private val et by lazy {
itemView.findViewById<EditText>(R.id.et)
}
init {
itemView.findViewById<TextView>(R.id.record).also {
it.background = gradient(radius = 16.PX, orientation = LEFT_RIGHT, centerX = 0.06f, startColor = Color.rgb(35, 146, 252), endColor = Color.rgb(28, 75, 252))
it.onClick { _ ->
val flag = (it.getTag(R.id.feed_back_badcase_tag) as? Int) ?: 0
if (flag == 0) {
cb?.onStartBadCaseRecord(it)
} else {
cb?.onStopBadCaseRecord(it)
}
}
}
itemView.findViewById<View>(R.id.et_root).also {
it.background = shape(solid = Color.parseColor("#263869"), radius = 20.PX)
}
val words = itemView.findViewById<TextView>(R.id.words_count)
et.onClick {
et.requestFocus()
et.isFocusable = true
et.isFocusableInTouchMode = true
}
et.watch(
200,
onCountChanged = {
words.spannableText(listOf(it.toString(), "/200"), listOf(Color.parseColor("#5EBFFF"), Color.WHITE))
},
onTextChanged = {
data().remark.text = it?.toString() ?: ""
},
onReachMaxCountAction = {
ToastUtils.showShort("已超过最大字符数")
},
onGetFocus = {
it.background = shape(solid = Color.parseColor("#263869"), radius = 20.PX, stroke = Color.parseColor("#5EBFFF"))
}
)
}
override fun onBind(data: BadCase, position: Int) {
super.onBind(data, position)
flex.refresh(data)
val text = data.remark.text
if (!TextUtils.isEmpty(text)) {
et.setText(text)
Selection.setSelection(et.text, et.text.length)
}
}
private fun FlexboxLayout.refresh(data: BadCase) {
val vh = (tag as? Map<*, *> ?: emptyMap<Reason, ViewHolder>()).toMutableMap()
if (vh.isEmpty()) {
data.reasons.forEach { itx ->
val view = getBadCaseView(context)
val check = view.findViewById<ImageView>(R.id.check)
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))
}
check.isSelected = itx.isChecked
val reason = view.findViewById<TextView>(R.id.reason)
reason.text = itx.reason
vh[itx] = ViewHolder(check, reason)
view.onClick {
cb?.onBadCaseItemClicked(itx)
}
view.layoutParams = FlexboxLayout.LayoutParams(FlexboxLayout.LayoutParams.MATCH_PARENT, FlexboxLayout.LayoutParams.WRAP_CONTENT).also {
it.flexBasisPercent = 0.5f
}
addView(view)
}
tag = vh
} else {
data.reasons.forEach {
(vh[it] as? ViewHolder)?.run {
check.isSelected = it.isChecked
reason.text = it.reason
}
}
}
}
private fun getBadCaseView(context: Context): View {
return LinearLayout(context).also { itx ->
itx.orientation = LinearLayout.HORIZONTAL
itx.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
itx.gravity = Gravity.CENTER_VERTICAL
//ImageView -- check
val check = ImageView(context)
check.layoutParams = LinearLayout.LayoutParams(70.PX, 70.PX)
check.id = R.id.check
itx.addView(check)
//TextView -- Reason
val reason = TextView(context)
reason.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
it.marginStart = 30.PX
it.marginEnd = 20.PX
}
reason.setTextColor(Color.WHITE)
reason.setTextSize(TypedValue.COMPLEX_UNIT_PX, 42.0f)
reason.maxLines = 1
reason.ellipsize = END
reason.id = R.id.reason
itx.addView(reason)
}
}
private data class ViewHolder(val check: ImageView, val reason: TextView)
}

View File

@@ -0,0 +1,22 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz.adapter.vh.base
import android.view.View
import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback
import com.zhjt.mogo_core_function_devatools.feedback.callback.IFeedbackCallback
import java.util.concurrent.atomic.AtomicReference
internal open class FeedbackViewHolder<T: Feedback>(internal val cb: IFeedbackCallback?, view: View): RecyclerView.ViewHolder(view) {
private val data by lazy { AtomicReference<T>() }
fun data(): T {
return data.get()
}
@CallSuper
open fun onBind(data: T, position: Int) {
this.data.set(data)
}
}

View File

@@ -0,0 +1,28 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz.bean
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.BadCaseResponse.Reason
internal sealed class Feedback {
class BadCase(var remark: Remark, var reasons: List<Reason>): Feedback() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BadCase
if (reasons != other.reasons) return false
return true
}
override fun hashCode(): Int {
return reasons.hashCode()
}
}
}
/**
* 记录文本编辑框的状态
* @param text: 文件编辑框中输入的文字
* @param cursorPos: 光标位置
*/
data class Remark(var text: CharSequence = "", var cursorPos: Int = 0)

View File

@@ -0,0 +1,25 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz.diff
import androidx.recyclerview.widget.DiffUtil
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback.BadCase
internal class FeedbackDiffCallback<T: Feedback>(private val oldData: List<T>?, private val newData: List<T>?): DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldData?.size ?: 0
}
override fun getNewListSize(): Int = newData?.size ?: 0
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldData?.takeIf { it.size > oldItemPosition }?.equals(newData?.takeIf { it.size > newItemPosition }) ?: false
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldData?.get(oldItemPosition)
val newItem = newData?.get(newItemPosition)
if (oldItem == null || newItem == null) {
return false
}
return oldItem == newItem
}
}

View File

@@ -0,0 +1,28 @@
package com.zhjt.mogo_core_function_devatools.feedback.biz.impl
import com.zhjt.mogo_core_function_devatools.badcase.biz.BadCasePresenter
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.UploadResult
import com.zhjt.mogo_core_function_devatools.feedback.biz.IFeedbackPresenter
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Feedback.BadCase
import com.zhjt.mogo_core_function_devatools.feedback.biz.bean.Remark
internal class FeedbackPresenter: IFeedbackPresenter {
private val badCase by lazy {
BadCasePresenter()
}
override suspend fun loadFeedBacks(): List<Feedback> = mutableListOf<Feedback>().also {
//添加BadCase数据
it += BadCase(Remark(), badCase.loadBadCases(false))
}
override suspend fun getBadCaseTaskId(): Int {
return badCase.getTaskId()
}
override suspend fun upload(params: Map<String, String>): UploadResult? {
return badCase.upload(params)
}
}

View File

@@ -0,0 +1,28 @@
package com.zhjt.mogo_core_function_devatools.feedback.callback
import android.view.View
import android.widget.TextView
import com.zhjt.mogo_core_function_devatools.badcase.repository.net.api.entity.BadCaseResponse.Reason
internal interface IFeedbackCallback {
/**
* 点击关闭弹窗按钮时回调
*/
fun onClose()
/**
* BadCase某一条目被点击了
*/
fun onBadCaseItemClicked(reason: Reason)
/**
* 点击开始录制
*/
fun onStartBadCaseRecord(record: TextView)
/**
* 点击停止录制
*/
fun onStopBadCaseRecord(record: TextView)
}

View File

@@ -6,7 +6,9 @@ option java_outer_classname = "BadCausesProto";
message BadCauses {
int64 lastModified = 1;
repeated Cause data = 2 ;
repeated Cause drivenData = 2 ; //被动触发BadCase数据
int32 taskId = 3;
repeated Cause drivingData = 4; //主动触发BadCase数据
}
message Cause {

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="0px" android:height="50px" />
</shape>

View File

@@ -103,7 +103,7 @@
android:id="@+id/pb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.Holo.ProgressBar.Large"
style="@android:style/Widget.Holo.Light.ProgressBar.Inverse"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -2,15 +2,16 @@
<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_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
android:gravity="center_vertical"
tools:ignore="HardcodedText,PxUsage,ContentDescription">
<ImageView
android:id="@+id/check"
android:layout_width="70px"
android:layout_height="70px"
android:layout_marginStart="113px"/>
android:layout_marginStart="113px" />
<TextView
android:id="@+id/reason"

View File

@@ -0,0 +1,48 @@
<?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:layout_width="match_parent"
tools:layout_height="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="120px"/>
<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"
tools:ignore="PxUsage" />
<ProgressBar
android:id="@+id/pb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.Holo.Light.ProgressBar.Inverse"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"/>
<View
android:id="@+id/top_mask"
android:layout_width="0dip"
android:layout_height="80px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</merge>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="HardcodedText,PxUsage">
<LinearLayout
android:id="@+id/ll_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center_vertical"
android:layout_marginTop="92px"
android:layout_marginStart="80px"
android:orientation="horizontal">
<View
android:layout_width="14px"
android:layout_height="50px"
android:background="#2966EC"
android:layout_marginEnd="19px"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="42px"
android:textColor="#ffffff"
android:text="Case上报"/>
</LinearLayout>
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/flex"
android:layout_width="0dip"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_title"
app:flexWrap="wrap"
app:flexDirection="row"
app:dividerDrawable="@drawable/flex_divider"
app:showDivider="middle"
app:justifyContent="flex_start"
android:layout_marginStart="80px"
android:layout_marginTop="54px" />
<TextView
android:id="@+id/supply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/flex"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="80px"
android:layout_marginTop="86px"
android:text="补充"
android:textColor="#ffffff"
android:textSize="36px" />
<LinearLayout
android:id="@+id/et_root"
android:layout_width="0dip"
android:layout_height="396px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/supply"
app:layout_constraintEnd_toEndOf="parent"
android:paddingTop="30px"
android:paddingStart="25px"
android:paddingEnd="45px"
android:layout_marginStart="75px"
android:layout_marginEnd="95px"
android:layout_marginTop="32px"
android:orientation="vertical">
<EditText
android:id="@+id/et"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:background="@null"
android:focusable="false"
android:gravity="start"
android:focusableInTouchMode="true"
android:inputType="textMultiLine"
android:textSize="36px"
android:textColorHint="#4CFFFFFF"
android:hint="Case细节描述"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/words_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28px"
android:layout_gravity="end"
tools:text="126/200"/>
</LinearLayout>
<TextView
android:id="@+id/record"
android:layout_width="0px"
android:layout_height="126px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="75px"
android:layout_marginEnd="95px"
android:layout_marginTop="40px"
android:textColor="#ffffff"
android:textSize="42px"
android:gravity="center"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@+id/et_root"
android:text="录制Case"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="feed_back_badcase_tag" type="id" />
<item name="feed_back_badcase_taskid_tag" type="id" />
<item name="feed_back_badcase_job" type="id" />
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -169,6 +169,10 @@ class WarningFloat {
this.config.isEnqueue = enqueue
}
fun softInputMode(mode: Int) = apply {
this.config.softInputMode = mode
}
/**
* 创建浮窗包括Activity浮窗和系统浮窗如若系统浮窗无权限先进行权限申请
*/

View File

@@ -56,6 +56,7 @@ internal class WarningFloatWindowHelper(
// 获取 WindowManager
windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
params = WindowManager.LayoutParams().apply {
// 设置窗口类型为应用子窗口和PopupWindow同类型
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
format = PixelFormat.RGBA_8888
@@ -73,6 +74,10 @@ internal class WarningFloatWindowHelper(
x = config.locationPair.first
y = config.locationPair.second
}
//添加软键盘展示模式
if (config.softInputMode != 0) {
softInputMode = config.softInputMode
}
}
}
@@ -89,6 +94,7 @@ internal class WarningFloatWindowHelper(
// 为了避免创建的时候闪一下我们先隐藏视图不能直接设置GONE否则定位会出现问题
floatingView.visibility = View.INVISIBLE
// 将frameLayout添加到系统windowManager中
windowManager.addView(frameLayout, params)
// 在浮窗绘制完成的时候,设置初始坐标、执行入场动画

View File

@@ -19,6 +19,9 @@ data class WarningNotificationConfig(
var layoutId: Int? = null,
var layoutView: View? = null,
// 软键盘模式
var softInputMode: Int = 0,
// 当前浮窗的tag
var floatTag: String? = null,
// 是否正在执行动画

View File

@@ -6,6 +6,7 @@ import android.text.TextUtils
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import android.view.animation.OvershootInterpolator
import androidx.lifecycle.lifecycleScope
import com.alibaba.android.arouter.facade.annotation.Route
@@ -80,8 +81,6 @@ class MoGoHmiFragment : MvpFragment<MoGoWarningContract.View?, WaringPresenter?>
private var toolsView: AutoPilotAndCheckView? = null
private var autoPilotToolsFloat: WarningFloat.Builder? = null
// 检测、自动驾驶速度设置
private var toolsViewFloat: WarningFloat.Builder? = null
@@ -223,6 +222,11 @@ class MoGoHmiFragment : MvpFragment<MoGoWarningContract.View?, WaringPresenter?>
override fun showDebugPanelView() {
toggleDebugView()
}
override fun showFeedbackView() {
dismissToolsFloatView()
CallerDevaToolsManager.showFeedbackView(it)
}
})
}
toolsViewFloat = WarningFloat.with(it)
@@ -874,7 +878,8 @@ class MoGoHmiFragment : MvpFragment<MoGoWarningContract.View?, WaringPresenter?>
}
override fun showBadCaseFloat(tag: String, floatView: View): () -> Unit {
override fun showFloatWindow(tag: String, floatView: View, attrs: LayoutParams?): () -> Unit {
var floatWindow: WarningFloat.Builder?
WarningFloat.with(context ?: Utils.getApp())
.setTag(tag)
.setLayout(floatView)
@@ -884,7 +889,7 @@ class MoGoHmiFragment : MvpFragment<MoGoWarningContract.View?, WaringPresenter?>
.setAnimator(object : DefaultAnimator() {
override fun enterAnim(
view: View,
params: WindowManager.LayoutParams,
params: LayoutParams,
windowManager: WindowManager,
sidePattern: SidePattern
): Animator? =
@@ -895,7 +900,7 @@ class MoGoHmiFragment : MvpFragment<MoGoWarningContract.View?, WaringPresenter?>
override fun exitAnim(
view: View,
params: WindowManager.LayoutParams,
params: LayoutParams,
windowManager: WindowManager,
sidePattern: SidePattern
): Animator? =
@@ -904,17 +909,20 @@ class MoGoHmiFragment : MvpFragment<MoGoWarningContract.View?, WaringPresenter?>
})
.addWarningStatusListener(object : IMoGoWarningStatusListener {
override fun onDismiss() {
autoPilotToolsFloat = null
floatWindow = null
}
})
.also {
autoPilotToolsFloat = it
floatWindow = it
if (((attrs?.softInputMode?: 0) and LayoutParams.SOFT_INPUT_MASK_ADJUST) != 0) {
it.softInputMode(attrs!!.softInputMode)
}
}
.show()
return {
autoPilotToolsFloat?.let {
floatWindow?.let {
WarningFloat.dismiss(it.config.floatTag, false)
autoPilotToolsFloat = null
floatWindow = null
}
}
}

View File

@@ -13,10 +13,10 @@ import com.mogo.eagle.core.function.call.autopilot.CallerAutoPilotManager
import com.mogo.eagle.core.function.call.autopilot.CallerAutoPilotStatusListenerManager
import com.mogo.eagle.core.function.hmi.R
import com.mogo.eagle.core.function.hmi.ui.utils.KeyBoardUtil
import com.mogo.eagle.core.utilcode.kotlin.onClick
import com.mogo.eagle.core.utilcode.mogo.AppIdentityModeUtils
import com.mogo.eagle.core.utilcode.util.ToastUtils
import kotlinx.android.synthetic.main.view_auto_pilot_check.view.*
import kotlinx.android.synthetic.main.view_check_system.view.*
/**
* @author ChenFufeng
@@ -85,6 +85,9 @@ class AutoPilotAndCheckView @JvmOverloads constructor(
ivDebugPanel.setOnClickListener {
clickListener?.showDebugPanelView()
}
ivDebugFeedback.onClick {
clickListener?.showFeedbackView()
}
etInputSpeed.setOnTouchListener { v, _ ->
if (!connectStatus) {
ToastUtils.showShort("设置车速失败,请启动域控制器")
@@ -176,5 +179,6 @@ class AutoPilotAndCheckView @JvmOverloads constructor(
fun go2CheckPage()
fun onClose(v: View)
fun showDebugPanelView()
fun showFeedbackView()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -67,8 +67,7 @@
android:background="@drawable/debug_icon_nor"
app:layout_constraintStart_toEndOf="@id/viewCheckStatus"
app:layout_constraintTop_toTopOf="@id/viewCheckStatus"
android:layout_marginStart="142px"
/>
android:layout_marginStart="142px" />
<TextView
android:id="@+id/tvDebug"
@@ -82,6 +81,27 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivDebugPanel" />
<ImageView
android:id="@+id/ivDebugFeedback"
android:layout_width="150px"
android:layout_height="150px"
android:background="@drawable/debug_icon_feedback"
app:layout_constraintStart_toEndOf="@id/ivDebugPanel"
app:layout_constraintTop_toTopOf="@id/viewCheckStatus"
android:layout_marginStart="142px" />
<TextView
android:id="@+id/tvDebugFeedback"
android:layout_width="128px"
android:layout_height="42px"
android:layout_marginStart="740px"
android:layout_marginTop="23px"
android:text="@string/debug_panel_fb"
android:textColor="@color/color_FFA7B6F0"
android:textSize="32px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivDebugFeedback" />
<View
android:layout_width="14px"
android:layout_height="50px"

View File

@@ -21,6 +21,7 @@
<string name="check_vehicle_detection">车辆检测</string>
<string name="debug_panel">调试面板</string>
<string name="debug_panel_fb">反馈</string>
<string name="check_vehicle_speed_setting">车速设置</string>
<string name="check_system_operation">系统运行</string>
<string name="check_system_shut_down">关机</string>

View File

@@ -135,5 +135,8 @@ interface IMoGoAutopilotProvider : IMoGoFunctionServerProvider {
*/
fun getGlobalPath()
/**
* 域控制器是否连上了
*/
fun isConnected(): Boolean
}

View File

@@ -1,5 +1,6 @@
package com.mogo.eagle.core.function.api.devatools
import android.content.Context
import android.view.View
import com.mogo.eagle.core.data.chain.ChainLogParam
import com.mogo.eagle.core.function.api.base.IMoGoFunctionServerProvider
@@ -33,4 +34,9 @@ interface IDevaToolsProvider : IMoGoFunctionServerProvider {
* 当工控机回调时调用
*/
fun onReceiveBadCaseRecord(record: RecordPanelOuterClass.RecordPanel)
/**
* 展示反馈页面
*/
fun showFeedbackWindow(ctx: Context)
}

View File

@@ -1,6 +1,7 @@
package com.mogo.eagle.core.function.api.hmi.warning
import android.view.View
import android.view.WindowManager.LayoutParams
import com.mogo.eagle.core.data.enums.WarningDirectionEnum
import com.mogo.eagle.core.data.notice.NoticeNormalData
import com.mogo.eagle.core.data.notice.NoticeTrafficStylePushData
@@ -212,7 +213,7 @@ interface IMoGoWaringProvider : IMoGoHmiViewProxy {
* @param tag: 唯一标识
* @return 触发消失时回调
*/
fun showBadCaseFloat(tag: String = "BadCaseFloat", floatView: View): () -> Unit
fun showFloatWindow(tag: String = "BadCaseFloat", floatView: View, attrs: LayoutParams?): () -> Unit
}

View File

@@ -73,6 +73,13 @@ object CallerAutoPilotManager {
providerApi?.recordPackage(type, id, duration)
}
/**
* 停止录制bag包
*/
fun stopRecord(type: Int, id: Int) {
providerApi?.stopRecord(type, id)
}
fun setEnableLog(isEnableLog: Boolean) {
providerApi?.setEnableLog(isEnableLog)
}
@@ -124,4 +131,11 @@ object CallerAutoPilotManager {
fun setControlAutopilotCarAuto(isEnable: Boolean) {
providerApi?.setControlAutopilotCarAuto(isEnable)
}
/**
* 车机与工控机是否连上了
*/
fun isConnected(): Boolean {
return providerApi?.isConnected() ?: false
}
}

View File

@@ -1,5 +1,7 @@
package com.mogo.eagle.core.function.call.devatools
import android.content.Context
import android.view.ContextMenu
import android.view.View
import com.mogo.eagle.core.data.constants.MogoServicePaths
import com.mogo.eagle.core.data.chain.ChainLogParam
@@ -64,4 +66,11 @@ object CallerDevaToolsManager {
fun onReceiveBadCaseRecord(record: RecordPanelOuterClass.RecordPanel) {
devaToolsProviderApi?.onReceiveBadCaseRecord(record)
}
/**
* 展示反馈界面
*/
fun showFeedbackView(ctx: Context) {
devaToolsProviderApi?.showFeedbackWindow(ctx)
}
}

View File

@@ -1,6 +1,7 @@
package com.mogo.eagle.core.function.call.hmi
import android.view.View
import android.view.WindowManager.LayoutParams
import com.alibaba.android.arouter.launcher.ARouter
import com.mogo.eagle.core.data.constants.MoGoFragmentPaths
import com.mogo.eagle.core.data.enums.WarningDirectionEnum
@@ -289,10 +290,10 @@ object CallerHmiManager : CallerBase() {
}
/**
* 展示BadCase浮层
* 展示浮层
*/
fun showBadCaseFloat(tag: String = "BadCaseFloat", floatView: View): (() -> Unit)? {
return waringProviderApi?.showBadCaseFloat(tag, floatView)
fun showFloatWindow(tag: String, floatView: View, attrs: LayoutParams? = null): (() -> Unit)? {
return waringProviderApi?.showFloatWindow(tag, floatView, attrs)
}
/**

View File

@@ -2,22 +2,41 @@ package com.mogo.eagle.core.utilcode.kotlin
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.text.TextUtils
import android.text.*
import android.text.style.ForegroundColorSpan
import android.util.TypedValue
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.EditText
import android.widget.PopupWindow
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.IntRange
import androidx.core.view.ViewCompat
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doBeforeTextChanged
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import androidx.lifecycle.Lifecycle.Event
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
import androidx.recyclerview.widget.RecyclerView
import com.mogo.eagle.core.utilcode.reminder.Reminder
import com.mogo.eagle.core.utilcode.reminder.api.impl.PopupWindowReminder
import com.mogo.eagle.core.utilcode.util.ClickUtils
import com.mogo.eagle.core.utilcode.util.R
import com.mogo.eagle.core.utilcode.util.Utils
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.lang.IllegalStateException
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.math.abs
val <T: View> T.lifecycleOwner: LifecycleOwner
get() = getTag(R.id.view_lifecycle_owner) as? LifecycleOwner ?: object : LifecycleOwner, LifecycleEventObserver {
@@ -64,6 +83,9 @@ val <T: View> T.lifecycleOwner: LifecycleOwner
setTag(R.id.view_lifecycle_owner, it)
}
val <T: View> T.scope
get() = lifecycleOwner.lifecycleScope
fun View.onClick(block: (View) -> Unit) {
this.setOnClickListener {
if (ClickUtils.isClickTooFrequent(this)) {
@@ -76,6 +98,9 @@ fun View.onClick(block: (View) -> Unit) {
val <T: Context> T.lifeCycleScope: LifecycleCoroutineScope
get() = (this as? LifecycleOwner)?.lifecycleScope ?: ProcessLifecycleOwner.get().lifecycleScope
val <T: Context> T.lifeCycleOwner: LifecycleOwner
get() = (this as? LifecycleOwner) ?: ProcessLifecycleOwner.get()
fun <T: View> T.observe(target: Array<Event>, block: ((event: Event) -> Unit) ?= null) {
block?.let {
this.lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
@@ -88,6 +113,18 @@ fun <T: View> T.observe(target: Array<Event>, block: ((event: Event) -> Unit) ?=
}
}
fun <T: View> T.onDetach(block: (() -> Unit)? = null) {
block?.also {
this.lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Event) {
if (event == ON_DESTROY) {
it.invoke()
}
}
})
}
}
fun Job.safeCancel(cause: String? = null) = try {
this.cancel(if (TextUtils.isEmpty(cause)) null else CancellationException(cause))
} catch (t: Throwable) { t.printStackTrace() }
@@ -104,8 +141,9 @@ fun <T: Any> Deferred<T>.safeCancel(cause: String? = null) = try {
this.cancel(if (TextUtils.isEmpty(cause)) null else CancellationException(cause))
} catch (t: Throwable) { t.printStackTrace() }
fun shape(@ColorInt solid: Int = Color.TRANSPARENT, @ColorInt stroke: Int = Color.TRANSPARENT, @IntRange(from = 0) strokeWidth: Int = 0, @IntRange(from = 0) radius: Int = 0, radii: FloatArray = FloatArray(8).apply { Arrays.fill(this, radius.toFloat()) }, shape: Int = GradientDrawable.RECTANGLE): Drawable {
fun shape(@ColorInt solid: Int = Color.TRANSPARENT, @ColorInt stroke: Int = Color.TRANSPARENT, @IntRange(from = 0) strokeWidth: Int = 0, @IntRange(from = 0) radius: Int = 0, radii: FloatArray = FloatArray(8).apply { Arrays.fill(this, radius.toFloat()) }, shape: Int = GradientDrawable.RECTANGLE, width: Int = 0, height: Int = 0): Drawable {
val drawable = GradientDrawable()
drawable.setSize(width, height)
drawable.shape = shape
drawable.gradientType = GradientDrawable.LINEAR_GRADIENT
drawable.setColor(solid)
@@ -115,7 +153,7 @@ fun shape(@ColorInt solid: Int = Color.TRANSPARENT, @ColorInt stroke: Int = Colo
}
fun gradient(shape: Int = GradientDrawable.RECTANGLE, @IntRange(from = 0) radius: Int = 0, radii: FloatArray = FloatArray(8).apply { Arrays.fill(this, radius.toFloat()) }, gradientType: Int = GradientDrawable.LINEAR_GRADIENT, orientation: GradientDrawable.Orientation = GradientDrawable.Orientation.TOP_BOTTOM, centerX: Float = 0.5f, centerY: Float = 0.5f, @ColorInt startColor: Int, @ColorInt centerColor: Int = startColor, @ColorInt endColor: Int): Drawable{
val drawable = GradientDrawable(orientation, intArrayOf(endColor, centerColor, startColor))
val drawable = GradientDrawable(orientation, intArrayOf(startColor, centerColor, endColor))
drawable.shape = shape
drawable.gradientType = gradientType
drawable.orientation = orientation
@@ -124,7 +162,146 @@ fun gradient(shape: Int = GradientDrawable.RECTANGLE, @IntRange(from = 0) radius
return drawable
}
fun Int.toPixels(context: Context? = null): Float {
val ctx = context ?: Utils.getApp()
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, this.toFloat(), ctx.resources.displayMetrics)
val Int.PX: Int
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, this.toFloat(), Utils.getApp().resources.displayMetrics).toInt()
val Int.SP: Int
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Utils.getApp().resources.displayMetrics).toInt()
fun <T: EditText> T.watch(@IntRange(from = 1) maxCharCount: Int, onCountChanged:((count: Int) -> Unit)? = null, onTextChanged:((editable: Editable?) -> Unit)? = null, onReachMaxCountAction:((text: T) -> Unit)? = null, onGetFocus:((text: T) -> Unit)? = null) {
doAfterTextChanged { itx ->
onTextChanged?.invoke(itx)
if (itx == null || itx.isEmpty()) {
onCountChanged?.invoke(0)
return@doAfterTextChanged
}
val length = itx.length
if (length > maxCharCount) {
text = itx.delete(maxCharCount, length)
Selection.setSelection(text, maxCharCount)
onCountChanged?.invoke(maxCharCount)
onReachMaxCountAction?.invoke(this)
} else {
onCountChanged?.invoke(length)
}
}
doBeforeTextChanged { _, _, _, _ ->
onGetFocus?.invoke(this)
}
}
fun <T: TextView> T.spannableText(parts: List<CharSequence>, colors: List<Int>) {
if (parts.isEmpty() || colors.isEmpty() || parts.size != colors.size) {
return
}
val ssb = SpannableStringBuilder()
parts.forEachIndexed { index, sequence ->
val text = SpannableString(sequence)
text.setSpan(ForegroundColorSpan(colors[index]), 0, sequence.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
ssb.append(text)
}
text = ssb
}
fun RecyclerView.fixGestureConflictForViews(ids: List<Int> = emptyList()) {
val fixer = GestureConflictFixer(ids)
addOnItemTouchListener(fixer)
addOnScrollListener(fixer)
}
private class GestureConflictFixer(private val ids: List<Int>) : RecyclerView.OnScrollListener(), RecyclerView.OnItemTouchListener {
private var scrollState = RecyclerView.SCROLL_STATE_IDLE
private var scrollPointerId = -1
private var initialTouchX = 0
private var initialTouchY = 0
private var dx = 0
private var dy = 0
private var intercpted = false
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
when (e.actionMasked) {
MotionEvent.ACTION_DOWN -> {
scrollPointerId = e.getPointerId(0)
initialTouchX = (e.x + 0.5f).toInt()
initialTouchY = (e.y + 0.5f).toInt()
intercpted = computeIntercepted(initialTouchX, initialTouchY, rv, ids)
}
MotionEvent.ACTION_POINTER_DOWN -> {
val actionIndex = e.actionIndex
scrollPointerId = e.getPointerId(actionIndex)
initialTouchX = (e.getX(actionIndex) + 0.5f).toInt()
initialTouchY = (e.getY(actionIndex) + 0.5f).toInt()
}
MotionEvent.ACTION_MOVE -> {
val index = e.findPointerIndex(scrollPointerId)
if (index >= 0 && scrollState != RecyclerView.SCROLL_STATE_DRAGGING) {
val x = (e.getX(index) + 0.5f).toInt()
val y = (e.getY(index) + 0.5f).toInt()
dx = x - initialTouchX
dy = y - initialTouchY
}
}
MotionEvent.ACTION_UP -> {
intercpted = false
}
}
return false
}
private fun computeIntercepted(x: Int, y: Int, rv: RecyclerView, ids: List<Int>): Boolean {
return ids.takeIf { it.isNotEmpty() }?.find {
val out = Rect()
rv.findViewById<View>(it).getGlobalVisibleRect(out)
out.contains(x, y)
}?.let { it != View.NO_ID } ?: false
}
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
val oldState = scrollState
scrollState = newState
if (oldState == RecyclerView.SCROLL_STATE_IDLE && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
recyclerView.layoutManager?.let {
if (intercpted) {
recyclerView.stopScroll()
}
}
}
}
}
fun Context.toast(text: CharSequence, duration: Long = 2, unit: TimeUnit = SECONDS) {
val activity = (this as? FragmentActivity) ?: throw IllegalStateException("please use Activity to trigger toast show.")
activity.lifeCycleScope.launchWhenResumed {
val pop = PopupWindow(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT).also {
it.isOutsideTouchable = false
it.isTouchable = false
it.isFocusable = false
it.setBackgroundDrawable(shape(solid = Color.parseColor("#99000000"), radius = 32.PX))
}
val tv = TextView(this@toast)
tv.setTextColor(Color.WHITE)
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, 56.0f)
tv.setPaddingRelative(114.PX, 61.PX, 114.PX, 61.PX)
tv.text = text
pop.contentView = tv
val reminder = object : PopupWindowReminder(pop) {
override fun show() {
pop.showAtLocation(activity.window.decorView, Gravity.CENTER, 0, 0)
lifecycleOwner().lifecycleScope.launch {
delay(unit.toMillis(duration))
hide()
}
}
override fun isOverride(): Boolean = true
}
Reminder.enqueue(activity.lifeCycleOwner, reminder)
}
}