Author SHA1 Message Date
lvmeng 59b3ad72fe 优化人脸识别显示形状 2026-02-11 17:00:08 +08:00
lvmeng 462755824d 优化 2026-02-11 15:00:27 +08:00
lvmeng 0c990ea7ee 打开增加检查网络情况 2026-02-11 14:48:30 +08:00
lvmeng ac30d95812 优化人脸识别区域 2026-02-10 09:48:26 +08:00
lvmeng 23546a967b 首页人脸识别区域改为圆形 2026-02-09 18:47:53 +08:00
lvmeng f227d15108 菜品信息查询接口增加deviceType字段 2026-02-06 11:20:58 +08:00
lvmeng e2c0ed35ec 优化识别及增加扫码枪逻辑 2026-02-04 18:00:46 +08:00
lvmeng 7596924d75 优化 2026-02-03 15:00:19 +08:00
lvmeng 6e7eb80715 优化 2026-01-26 15:53:43 +08:00
lvmeng 50d12de534 优化 2026-01-14 10:38:09 +08:00
lvmeng e86db688e1 优化 2026-01-13 14:26:18 +08:00
lvmeng c38bead8a3 优化 2026-01-13 09:30:12 +08:00
lvmeng ebc3d856d6 优化 2026-01-09 18:38:55 +08:00
lvmeng a6fa5f153a 优化 2026-01-09 16:10:57 +08:00
lvmeng a0452e985a 优化不足一份的识别问题 2026-01-09 11:59:24 +08:00
lvmeng a79f7db867 优化 2026-01-08 19:36:31 +08:00
马增飞 fce721bfc3 优化 2026-01-08 17:31:13 +08:00
马增飞 3cccba4f08 优化 2026-01-08 17:30:03 +08:00
lvmeng 2a55ddb617 采集向量优化调试 2026-01-08 15:02:44 +08:00
lvmeng 378e6088e7 采集向量数据接口调试 2026-01-06 18:49:33 +08:00
lvmeng fc04db9268 增量人脸查询优化 2025-12-31 11:54:17 +08:00
lvmeng 0d2a38391c 增量分页优化 2025-12-29 14:17:57 +08:00
lvmeng 7374f279b4 增量分页优化 2025-12-29 14:08:04 +08:00
lvmeng bb4230419a 增量接口时间戳来源于全量查询时间 2025-12-26 14:12:18 +08:00
lvmeng 5c7f222a0a 修改接口地址 2025-12-26 13:50:28 +08:00
lvmeng dfb734e25d 营养数据计算优化;人脸增加数据处理;其它优化; 2025-12-25 18:57:26 +08:00
lvmeng f080a8b73c 优化识别后未离开多次切换模式导致的不再识别问题 2025-12-22 14:18:46 +08:00
lvmeng 960b429e69 优化 2025-12-19 17:34:31 +08:00
lvmeng af56fb4aeb 优化 2025-12-19 11:10:56 +08:00
lvmeng ab3109f36a 支付完成倒计时3秒返回首页;秤默认置零取消,打开直接识别物品;人脸识别图片显示拉伸优化;设置页面切换是否收费模式首页逻辑处理; 2025-12-18 18:25:15 +08:00
lvmeng 1d0e1a62e2 增加消费码支付功能、支付逻辑优化、金额问题优化、订单绑定优化 2025-12-12 17:50:51 +08:00
lvmeng a0847d21b7 调试优化 2025-12-11 18:54:57 +08:00
lvmeng 6e318f8c82 支付调试优化 2025-12-10 17:56:24 +08:00
lvmeng 88d7530568 t调试 2025-12-09 18:38:26 +08:00
lvmeng 086845dd16 营养接口调试、支付接口联调 2025-12-08 18:06:48 +08:00
lvmeng 04e318f47b 增加支付接口相关逻辑 2025-12-05 18:03:31 +08:00
lvmeng 8899cae323 接口调试、增加人脸识别后主副屏支付金额逻辑、其它优化 2025-12-03 18:40:47 +08:00
lvmeng 29188c5944 接口联调 2025-11-28 18:46:41 +08:00
lvmeng 9bcd7c9983 人脸识别支付流程 2025-11-28 09:13:29 +08:00
lvmeng 88179e27b7 首页、支付功能 2025-11-26 18:57:53 +08:00
lvmeng 60b712f216 结算页面、设置页面 2025-11-19 18:35:53 +08:00
196 changed files with 15646 additions and 3575 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-07T10:55:54.677927600Z">
<DropdownSelection timestamp="2026-02-06T02:13:05.230327500Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.56:5555;connection=2687c52a" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.170:5555;connection=1ec19d1e" />
</handle>
</Target>
</DropdownSelection>
+5
View File
@@ -108,12 +108,17 @@ dependencies {
implementation(libs.pytorch.android)
implementation(libs.pytorch.android.torchvision)
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
// implementation("io.objectbox:objectbox-fulltext:5.0.1")
implementation(libs.androidx.recyclerview)
implementation("org.greenrobot:eventbus:3.2.0")
}
apply(plugin = "io.objectbox")
+40 -12
View File
@@ -5,7 +5,7 @@
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "4:1528837175750321569",
"lastPropertyId": "11:7949771231314213889",
"name": "Food",
"properties": [
{
@@ -14,22 +14,47 @@
"type": 6,
"flags": 1
},
{
"id": "2:340951913928211738",
"name": "name",
"type": 9
},
{
"id": "3:5484846939472684412",
"name": "foodIdx",
"type": 5
},
{
"id": "4:1528837175750321569",
"name": "foodVector",
"indexId": "1:6010838397086628487",
"type": 28,
"flags": 8
},
{
"id": "5:5561277398349179032",
"name": "collectId",
"type": 9
},
{
"id": "6:8313847979709019172",
"name": "foodId",
"type": 9
},
{
"id": "7:1375536603452792462",
"name": "foodName",
"type": 9
},
{
"id": "8:7942065542192169896",
"name": "version",
"type": 9
},
{
"id": "9:4394472487596452023",
"name": "otherField",
"type": 9
},
{
"id": "10:5704890122356491756",
"name": "createTime",
"type": 9
},
{
"id": "11:7949771231314213889",
"name": "isDel",
"type": 1
}
],
"relations": []
@@ -43,7 +68,10 @@
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredPropertyUids": [
340951913928211738,
5484846939472684412
],
"retiredRelationUids": [],
"version": 1
}
+16 -7
View File
@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@@ -40,7 +41,8 @@
</receiver>
<activity
android:name=".activity.InitActivity"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -49,14 +51,21 @@
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>
<activity
android:name=".activity.FoodCollectionActivity"
android:exported="true">
<!-- <activity-->
<!-- android:name=".activity.FoodCollectionActivity"-->
<!-- android:exported="true">-->
</activity>
<activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />
<!-- </activity>-->
<!-- <activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />-->
<activity android:name="com.sw.dualscreen.activity.SettingActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity android:name="com.sw.dualscreen.activity.CollectedFoodActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity android:name="com.sw.dualscreen.activity.PayActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
@@ -20,7 +20,13 @@ object GlobalData {
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
var appBaseUrl2: String = "http://192.168.1.201:14801"
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 菜品识别模型版本号,后边以接口返回为准
*/
var foodModelVersion: String = "1.0.0"
/**
* 横排数量
@@ -39,7 +45,7 @@ object GlobalData {
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q3LH-HPGZ"//2号楼"085F-118G-Q3AB-1WVJ" 5号楼"085F-118G-Q3LH-HPGZ"
var activeKey = "085F-118G-Q3NU-134V"//2号楼"085F-118G-Q3AB-1WVJ" 5号楼"085F-118G-Q3LH-HPGZ"
}
/**
@@ -61,4 +67,5 @@ object GlobalKey {
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_PICKUP_MODE = "pickupMode"
const val KEY_CHARGE_MODE = "chargeMode"
}
+46 -2
View File
@@ -1,6 +1,10 @@
package com.sw.dualscreen
import android.app.Activity
import android.os.Bundle
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.ActivityManager
import com.sw.dualscreen.utils.CrashHandler
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
@@ -17,11 +21,51 @@ class MyApp : App() {
instance = this
Timber.plant(Timber.DebugTree())
var deviceId = AppUtil.getUDID(this)
Timber.d("UDID = ${AppUtil.getUDID(this)}")
deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
Timber.d("deviceId = $deviceId")
// deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId
GlobalData.appBaseUrl = if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.appBaseUrl2 else GlobalData.appBaseUrl
ObjectBox.init(this)
// 初始化崩溃处理器
CrashHandler.init(this)
SensorScaleUtils.startScale()
addActivityLifecycleListener()
}
private fun addActivityLifecycleListener() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
ActivityManager.addActivity(activity)
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle
) {
}
override fun onActivityDestroyed(activity: Activity) {
ActivityManager.removeActivity(activity)
if (ActivityManager.isEmpty()) {
SensorScaleUtils.closeScale()
}
}
})
}
}
@@ -0,0 +1,42 @@
package com.sw.dualscreen
class Test {
private var lastWeight = 0
// private var currentWeight = 0
data class WeightRecord(
var eatWeight: Int = 0,
var deviceWeight: Int = 0,
var lastWeight: Int = 0,
var state: Boolean = false
)
private val weightRecord by lazy { WeightRecord() }
fun main() {
readWeight { weight ->
if (weightRecord.state) {
//数据已记录
return@readWeight
}
if (weight == 0 || lastWeight == 0) {
return@readWeight
}
if (weight < lastWeight) {
weightRecord.let {
it.deviceWeight = weight
it.eatWeight = it.deviceWeight - it.lastWeight
it.state = true
}
}
lastWeight = weight
}
}
fun readWeight(block: (Int) -> Unit) {
}
}
@@ -1,25 +1,42 @@
package com.sw.dualscreen.activity
import android.app.Dialog
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Display
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.DialogWaitingBinding
import com.sw.dualscreen.model.IActivityResult
import com.sw.dualscreen.model.response.PostEvent
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import kotlinx.coroutines.launch
import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
val displays: Array<Display> by lazy {
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.displays
}
protected lateinit var binding: VB
protected lateinit var context: Context
private var mDialogWaiting: CustomDialog? = null
@@ -27,6 +44,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
enableEdgeToEdge()
context = this
@@ -40,6 +58,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
registerDataChange()
}
override fun onDestroy() {
keyEventHelper?.onDestroy()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
abstract fun getViewModel(): BaseViewModel
protected abstract fun inflateViewBinding(): VB
@@ -49,37 +73,52 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
Timber.d("registerDataChange 用户 showLoading = $it")
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
// lifecycleScope.launch {
// viewModel.showLoading.collect {
// Timber.d("registerDataChange 用户 showLoading = $it")
// if (it) {
// showWaitingDialog("")
// } else {
// hideWaitingDialog()
// }
// }
// }
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
fun showWaitingDialog(tip: String?) {
runOnUiThread {
hideWaitingDialog()
//val view = View.inflate(this, R.layout.dialog_waiting, null)
val dialogBinding = DialogWaitingBinding.inflate(LayoutInflater.from(this))
dialogBinding.tvTip.text = tip
mDialogWaiting = CustomDialog(this, dialogBinding.root)
mDialogWaiting?.show()
}
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
runOnUiThread {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
}
fun showWaitingDialog2(tip: String?) {
if (mDialogWaiting == null) {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting?.show()
}
val contentView = mDialogWaiting?.findViewById<ViewGroup>(android.R.id.content)
val tvTip = contentView?.findViewById<TextView>(R.id.tvTip)
tvTip?.text = tip
}
private var launchPermissionCallback: IActivityResult.RequestPermissionCallback? = null
@@ -97,4 +136,51 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
) { result: Boolean ->
launchPermissionCallback?.callback(result)
}
fun log(msg: String) {
Timber.d(msg)
FileUtil.saveLog(msg)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPostEvent(event: PostEvent) {
}
/**
* 监听扫描枪扫描事件
*/
fun registerKeyEvent() {
keyEventHelper =
ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
override fun onScanSuccess(barcode: String?) {
Timber.d("onScanSuccess barcode = $barcode")
if (barcode == null) return
handleScanKeyInfo(barcode)
}
})
}
protected var keyEventHelper: ScanGunKeyEventHelper? = null
/**
* 处理扫描枪数据
*/
protected open fun handleScanKeyInfo(scanInfo: String) {
var payCode = scanInfo
//if (scanInfo.length > 6) {
// payCode = scanInfo.take(6)
//}
Log.d("TAG", "handleScanKeyInfo: " + payCode)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) {
keyEventHelper!!.analysisKeyEvent(event)
return true
}
}
return super.dispatchKeyEvent(event)
}
}
@@ -1,134 +0,0 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodAdapter
import com.sw.dualscreen.databinding.ActivityCollectedDataBinding
import com.sw.dualscreen.databinding.LayoutEmptySearchBinding
import com.sw.dualscreen.dialog.WarnDialog
import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.objbox.CollectedFoodBean
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlin.getValue
class CollectedDataActivity : BaseActivity<ActivityCollectedDataBinding>() {
private val viewModel by viewModels<UserViewModel>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityCollectedDataBinding {
return ActivityCollectedDataBinding.inflate(layoutInflater)
}
private val list: MutableList<CollectedFoodBean> = mutableListOf()
private val adapter by lazy {
CollectedFoodAdapter(list).apply {
isStateViewEnable = true
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
deleteGoods(position)
}
}
}
override fun initialize() {
super.initialize()
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
}
binding.ivBack.setOnClickListener { finish() }
binding.ivGoodsSearch.setOnClickListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@setOnClickListener
}
getCollectGoods(searchName)
}
binding.etInputGoods.let { v ->
v.addOnActionSearchListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@addOnActionSearchListener
}
getCollectGoods(searchName)
}
v.addTextChangedListener {
if (it.isNullOrBlank()) {
getCollectGoods()
}
}
}
getCollectGoods()
}
private var box: Box<Food>? = null
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName:String?=null) {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
list.clear()
var queryList = box?.all?.distinctBy { it.name }
if (searchName.isNullOrBlank().not()) {
queryList = queryList?.filter { it.name?.contains(searchName) == true }
}
queryList?.forEach {
list.add(CollectedFoodBean(foodName = it.name))
}
adapter.notifyDataSetChanged()
if (list.isEmpty()) {
loadEmptyView()
}
binding.root.hideKeyboard()
}
private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvFoodList, false)
}
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
adapter.stateView = layout
}
}
private fun deleteGoods(position: Int) {
WarnDialog(
context = context,
content = "请确认是否删除菜品:${list[position].foodName}",
confirmBlock = {
Thread {
runOnUiThread {
showWaitingDialog("加载中……")
}
val name = list[position].foodName
val filterIdList = box?.all?.filter { it.name == name }?.map { it.id }
box?.removeByIds(filterIdList)
runOnUiThread {
hideWaitingDialog()
adapter.removeAt(position)
ToastUtils.showToast("删除成功")
if (list.isEmpty()) {
loadEmptyView()
}
}
}.start()
}).show()
}
}
@@ -0,0 +1,227 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
import com.sw.dualscreen.dialog.RemindDialog
import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch
class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
companion object {
private const val PAGE_SIZE = 100
}
private val viewModel by viewModels<UserViewModel>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityCollectedFoodBinding {
return ActivityCollectedFoodBinding.inflate(layoutInflater)
}
private val list: MutableList<CollectedFoodInfo> = mutableListOf()
private val adapter by lazy {
CollectedFoodNewAdapter(list).apply {
// isStateViewEnable = true
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
loadDeleteDialog(position)
}
}
}
override fun initialize() {
super.initialize()
binding.root.setOnClickListener { it.hideKeyboard() }
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
}
binding.include.let {
it.tvPageTitle.text = "已采集餐品"
it.ivPageBack.setOnClickListener { finish() }
}
binding.ivFoodSearch.setOnClickListener {
val searchName = binding.etInputFood.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@setOnClickListener
}
getCollectGoods(searchName)
}
binding.etInputFood.let { v ->
v.addOnActionSearchListener {
val searchName = binding.etInputFood.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@addOnActionSearchListener
}
pageNo = 1
getCollectGoods(searchName)
}
v.addTextChangedListener {
if (it.isNullOrBlank()) {
pageNo = 1
getCollectGoods()
}
}
}
binding.refreshLayout.let {
it.setEnableRefresh(true)
it.setEnableLoadMore(false)
it.setOnRefreshListener {
pageNo = 1
getCollectGoods()
}
it.setOnLoadMoreListener {
getCollectGoods()
}
}
binding.emptyInclude.root.setOnClickListener {
it.hideKeyboard()
}
getCollectGoods()
}
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName: String? = null) {
// if (box == null) {
// box = ObjectBox.boxStore.boxFor(Food::class)
// }
// list.clear()
// val totalList = box?.all
// ?.filter { it.name!=null }
// ?.groupBy { it.name!! }
// ?.map { CollectedFoodInfo(foodName = it.key, foodCount = it.value.size) }
// //var queryMap:Map<String, List<Food>> ?= null
// if (searchName.isNullOrBlank().not()) {
// //queryMap = totalMap?.filter { it.key.contains(searchName) }
// val temp = totalList?.filter { it.foodName?.contains(searchName) == true}
// if (temp.isNullOrEmpty().not()) {
// list.addAll(temp)
// }
// } else {
// if (totalList.isNullOrEmpty().not()) {
// list.addAll(totalList)
// }
// }
// adapter.notifyDataSetChanged()
// if (list.isEmpty()) {
// loadEmptyView()
// }
viewModel.getCollectedFoodList(
pageNo = pageNo,
pageSize = PAGE_SIZE,
foodName = searchName ?: ""
) { items ->
runOnUiThread {
loadFoodList(items)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun loadFoodList(items: List<CollectedFoodInfo>) {
finishRefresh()
if (pageNo == 1 && items.isEmpty()) {
list.clear()
adapter.notifyDataSetChanged()
loadEmptyView()
binding.refreshLayout.setEnableLoadMore(false)
return
}
binding.refreshLayout.visible()
binding.emptyInclude.root.gone()
if (pageNo == 1) {
list.clear()
}
list.addAll(items)
val isEnableLoadMore = items.size >= PAGE_SIZE
if (isEnableLoadMore) {
pageNo++
}
binding.refreshLayout.setEnableLoadMore(isEnableLoadMore)
adapter.notifyDataSetChanged()
binding.root.hideKeyboard()
}
private var pageNo = 1
// private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
binding.refreshLayout.gone()
binding.emptyInclude.root.visible()
//if (emptyBinding == null) {
// emptyBinding =
// LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvFoodList, false)
//}
//emptyBinding?.root?.let { layout ->
// layout.setOnClickListener { layout.hideKeyboard() }
// adapter.stateView = layout
//}
}
private fun loadDeleteDialog(position: Int) {
RemindDialog(
context = context,
content = "请确认是否删除菜品:${list[position].foodName}",
confirmBlock = {
deleteGoods(position)
}).show()
}
private fun deleteGoods(position: Int) {
val food = list[position]
showWaitingDialog("加载中……")
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess ->
if (deleteSuccess) {
// Thread {}.start()
lifecycleScope.launch {
val filterFoodList = ObjectBox.filter(food.foodName)
filterFoodList.forEach { it.isDel = true }
if (filterFoodList.isNotEmpty()) {
ObjectBox.putAll(filterFoodList)
}
runOnUiThread {
hideWaitingDialog()
adapter.removeAt(position)
ToastUtils.showToast("删除成功")
if (list.isEmpty()) {
loadEmptyView()
}
}
}
}
}
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (pageNo == 1) {
it.finishRefresh(500)
} else {
it.finishLoadMore(500)
}
}
}
}
@@ -1,370 +0,0 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Typeface
import android.net.Uri
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.net.toUri
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.example.utils.FloatBase64Utils
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.ActivityFoodCollectionBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.utils.BitmapCropper
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.CameraHelper
import com.sw.dualscreen.utils.CameraUtils
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import kotlin.math.max
class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
companion object {
val MAX_COUNT = 100
}
private val viewModel by viewModels<UserViewModel>()
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private var box: Box<Food>? = null
private val foodCollectionList: MutableList<FoodCollectionBean> = mutableListOf()
// private lateinit var cameraHelper: CameraHelper
private lateinit var foodAdapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private val foodList = mutableListOf<FoodInfo>() // 适配器内部维护的数据列表
private val debouncer = Debouncer(2000)
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
// setOnItemClickListener { _, _, position ->
// if (list[position].isShowCamera) {
//// takePhoto()
// cameraHelper.openCamera()
// }
// }
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
// list.removeAt(position)
// collectionAdapter.notifyItemRemoved(position)
// collectionAdapter.notifyItemRangeChanged(position, list.size)
foodCollectionList[position].let {
it.bitmap = null
it.isShowCamera = true
it.isFinish = false
}
notifyItemChanged(position)
}
}
}
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityFoodCollectionBinding {
return ActivityFoodCollectionBinding.inflate(layoutInflater)
}
private val cameraCallback: (Uri) -> Unit = { uri ->
val index = foodCollectionList.indexOfFirst { it.bitmap == null }
if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
rerurn@ cameraCallback
}
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
// val cropBitmap = BitmapCropper.cropCenter(
// original = bitmap,
// targetWidth = 1300, targetHeight = 900,
// //offsetX = 30, offsetY = 100
// )
val file = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
foodCollectionList[index].let {
it.bitmap = bitmap
it.isShowCamera = false
it.imageUri = file?.toUri()
}
collectionAdapter.notifyItemChanged(index)
}
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return
}
cameraUtils.takePhoto(cameraCallback)
}
override fun initialize() {
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 180.dp
height = 180.dp
}
}
cameraUtils.setPreviewController(previewView)
// 初始化 CameraHelper
// cameraHelper = CameraHelper(
// context = this,
// caller = this,
// authority = "${packageName}.fileprovider"
// ) { uri, path ->
// //if (foodCollectionList.size < 6) {
// // val insertIndex = if (foodCollectionList.isEmpty()) 0 else foodCollectionList.size - 1
// // foodCollectionList.add(insertIndex, FoodCollectionBean(imageUri = uri))
// //} else {
// // foodCollectionList[foodCollectionList.size - 1] = FoodCollectionBean(imageUri = uri)
// //}
// //collectionAdapter.notifyDataSetChanged()
// cameraCallback(uri)
// }
binding.ivBack.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
repeat(MAX_COUNT) {
foodCollectionList.add(FoodCollectionBean(isShowCamera = true))
}
binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
binding.btnFoodSearch.setOnClickListener {
searchInfo()
}
binding.btnSave.setOnClickListener {
if (selectedFoodName.isNullOrBlank()) {
Toast.makeText(this, "请选择菜品名称", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (foodCollectionList.size <= 1) {
Toast.makeText(this, "请拍摄菜品照片", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
vectorThread()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchInfo()
val imm =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
true
} else {
false
}
}
binding.btnCollectedGoods.setOnClickListener {
startActivity(Intent(this, CollectedDataActivity::class.java))
}
binding.btnTakePhoto.clickWithDebounce {
binding.btnTakePhoto.text = "拍照"
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return@clickWithDebounce
}
// cameraHelper.openCamera()
takePhoto()
}
binding.btnClearData.setOnClickListener { clearData() }
foodAdapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = dpToPx(30),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = foodAdapter
}
private fun vectorThread() {
//Loading.show(this)
showWaitingDialog("加载中……")
Thread {
foodCollectionList
.filter { it.bitmap != null }
.forEachIndexed { index, it ->
image2VectorTask(it, index)
}
runOnUiThread {
window.decorView.postDelayed({
//Loading.dismiss()
hideWaitingDialog()
}, 1000)
}
}.start()
}
private fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
private fun image2VectorTask(item: FoodCollectionBean, position: Int) {
initBox()
val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
Timber.tag("mzf1").e(base64Str)
// viewModel.postImageData(
// context,
// foodId = selectedFoodId.toString(),
// foodName = selectedFoodName.toString(),
// foodVector = base64Str,
// uri = item.imageUri!!
// )
box?.put(Food(name = checkedItem!!.foodName, foodIdx = 0, foodVector = imageVector))
foodCollectionList[position].let {
it.imageVector = imageVector
it.isFinish = true
}
runOnUiThread {
collectionAdapter.notifyItemChanged(position)
}
}
private fun searchInfo() {
debouncer.debounce {
viewModel.searchByFoodName(binding.editFoodName.text.toString())
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchFoodInfoList.collect {
// foodList.clear()
// foodList.addAll(it)
foodAdapter.updateData(it)
}
}
}
private var checkedItem: FoodInfo? = null
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.foodName
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.search_normal))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
} else {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
this.tvName.setTextColor(resources.getColor(R.color.search_checked))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.foodName}, position = $position")
checkedItem = item
// viewModel.updateCurrentItem(item)
// itemClickCallback(item)
selectedFoodName = item.foodName
selectedFoodId = item.id
foodAdapter.notifyDataSetChanged()
}
}
)
}
private var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
foodCollectionList.forEach {
it.bitmap = null
it.isShowCamera = true
it.isFinish = false
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.editFoodName.setText("")
// foodList.clear()
// foodAdapter.updateData(mutableListOf())
// foodAdapter.notifyDataSetChanged()
//loadEmptyView()
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
visibility = View.GONE
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visibility = View.VISIBLE
}
}
@@ -1,56 +1,136 @@
package com.sw.dualscreen.activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.os.CountDownTimer
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.utils.QRCodeUtil
import com.sw.dualscreen.dialog.RemindDialog
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.utils.L
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.DeviceViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.AppUtil
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
class InitActivity : BaseActivity<ActivityInitBinding>() {
private val viewModel by viewModels<DeviceViewModel>()
companion object {
private const val PAGE_SIZE = 100
}
private val userViewModel by viewModels<UserViewModel>()
private var pageNum = 1
override fun getViewModel(): BaseViewModel {
return viewModel
return userViewModel
}
override fun inflateViewBinding(): ActivityInitBinding {
return ActivityInitBinding.inflate(layoutInflater)
}
private fun getCollectedFoodVector(block: () -> Unit) {
userViewModel.getCollectedFoodVector(
param = mutableMapOf(
"pageNum" to "$pageNum",
"pageSize" to "$PAGE_SIZE",
"version" to GlobalData.foodModelVersion
)
) { items ->
if (items == null) {
getVectorErrorDialog()
return@getCollectedFoodVector
}
val list = items
if (pageNum == 1 && list.isEmpty()) {
block()
return@getCollectedFoodVector
}
if (list.size >= PAGE_SIZE) {
saveFoodVector(list)
pageNum++
getCollectedFoodVector(block)
return@getCollectedFoodVector
}
if (list.isNotEmpty()) {
saveFoodVector(list)
block()
}
}
}
// private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
private fun saveFoodVector(list: List<FoodVector>) {
lifecycleScope.launch {
val vectorList = list.map { vt ->
val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")
?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
Food(
collectId = vt.id,
foodId = vt.foodId,
foodName = vt.foodName,
version = vt.version,
foodVector = foodVector
)
}
ObjectBox.putAll(vectorList)
}
}
override fun initialize() {
super.initialize()
val isSuccess = viewModel.checkEquipmentInfo()
if (isSuccess) {
goMainActivity()
return
}
binding.imgQr.setImageBitmap(
QRCodeUtil.generateQRCode(
content = GlobalData.deviceId,
size = 200
)
)
binding.initButton.setOnClickListener {
viewModel.getDeviceToken()
initNetworkTimer()
binding.okButton.clickWithDebounce(500) {
checkNetworkTimer?.cancel()
checkNetworkTimer?.start()
}
showWaitingDialog("网络连接中...")
checkNetworkTimer?.start()
// val isSuccess = viewModel.checkEquipmentInfo()
// if (isSuccess) {
// goMainActivity()
// return
// }
// binding.imgQr.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.initButton.setOnClickListener {
// viewModel.getDeviceToken()
// }
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.deviceInfoResult.drop(1).collect {
// if (it != true) return@collect
// goMainActivity()
// }
// }
}
private fun getCollectedFoodVector() {
showWaitingDialog2("加载中……")
lifecycleScope.launch {
viewModel.deviceInfoResult.drop(1).collect {
if (it != true) return@collect
goMainActivity()
FoodModule.init(this@InitActivity) {
getCollectedFoodVector {
runOnUiThread {
hideWaitingDialog()
binding.root.postDelayed({
goMainActivity()
}, 1000)
}
}
}
}
}
@@ -60,4 +140,48 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
startActivity(intent)
finish()
}
private var checkNetworkTimer: CountDownTimer? = null
private fun initNetworkTimer() {
checkNetworkTimer = object : CountDownTimer(5 * 300, 300) {
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
L.e("检测网络连接状态" + AppUtil.isNetworkConnected(this@InitActivity))
if (!AppUtil.isNetworkConnected(this@InitActivity)) {
hideWaitingDialog()
binding.okButton.visible()
loadNetworkErrorDialog()
return
}
getCollectedFoodVector()
}
}
}
private fun loadNetworkErrorDialog() {
RemindDialog(
context = context,
content = "网络连接异常,请检查WiFi连接状态后重试",
confirmBlock = {
startActivity(Intent("android.settings.WIFI_SETTINGS"))
}).show()
}
private fun getVectorErrorDialog() {
RemindDialog(
context = context,
content = "未查询到向量数据,请稍后重试?",
confirmBlock = {
getCollectedFoodVector()
}).show()
}
override fun onDestroy() {
checkNetworkTimer?.cancel()
super.onDestroy()
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,306 @@
package com.sw.dualscreen.activity
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.fragment.pay.CashPayFragment
import com.sw.dualscreen.activity.fragment.pay.FacePayFragment
import com.sw.dualscreen.activity.fragment.pay.NumberPayFragment
import com.sw.dualscreen.activity.fragment.pay.PayResultFragment
import com.sw.dualscreen.activity.fragment.pay.ScanQrCodePayFragment
import com.sw.dualscreen.databinding.ActivityPayBinding
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.presentation.pay.ScanQrCodePayPresentation
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import org.greenrobot.eventbus.EventBus
class PayActivity : BaseActivity<ActivityPayBinding>() {
companion object {
const val FOOD_INFO = "foodInfo"
const val TOTAL_AMOUNT = "totalAmount"
const val FOOD_ORDER_ID = "foodOrderId"
const val FOOD_EAT_NUM = "foodEatNum"
// const val MEMBER_INFO = "memberInfo"
const val TAG_PAY_QR_CODE = "tagPayQrCode"
const val TAG_PAY_CASH = "tagPayCash"
const val TAG_PAY_NUMBER = "tagPayNumber"
const val TAG_PAY_FACE = "tagPayFace"
const val TAG_PAY_RESULT = "tagPayResult"
}
val userViewModel by viewModels<UserViewModel>()
val recognizeViewModel by viewModels<RecognizeViewModel>()
override fun getViewModel(): BaseViewModel {
return userViewModel
}
override fun inflateViewBinding(): ActivityPayBinding {
return ActivityPayBinding.inflate(layoutInflater)
}
var foodInfo: FoodInfo? = null
// var totalAmount:Double = 0.0
private var qrCodePayFragment: ScanQrCodePayFragment? = null
private var cashPayFragment: CashPayFragment? = null
private var numberPayFragment: NumberPayFragment? = null
var facePayFragment: FacePayFragment? = null
private var payResultFragment: PayResultFragment? = null
var foodOrderId: String = ""
var eatNum: Int = 1
@Suppress("DEPRECATION")
override fun initialize() {
super.initialize()
foodInfo = intent.getParcelableExtra(FOOD_INFO)
foodOrderId = intent.getStringExtra(FOOD_ORDER_ID) ?: ""
eatNum = intent.getIntExtra(FOOD_EAT_NUM, 1)
// totalAmount = intent.getDoubleExtra(TOTAL_AMOUNT,0.0)
// memberInfo = intent.getParcelableExtra(MEMBER_INFO)
binding.tvFoodName.text = foodInfo?.foodName
binding.include.ivPageBack.setOnClickListener { finish() }
binding.include.tvPageTitle.text = "下单结算"
binding.btnPayQrCode.setOnClickListener {
if (binding.btnPayQrCode.isChecked) {
return@setOnClickListener
}
showScanQrCodePay()
}
binding.btnPayCash.setOnClickListener {
if (binding.btnPayCash.isChecked) {
return@setOnClickListener
}
cashPayFragment = CashPayFragment()
showFragment(cashPayFragment!!, TAG_PAY_CASH)
switchButton(false, true, false, false)
}
binding.btnPayNumber.setOnClickListener {
if (binding.btnPayNumber.isChecked) {
return@setOnClickListener
}
numberPayFragment = NumberPayFragment()
showFragment(numberPayFragment!!, TAG_PAY_NUMBER)
switchButton(false, false, true, false)
}
binding.btnPayFace.setOnClickListener {
if (binding.btnPayFace.isChecked) {
return@setOnClickListener
}
showFacePay()
}
showScanQrCodePay()
}
private fun showScanQrCodePay() {
qrCodePayFragment = ScanQrCodePayFragment()
showFragment(qrCodePayFragment!!, TAG_PAY_QR_CODE)
switchButton(true, false, false, false)
}
private fun switchButton(vararg arr: Boolean) {
binding.btnPayQrCode.isChecked = arr[0]
binding.btnPayCash.isChecked = arr[1]
binding.btnPayNumber.isChecked = arr[2]
binding.btnPayFace.isChecked = arr[3]
}
fun showFacePay() {
facePayFragment = FacePayFragment()
showFragment(facePayFragment!!, TAG_PAY_FACE)
switchButton(false, false, false, true)
}
private fun showFragment(fragment: Fragment, tag: String) {
runCatching {
supportFragmentManager.beginTransaction().apply {
replace(R.id.fragmentContainerView, fragment)
commitNowAllowingStateLoss()
}
}.onFailure {
it.printStackTrace()
}
}
override fun onDestroy() {
qrCodePayFragment?.presentation?.dismiss()
cashPayFragment?.presentation?.dismiss()
numberPayFragment?.presentation?.dismiss()
facePayFragment?.presentation?.dismiss()
scanQrCodePayPresentation?.dismiss()
super.onDestroy()
}
private var scanQrCodePayPresentation: ScanQrCodePayPresentation? = null
var memberInfo: MemberInfo? = null
fun showPayInfo(type: Int = 1, isVip: Boolean = false, memberInfo: MemberInfo? = null) {
this.memberInfo = memberInfo
payResultFragment = PayResultFragment.instance(type, memberInfo)
showFragment(payResultFragment!!, TAG_PAY_RESULT)
if (displays.size > 1) {
scanQrCodePayPresentation = ScanQrCodePayPresentation(
activity = this,
display = displays[1],
type = type
) {
scanQrCodePayPresentation?.dismiss()
}.also {
it.foodName = foodInfo?.foodName
//确认使用vipPrice 还是 specPrice
val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
it.totalPrice = (onePrice ?: 0.0) * eatNum
}
scanQrCodePayPresentation?.show()
}
}
fun updateQrCodeImage(qrCodeUrl: String?) {
scanQrCodePayPresentation?.loadQrCodeImage(qrCodeUrl)
}
fun showPaySuccess(isVip: Boolean) {
EventBus.getDefault().post(PaySuccessEvent())
showPayInfo(type = 2, isVip = isVip, memberInfo = memberInfo)
hidePayTab()
}
fun getQrCodeImg(
orderId: String,
userId: String? = null,
totalFee: String? = null,
block: (String?) -> Unit
) {
userViewModel.getQrCodeImg(
orderId = orderId,
userId = userId,
totalFee = totalFee
) { qrCodeImg ->
runOnUiThread {
if (qrCodeImg.isNullOrBlank()) {
//ToastUtils.showToast("获取二维码失败")
return@runOnUiThread
}
block(qrCodeImg)
}
}
}
fun cashPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
userViewModel.cashPay(param) {
runOnUiThread {
block(it)
}
}
}
fun memberPay(param: HashMap<String, String>, block: () -> Unit) {
userViewModel.memberPay(param) {
runOnUiThread {
if (it.not()) {
//ToastUtils.showToast("支付失败,请稍后重试")
return@runOnUiThread
}
block()
}
}
}
suspend fun queryOrderState(block: (Boolean) -> Unit) {
userViewModel.queryOrderState(orderId = foodOrderId) { payResult ->
runOnUiThread {
if (payResult) {
block(true)
} else {
block(false)
}
}
}
}
fun hidePayTab() {
binding.llPayTab.gone()
}
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
userViewModel.getMemberInfoByPhone(phone, key) { memberInfo ->
runOnUiThread {
block(memberInfo)
}
}
}
fun getMemberInfoById(userId: String, block: (MemberInfo?) -> Unit) {
userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
runOnUiThread {
block(memberInfo)
}
}
}
fun bindOrder(userId: String, block: (Boolean) -> Unit) {
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
val mode = if (pickupMode == 1) 1 else 2
userViewModel.bindOrder(userId, foodOrderId, mode = mode) { bindResult ->
runOnUiThread {
block(bindResult)
}
}
}
// fun bindOrder(userId: String, block: () -> Unit) {
// val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
// val mode = if (pickupMode == 1) 1 else 2
// userViewModel.bindOrder(userId, foodOrderId, mode = mode) { bindResult ->
// runOnUiThread {
// if (bindResult.not()) {
// hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@runOnUiThread
// }
// binding.root.postDelayed({
// hideWaitingDialog()
// showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
// hidePayTab()
// block()
// }, 1000)
// }
// }
// }
// fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo) -> Unit) {
// userViewModel.getMemberInfoByPhone(phone, key) { memberInfo ->
// runOnUiThread {
// if (memberInfo == null) {
// hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@runOnUiThread
// }
// block(memberInfo)
// }
// }
// }
// fun getMemberInfo(userId: String, block: (MemberInfo) -> Unit) {
// userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
// runOnUiThread {
// if (memberInfo == null) {
// hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@runOnUiThread
// }
// block(memberInfo)
// }
// }
// }
}
@@ -0,0 +1,87 @@
package com.sw.dualscreen.activity
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.fragment.BusinessFragment
import com.sw.dualscreen.activity.fragment.CollectFragment
import com.sw.dualscreen.databinding.ActivitySettingBinding
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.ClickBackEvent
import com.sw.dualscreen.model.response.UpdateRefreshEvent
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import org.greenrobot.eventbus.EventBus
class SettingActivity : BaseActivity<ActivitySettingBinding>() {
val viewModel by viewModels<UserViewModel>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivitySettingBinding {
return ActivitySettingBinding.inflate(layoutInflater)
}
private val fragmentList = mutableListOf<Fragment>().apply {
add(BusinessFragment())
add(CollectFragment())
}
override fun initialize() {
super.initialize()
initBackDispatcher()
binding.include.let {
it.ivPageBack.setOnClickListener {
//// val intent = Intent(this, MainActivity::class.java)
//// startActivity(intent)
// EventBus.getDefault().post(ClickBackEvent())
finish()
}
it.tvPageTitle.text = "设置"
}
binding.rgSetting.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == R.id.rbBusiness) {
showFragment(fragmentList[0])
return@setOnCheckedChangeListener
}
if (checkedId == R.id.rbCollect) {
EventBus.getDefault().post(UpdateRefreshEvent())
showFragment(fragmentList[1])
return@setOnCheckedChangeListener
}
}
binding.rgSetting.check(R.id.rbBusiness)
}
private fun showFragment(fragment: Fragment) {
runCatching {
supportFragmentManager.beginTransaction().apply {
replace(R.id.fragmentContainerView, fragment)
commit()
}
}.onFailure {
it.printStackTrace()
}
}
fun searchByFoodName(foodName:String, action: (List<FoodInfo>) -> Unit) {
viewModel.searchByFoodName(foodName, action)
}
private lateinit var backPressedCallback: OnBackPressedCallback
private fun initBackDispatcher() {
// 创建回调,true表示初始启用状态
// backPressedCallback = object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
//// EventBus.getDefault().post(ClickBackEvent())
// }
// }
// // 注册回调,使用lifecycleOwner确保生命周期安全
// onBackPressedDispatcher.addCallback(this, backPressedCallback)
}
}
@@ -0,0 +1,60 @@
package com.sw.dualscreen.activity.fragment
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.view.Display
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.sw.dualscreen.model.response.PostEvent
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
abstract class BaseFragment<VB : ViewBinding>: Fragment() {
val displays: Array<Display> by lazy {
val displayManager = requireActivity().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.displays
}
protected lateinit var binding: VB
protected abstract fun inflateViewBinding(): VB
abstract fun initialize()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = inflateViewBinding()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPostEvent(event: PostEvent) {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
}
}
@@ -0,0 +1,77 @@
package com.sw.dualscreen.activity.fragment
import android.view.LayoutInflater
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.databinding.FragmentBusinessBinding
import com.sw.dualscreen.model.response.ChargeModeEvent
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.SPUtil
import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus
import kotlin.math.roundToInt
class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
override fun inflateViewBinding(): FragmentBusinessBinding {
return FragmentBusinessBinding.inflate(LayoutInflater.from(context))
}
override fun initialize() {
binding.rgCharge.let {
// it.setOnCheckedChangeListener { group, checkedId ->
// val mode = if (checkedId == R.id.rbChargeYes) 0 else 1
// SPUtil.getInstance().put(GlobalKey.KEY_CHARGE_MODE, mode)
// }
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
it.check(if (mode == 0) R.id.rbChargeYes else R.id.rbChargeNot)
}
binding.rgTakeFood.let {
// it.setOnCheckedChangeListener { group, checkedId ->
// val mode = if (checkedId == R.id.rbPickAndPlace) 0 else 1
// SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, mode)
// }
val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
it.check(if (mode == 0) R.id.rbPickAndPlace else R.id.rbSurplusCalculate)
}
binding.btnClearZero.setOnClickListener {
SensorScaleUtils.tare()
}
binding.btnConfirm.setOnClickListener { v ->
if (binding.rgCharge.checkedRadioButtonId == -1) {
ToastUtils.showToast("请设置计费模式")
return@setOnClickListener
}
if (binding.rgTakeFood.checkedRadioButtonId == -1) {
ToastUtils.showToast("请设置取餐模式")
return@setOnClickListener
}
val chargeMode = if (binding.rgCharge.checkedRadioButtonId == R.id.rbChargeYes) 0 else 1
//原来的支付模式
val oldChargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (chargeMode != oldChargeMode) {
//支付模式有变化通知首页页面刷新
EventBus.getDefault().post(ChargeModeEvent(chargeMode))
}
SPUtil.getInstance().put(GlobalKey.KEY_CHARGE_MODE, chargeMode)
val pickupMode = if (binding.rgTakeFood.checkedRadioButtonId == R.id.rbPickAndPlace) 0 else 1
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, pickupMode)
//ToastUtils.showToast("设置已保存")
(activity as SettingActivity).let {
it.showWaitingDialog("设置保存中")
v.postDelayed({
it.hideWaitingDialog()
it.finish()
}, 1000)
}
}
SensorScaleUtils.addWeightListener { value ->
val realWeight = (value * 1000).roundToInt()
binding.tvFoodWeight.text = "$realWeight"
}
}
}
@@ -0,0 +1,441 @@
package com.sw.dualscreen.activity.fragment
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.CollectedFoodActivity
import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.adapter.CollectFoodListAdapter
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.databinding.FragmentCollectBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.CameraUtils
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUploader
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
@SuppressLint("NotifyDataSetChanged")
class CollectFragment : BaseFragment<FragmentCollectBinding>() {
companion object {
private const val TAG = "CollectFragment"
const val MAX_COUNT = 9
}
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private val foodCollectionList = mutableListOf<FoodCollectionBean>().apply {
repeat(MAX_COUNT) {
add(FoodCollectionBean(isShowCamera = true))
}
}
private val searchFoodList = mutableListOf<FoodInfo>()
private var settingActivity: SettingActivity? = null
private var checkedItem: FoodInfo? = null
private val searchFoodAdapter by lazy {
CollectFoodListAdapter(searchFoodList).apply {
setOnItemClickListener { adapter, view, position ->
searchFoodList.forEachIndexed { index, item -> item.isChecked = index == position }
checkedItem = searchFoodList[position]
notifyDataSetChanged()
selectedFoodId = checkedItem!!.foodId
selectedFoodName = checkedItem!!.foodName
}
}
}
private val debouncer = Debouncer(2000)
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(requireActivity())
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
foodCollectionList[position].let {
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.imageUri = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
}
notifyItemChanged(position)
}
}
}
override fun inflateViewBinding(): FragmentCollectBinding {
return FragmentCollectBinding.inflate(LayoutInflater.from(context))
}
private val cameraCallback: (Uri) -> Unit = { uri ->
try {
val index = foodCollectionList.indexOfFirst { it.imageFile == null }
if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
settingActivity?.hideWaitingDialog()
rerurn@ cameraCallback
}
activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = null
it.bitmap = null
it.isShowCamera = false
it.imageFile = null
it.imageUri = uri
}
collectionAdapter.notifyItemChanged(index)
}
// settingActivity?.hideWaitingDialog()
Thread {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
}
}.start()
} catch (e: Exception) {
e.printStackTrace()
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("程序异常${e.message}")
settingActivity?.log("程序异常${e.message}")
}
}
private fun getImageVector(index: Int, bitmap: Bitmap) {
// val bitmap = BitmapCropper.cropCenter(
// original = srcBmp,
// targetWidth = 900, targetHeight = 900,
//// offsetX = 30, offsetY = 100
// )
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showToast("操作失败")
settingActivity?.log("操作失败:${e.message}")
settingActivity?.hideWaitingDialog()
return
}
val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
collectionAdapter.notifyItemChanged(index)
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
settingActivity?.hideWaitingDialog()
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return
}
settingActivity?.showWaitingDialog("采集中……")
cameraUtils.takePhoto(cameraCallback) { errMsg ->
settingActivity?.hideWaitingDialog()
// ToastUtils.showToast("拍照异常,请重新操作")
// if (cameraErrorCount >= 10) {
// ToastUtils.showToast("拍照异常,请重新操作")
// settingActivity?.log("拍照异常,请重新操作:$errMsg")
// cameraErrorCount = 0
// return@takePhoto
// }
// cameraErrorCount++
// takePhoto()
}
}
private var cameraErrorCount = 0
override fun initialize() {
settingActivity = activity as SettingActivity
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 456.dp
height = 342.dp
}
}
cameraUtils.setPreviewController(previewView)
binding.rvFoodList.let {
it.layoutManager =
GridLayoutManager(requireActivity(), 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
binding.btnFoodSearch.setOnClickListener {
searchFood()
}
binding.btnSave.setOnClickListener {
if (checkedItem == null || checkedItem!!.isChecked.not()) {
ToastUtils.showToast("请选择菜品名称")
return@setOnClickListener
}
val count = foodCollectionList.count { it.imageFile != null }
if (count == 0) {
ToastUtils.showToast("请拍摄菜品照片")
return@setOnClickListener
}
upload()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchFood()
val imm =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
true
} else {
false
}
}
binding.btnCollectedFood.setOnClickListener {
startActivity(Intent(requireActivity(), CollectedFoodActivity::class.java))
}
binding.btnTakePhoto.clickWithDebounce {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return@clickWithDebounce
}
takePhoto()
}
binding.btnClearData.setOnClickListener { clearData() }
binding.rvSearchFood.let {
it.layoutManager = GridLayoutManager(context, 2)
it.adapter = searchFoodAdapter
}
searchFood()
}
@SuppressLint("NotifyDataSetChanged")
private fun upload() {
lifecycleScope.launch {
val totalFileCount = foodCollectionList.count { it.imageFile != null }
settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount")
val params = HashMap<String, RequestBody>()
//params["placeId"] = restId.toRequestBody()
params["foodId"] = checkedItem!!.foodId.toRequestBody()
params["foodName"] = checkedItem!!.foodName!!.toRequestBody()
params["version"] = GlobalData.foodModelVersion.toRequestBody()
ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
val files = batch.map { it.imageFile }
val foodVectorList = batch.filter { it.imageVector != null }.map {
it.imageVector!!.joinToString(
separator = ",", prefix = "[", postfix = "]"
)
}
val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody()
settingActivity?.viewModel?.uploadCollectFoodPics(files, params)
}, onProgress = { count, batch, idList ->
//batch.forEach {
// it.uploadSuccess = true
//}
runBlocking {
activity?.runOnUiThread {
settingActivity?.showWaitingDialog2("图片上传中$count/$totalFileCount")
}
val foodList = batch.mapIndexed { index, it ->
Food(
collectId = if (index < idList.size) idList[index] else null,
foodId = checkedItem!!.foodId,
foodName = checkedItem!!.foodName,
foodVector = it.imageVector,
version = GlobalData.foodModelVersion
)
}
ObjectBox.putAll(foodList)
activity?.runOnUiThread {
batch.forEach { it.isFinish = true }
collectionAdapter.notifyDataSetChanged()
}
}
}, onError = {
activity?.runOnUiThread {
binding.root.postDelayed({
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("上传失败,请稍后重试")
}, 1000)
}
}, onComplete = {
//vectorThread()
binding.root.postDelayed({
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("上传成功")
}, 1000)
}).processUploads()
}
// for (index in foodCollectionList.indices step 5) {
// val end = if(index + 5 < foodCollectionList.size - 1) index + 5 else foodCollectionList.size - 1
// val subList = foodCollectionList.subList(index, end)
// val subFiles = subList.map { it.imageFile }
// uploadCollectFoodPics(subFiles, params) { isSuccess->
// Timber.tag(TAG).d("uploadMultipleImages: ${isSuccess}")
// subList.filter { it.imageFile!=null }.forEach { it.uploadSuccess = isSuccess }
//
//// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
//// runOnUiThread {
//// binding.btnUploadImage.text = "待上传图片${count}张"
//// if (count == 0) {
//// Loading.dismiss()
//// }
//// }
// }
// }
}
// private fun vectorThread() {
// settingActivity?.showWaitingDialog("加载中……")
// Thread {
// foodCollectionList
//// .filter { it.bitmap != null }
//// .filter { it.imageVector != null }
// .forEachIndexed { index, it ->
// if (it.imageVector == null) {
// return@forEachIndexed
// }
// image2VectorTask(imageVector = it.imageVector!!, index)
// }
// activity?.runOnUiThread {
// binding.root.postDelayed({
// settingActivity?.hideWaitingDialog()
// }, 1000)
// }
// }.start()
// }
// private fun image2VectorTask(imageVector: FloatArray?, position: Int) {
// if (imageVector == null) return
////// val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
//////
//////// val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
//////// Timber.tag("mzf1").e(base64Str)
////////// viewModel.postImageData(
////////// context,
////////// foodId = selectedFoodId.toString(),
////////// foodName = selectedFoodName.toString(),
////////// foodVector = base64Str,
////////// uri = item.imageUri!!
////////// )
//// box.put(
//// Food(
//// collectId = null,
//// foodId = checkedItem!!.foodId,
//// foodName = checkedItem!!.foodName,
//// foodVector = imageVector,
//// version = "1.0.0"
//// )
//// )
//// if (position > -1) {
//// foodCollectionList[position].isFinish = true
//// activity?.runOnUiThread {
//// collectionAdapter.notifyItemChanged(position)
//// }
//// }
// }
@SuppressLint("NotifyDataSetChanged")
private fun searchFood() {
debouncer.debounce {
settingActivity?.searchByFoodName(binding.editFoodName.text.toString()) {
searchFoodList.clear()
searchFoodList.addAll(it)
searchFoodAdapter.notifyDataSetChanged()
}
}
}
private var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
foodCollectionList.forEach {
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.imageUri = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.editFoodName.setText("")
searchFood()
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
visibility = View.GONE
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visibility = View.VISIBLE
}
}
@@ -0,0 +1,96 @@
package com.sw.dualscreen.activity.fragment.pay
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import androidx.core.text.buildSpannedString
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentCashPayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.presentation.pay.CashPayPresentation
import com.sw.plate.utils.ToastUtils
class CashPayFragment : BaseFragment<FragmentCashPayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
var presentation: CashPayPresentation? = null
override fun inflateViewBinding(): FragmentCashPayBinding {
return FragmentCashPayBinding.inflate(layoutInflater)
}
override fun initialize() {
payActivity = requireActivity() as PayActivity
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.specPrice?:0.0) * payActivity.eatNum
}
binding.tvRealAmount.text = getAmountText(payAmount.format2String(2))
binding.btnConfirmPayFinish.setOnClickListener {
payActivity.cashPay(hashMapOf(
//标价金额(单位为元)
"totalFee" to payAmount.format2String(2),
//订单类型(0付款1会员充值)
"orderType" to "0",
//商户订单号
"orderNo" to payActivity.foodOrderId,
//支付来源:0线下收款 、3:线上纯会员支付 101:支付宝主动、102:支付宝被动、201:微信主动、202:微信被动
"paySource" to "0"
)) { paySuccess ->
if (paySuccess.not()) {
ToastUtils.showToast("接口调用失败")
return@cashPay
}
payActivity.showPaySuccess( false)
}
}
showSubScreen()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
dismissSubScreen()
super.onDestroy()
}
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation = CashPayPresentation(activity = payActivity, display = displays[1]) {
presentation?.dismiss()
}.also {
it.foodName = foodName
it.payAmount = payAmount.format2String(2)
}
presentation?.show()
}
}
private fun getAmountText(amount: String): SpannedString {
return buildSpannedString {
append("¥", AbsoluteSizeSpan(36, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
append(amount, AbsoluteSizeSpan(60, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
@@ -0,0 +1,77 @@
package com.sw.dualscreen.activity.fragment.pay
import android.graphics.Bitmap
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentFacePayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.presentation.pay.FacePayPresentation
class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
var presentation: FacePayPresentation? = null
override fun inflateViewBinding(): FragmentFacePayBinding {
return FragmentFacePayBinding.inflate(layoutInflater)
}
override fun initialize() {
payActivity = activity as PayActivity
// binding.tvFaceState.setOnClickListener {
// payActivity?.showPayResult()
// }
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum
}
showSubScreen()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
dismissSubScreen()
super.onDestroy()
}
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
fun loadBitmap(frame: Bitmap) {
binding.ivFaceImage.setImageBitmap(frame)
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation = FacePayPresentation(
activity = payActivity,
display = displays[1],
userViewModel = payActivity.userViewModel,
recognizeViewModel = payActivity.recognizeViewModel,
// parentTextureView = binding.parentTextureView,
) {
presentation?.dismiss()
}.also {
it.foodName = foodName
it.payAmount = payAmount.format2String(2)
}
presentation?.show()
}
}
}
@@ -0,0 +1,139 @@
package com.sw.dualscreen.activity.fragment.pay
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentNumberPayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.presentation.pay.CashPayPresentation
import com.sw.plate.utils.ToastUtils
class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
var presentation: CashPayPresentation? = null
override fun inflateViewBinding(): FragmentNumberPayBinding {
return FragmentNumberPayBinding.inflate(layoutInflater)
}
override fun initialize() {
binding.root.setOnClickListener { v ->
v.hideKeyboard()
}
payActivity = requireActivity() as PayActivity
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.specPrice ?: 0.0) * payActivity.eatNum
}
binding.btnConfirm.setOnClickListener { v ->
val phone = binding.etInputPhone.text.toString().trim()
val key = binding.etInputNumber.text.toString().trim()
if (phone.isBlank() || key.isBlank()) {
ToastUtils.showToast("请输入会员验证信息")
return@setOnClickListener
}
payActivity.showWaitingDialog("加载中,请稍后……")
v.hideKeyboard()
// payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
// payActivity.bindOrder(memberInfo.id?:"") {
// ToastUtils.showToast("会员验证成功")
// }
// }
payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
if (memberInfo == null) {
binding.root.postDelayed({
payActivity.hideWaitingDialog()
ToastUtils.showToast("未查询到会员信息,请稍后重试")
}, 1000)
return@getMemberInfoByPhone
}
//绑定订单使用faceUserId
payActivity.bindOrder(memberInfo.faceUserId ?: "") { bindResult ->
if (bindResult.not()) {
payActivity.hideWaitingDialog()
//ToastUtils.showToast("订单绑定失败")
return@bindOrder
}
binding.root.postDelayed({
payActivity.hideWaitingDialog()
payActivity.showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
payActivity.hidePayTab()
}, 1000)
}
}
// getMemberInfo(phone, key) { memberInfo ->
// bindOrder(memberInfo.id ?: "") {
// binding.root.postDelayed({
// payActivity.hideWaitingDialog()
// payActivity.showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
// payActivity.hidePayTab()
// }, 1000)
// }
// }
}
showSubScreen()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
dismissSubScreen()
super.onDestroy()
}
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation = CashPayPresentation(activity = payActivity, display = displays[1]) {
presentation?.dismiss()
}.also {
it.foodName = foodName
it.payAmount = payAmount.format2String(2)
}
presentation?.show()
}
}
// private fun bindOrder(userId: String, block: () -> Unit) {
// payActivity.bindOrder(userId) { bindResult ->
// if (bindResult.not()) {
// payActivity.hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@bindOrder
// }
// block()
// }
// }
// private fun getMemberInfo(phone: String, key: String, block: (MemberInfo) -> Unit) {
// payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
// if (memberInfo == null) {
// payActivity.hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@getMemberInfoByPhone
// }
// block(memberInfo)
// }
// }
}
@@ -0,0 +1,296 @@
package com.sw.dualscreen.activity.fragment.pay
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentPayResultBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.IntervalExecutor
import com.sw.dualscreen.utils.SpannedUtils
import com.sw.dualscreen.utils.countDownByFlow
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.EventBus
class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
//type = 0,扫码支付默认显示二维码
//type = 1,会员结算显示二维码,是会员则显示名字、头像、手机号、可用余额
//type = 2,支付成功,是会员则显示名字、头像、手机号、可用余额
companion object {
const val PAGE_TYPE = "pageType"
const val MEMBER_INFO = "memberInfo"
fun instance(pageType: Int, memberInfo: MemberInfo?): PayResultFragment {
return PayResultFragment().apply {
arguments = Bundle().also {
it.putInt(PAGE_TYPE, pageType)
it.putParcelable(MEMBER_INFO, memberInfo)
}
}
}
}
private var pageType: Int = 1
private var memberInfo: MemberInfo? = null
override fun inflateViewBinding(): FragmentPayResultBinding {
return FragmentPayResultBinding.inflate(layoutInflater)
}
private var countDownJob: Job? = null
private fun closePageCountDown() {
countDownJob = countDownByFlow(
total = 3,
scope = lifecycleScope,
onStart = {
binding.btnBack.text = "返回(3s)..."
},
onTick = { seconds ->
binding.btnBack.text = "返回(${seconds}s)..."
},
onFinish = {
payActivity.finish()
}
)
}
private lateinit var payActivity: PayActivity
override fun initialize() {
payActivity = activity as PayActivity
binding.btnBack.setOnClickListener {
activity?.finish()
}
arguments?.let {
pageType = it.getInt(PAGE_TYPE, 0)
memberInfo = it.getParcelable(MEMBER_INFO)
}
if (memberInfo != null) {
totalPrice = (payActivity.foodInfo?.vipPrice ?: 0.0) * payActivity.eatNum
binding.layoutVip.visible()
balance = (memberInfo!!.topUpBalance ?: 0.0) + (memberInfo!!.rewardBalance ?: 0.0)
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
expensesBalance = if (balance >= totalPrice) totalPrice else balance
loadUserInfo(memberInfo!!)
} else {
binding.layoutVip.gone()
totalPrice = (payActivity.foodInfo?.specPrice ?: 0.0) * payActivity.eatNum
}
when (pageType) {
0 -> {
}
1 -> {
binding.layoutPayInfo.visible()
binding.layoutPaySuccess.gone()
binding.tvRealAmount.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 36),
TextBean(text = totalPrice.format2String(2), textSize = 60),
)
)
//余额扣除 -36.80 元,扣除后可用余额 10.00 元
//余额扣除 -16.80 元,还需支付 20.00 元
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 24),
TextBean(text = balance.format2String(2), textSize = 36),
)
)
binding.tvAccountInfo.text = SpannedUtils.getAmountText(
getAmountList()
)
if (balance >= totalPrice) {
binding.ivPayQrCode.invisible()
binding.btnConfirmPay.run {
isEnabled = true
//点击支付,接口成功打开成功页面
setOnClickListener {
payActivity.showWaitingDialog("支付中,请稍后……")
memberPay(totalPrice.format2String(2)) {
binding.root.postDelayed({
payActivity.hideWaitingDialog()
payActivity.showPaySuccess(true)
}, 1000)
}
}
}
} else {
binding.ivPayQrCode.visible()
binding.btnConfirmPay.isEnabled = false
val payAmount = totalPrice - balance
payActivity.getQrCodeImg(
orderId = payActivity.foodOrderId,
userId = memberInfo!!.id,
//混合支付,实际支付金额
//totalFee = payAmount.format2String(2)
) { qrCodeImg ->
binding.ivPayQrCode.load(qrCodeImg)
//更新副屏二维码
payActivity.updateQrCodeImage(qrCodeImg)
}
paySuccessCallback {
//扫码成功回调打开成功页面,后端处理余额扣除
payActivity.showPaySuccess(true)
// if (balance > 0.0) {
// memberPay(balance.format2String(2)) {
// //扫码成功+会员支付完成后打开成功页面
// payActivity?.showPaySuccess(true)
// }
// } else {
// }
}
}
}
2 -> {
binding.layoutPayInfo.gone()
binding.layoutPaySuccess.visible()
if (memberInfo != null) {
val remainBalance = if (balance >= totalPrice) balance - totalPrice else 0.0
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 24),
TextBean(text = remainBalance.format2String(2), textSize = 36),
)
)
val showTotalPrice = totalPrice.format2String(2)
binding.tvPayAmount.text = "收款金额 $showTotalPrice"
val showExpensesBalance = expensesBalance.format2String(2)
binding.tvPayInfo.text =
"应收 $showTotalPrice 元,余额扣除 $showExpensesBalance"
} else {
binding.tvPayAmount.text = "收款金额 ${totalPrice.format2String(2)}"
binding.tvPayInfo.text = "应收 ${totalPrice.format2String(2)}"
}
closePageCountDown()
}
else -> {}
}
}
//总价格
private var totalPrice = 0.0
//余额
private var balance = 0.0
//实际支付金额
private var realPayPrice = 0.0
//扣除余额
private var expensesBalance = 0.0
private fun getAmountList(): List<TextBean> {
val list: MutableList<TextBean> = mutableListOf()
list.add(TextBean(text = "余额扣除 ", textSize = 30, textColor = "#FF889AC2"))
list.add(
TextBean(
text = "-${expensesBalance.format2String(2)}",
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
if (balance >= totalPrice) {
//余额大于等于总价格,使用余额支付
list.add(TextBean(text = " 元,扣除后可用余额 ", textSize = 30, textColor = "#FF889AC2"))
val remainingBalance = balance - totalPrice
list.add(
TextBean(
text = remainingBalance.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
} else {
//余额小于总价格,使用余额+扫码支付
//实际支付金额
list.add(TextBean(text = " 元,还需支付 ", textSize = 30, textColor = "#FF889AC2"))
list.add(
TextBean(
text = realPayPrice.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
}
list.add(TextBean(text = "", textSize = 30, textColor = "#FF889AC2"))
return list
}
private fun loadUserInfo(item: MemberInfo) {
binding.ivHeadPic.load(
if(item.faceUrl.isNullOrBlank()) R.drawable.ic_avatar_default
else item.faceUrl
)
binding.tvUserName.text = item.name
val phone = item.phone ?: ""
binding.tvUserPhone.text =
if (phone.length == 11)
phone.replace(phone.substring(3, 7), "****")
else
phone
}
private fun memberPay(total: String, callback: () -> Unit) {
payActivity.run {
memberPay(
param = hashMapOf(
//支付金额
"totalFee" to total,
//订单号
"orderNo" to payActivity.foodOrderId,
//用户id
"memberId" to memberInfo!!.id!!
),
block = callback
)
}
}
override fun onDestroy() {
payTaskJob?.cancel()
super.onDestroy()
}
private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null
fun paySuccessCallback(callback: () -> Unit) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity?.queryOrderState { paySuccess ->
if (paySuccess) {
callback()
payTaskJob?.cancel()
}
}
}
}
}
@@ -0,0 +1,137 @@
package com.sw.dualscreen.activity.fragment.pay
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import androidx.core.text.buildSpannedString
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.presentation.pay.ScanQrCodePayPresentation
import com.sw.dualscreen.utils.countDownByFlow
import kotlinx.coroutines.Job
import com.sw.dualscreen.databinding.FragmentScanQrcodePayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.utils.IntervalExecutor
class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
companion object {
}
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
private var payQrCodePic: String? = null
var presentation: ScanQrCodePayPresentation? = null
private var countDownJob: Job? = null
override fun inflateViewBinding(): FragmentScanQrcodePayBinding {
return FragmentScanQrcodePayBinding.inflate(layoutInflater)
}
override fun initialize() {
payActivity = requireActivity() as PayActivity
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.specPrice?:0.0)*payActivity.eatNum
}
binding.tvRealAmount.text = getAmountText(payAmount.format2String(2))
//不需要绑定会员id
payActivity.getQrCodeImg(
orderId = payActivity.foodOrderId,
//非会员扫码支付,不用传金额,后端处理,混合支付要传
//totalFee = payAmount.format2String(2)
) {
payQrCodePic = it
binding.ivPayQrCode.load(payQrCodePic)
binding.tvPayResult.text = "待支付(60s)..."
paySuccessCallback(false)
showSubScreen()
startCountDown()
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
dismissSubScreen()
countDownJob?.cancel() // 自动取消订阅,防止内存泄漏
payTaskJob?.cancel()
super.onDestroy()
}
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation =
ScanQrCodePayPresentation(activity = payActivity, type = 0, display = displays[1]) {
presentation?.dismiss()
}.also {
it.foodName = foodName
it.totalPrice = payAmount ?: 0.0
it.payQrCodePic = payQrCodePic
}
presentation?.show()
}
}
private fun getAmountText(amount: String): SpannedString {
return buildSpannedString {
append("¥", AbsoluteSizeSpan(36, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
append(amount, AbsoluteSizeSpan(60, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
private fun startCountDown() {
countDownJob = countDownByFlow(
total = 60,
scope = lifecycleScope,
onStart = {
binding.tvPayResult.text = "待支付(60s)..."
},
onTick = { seconds ->
binding.tvPayResult.text = "待支付(${seconds}s)..."
},
onFinish = {
binding.tvPayResult.text = "待支付"
}
)
}
private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null
fun paySuccessCallback(isVip: Boolean) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity.queryOrderState { paySuccess ->
if (paySuccess) {
payActivity.showPaySuccess(isVip)
payTaskJob?.cancel()
}
}
}
}
}
@@ -0,0 +1,41 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFoodBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
class CollectFoodListAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, CollectFoodListAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFoodBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFoodBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodInfo?) {
holder.binding.root.setBackgroundResource(R.drawable.bg_text_collect)
holder.binding.tvFoodName.run {
if (item!!.score == 0) {
text = item.foodName
} else {
val score = (item.score / 100.0).format2String(2)
text = item.foodName + (if (item.score != 0) "-${score}%" else "")
}
isChecked = item.isChecked
setTextColor(
if (isChecked) "#FF3232".toColorInt()
else "#5E7585".toColorInt()
)
}
}
}
@@ -1,34 +0,0 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemCollectedDataBinding
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
import com.sw.dualscreen.objbox.CollectedFoodBean
import com.sw.dualscreen.objbox.FoodCollectionBean
class CollectedFoodAdapter (var list: MutableList<CollectedFoodBean>) :
BaseQuickAdapter<CollectedFoodBean, CollectedFoodAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedDataBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemCollectedDataBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodBean?) {
holder.binding.tvFoodName.text = item?.foodName
holder.binding.divider.run {
visibility = if (position == list.size - 1) View.GONE else View.VISIBLE
}
}
}
@@ -0,0 +1,32 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
BaseQuickAdapter<CollectedFoodInfo, CollectedFoodNewAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemCollectedFoodBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) {
holder.binding.tvFoodName.text = item?.foodName
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
holder.binding.divider.run {
if (position == list.size-1) gone() else visible()
}
}
}
@@ -0,0 +1,38 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFood2Binding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
class DialogSearchFoodAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, DialogSearchFoodAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFood2Binding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFood2Binding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodInfo?) {
holder.binding.tvFoodName.run {
if (item!!.score == 0) {
text = item.foodName
} else {
val score = (item.score / 100.0).format2String(2)
text = item.foodName + (if (item.score != 0) "-${score}%" else "")
}
isChecked = item.isChecked
}
}
}
@@ -9,7 +9,10 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.utils.GlideUtils
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
@@ -30,11 +33,25 @@ class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
binding.imageView.run {
if (it.isShowCamera) {
scaleType = ImageView.ScaleType.CENTER
setImageResource(R.drawable.ic_camera256)
setImageResource(R.drawable.ic_camera_default)
} else {
scaleType = ImageView.ScaleType.FIT_CENTER
//scaleType = ImageView.ScaleType.FIT_CENTER
scaleType = ImageView.ScaleType.CENTER_CROP
//setImageURI(it.imageUri)
setImageBitmap(it.bitmap)
//setImageBitmap(it.bitmap)
if (it.imageUri!=null) {
setImageURI(it.imageUri)
} else {
load(it.imageFile)
}
// it.bitmap?.let { bitmap ->
// GlideUtils.loadRoundCornerWitBitmap(
// context,
// url = bitmap,
// imageView = this,
// radius = 8.dp
// )
// }
}
}
}
@@ -0,0 +1,44 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFoodBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
class MainFoodListAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, MainFoodListAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFoodBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFoodBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodInfo?) {
holder.binding.root.setBackgroundResource(R.drawable.bg_text_main)
holder.binding.tvFoodName.run {
if (item!!.score == 0) {
text = item.foodName
} else {
val score = (item.score / 100.0).format2String(2)
text = item.foodName + (if (item.score != 0) "-${score}%" else "")
}
isChecked = item.isChecked
setTextColor(
if (isChecked) "#FF3232".toColorInt()
else "#0A1428".toColorInt()
)
}
}
}
@@ -5,7 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import com.sw.dualscreen.databinding.DialogWarnBinding
class WarnDialog(
class RemindDialog(
context: Context,
var content:String,
var cancelBlock: () -> Unit = {},
@@ -31,5 +31,4 @@ class WarnDialog(
}
}
}
@@ -20,6 +20,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.math.BigDecimal
import java.math.RoundingMode
/**
* 将 Int 值转换为 dp 值
@@ -65,8 +67,12 @@ val Float.sp: Float
Resources.getSystem().displayMetrics
)
fun Double.format2String(): String = "%.1f".format(this)
fun Double.format2String(num:Int): String = "%.${num}f".format(this)
fun Double.roundedDecimalPlace(num: Int = 2): Double {
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
}
fun Double?.format2String(): String = this.format2String(1)
fun Double?.format2String(num:Int): String = "%.${num}f".format(this?:0.0)
// 添加扩展函数
fun ImageProxy.toSafeBitmap(): Bitmap {
@@ -110,13 +116,46 @@ fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
return@setOnEditorActionListener false
}
}
fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
var job: Job? = null
//fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
// var job: Job? = null
// setOnClickListener {
// job?.cancel()
// job = CoroutineScope(Dispatchers.Main).launch {
// delay(delay)
// action()
// }
// }
//}
/**
* 极简版防重复点击扩展函数
* @param delay 防抖时间(默认300ms
* @param action 点击执行逻辑
*/
fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) {
setOnClickListener {
job?.cancel()
job = CoroutineScope(Dispatchers.Main).launch {
delay(delay)
action()
// 用View的tag存储是否可点击的状态(默认可点击)
if (tag as? Boolean ?: true) {
tag = false // 标记为不可点击
action() // 立即执行点击逻辑
// 启动协程,延迟后恢复可点击状态
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
tag = true // 恢复可点击
}
}
}
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
@@ -2,8 +2,10 @@ package com.sw.dualscreen.ext
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
import com.sw.dualscreen.utils.GlideUtils
/**
* 设置圆角边框
@@ -27,4 +29,8 @@ fun View.setRoundedBorder(
private fun View.dpToPx(dp: Float): Float {
return dp * context.resources.displayMetrics.density
}
fun ImageView.load(url: Any?) {
GlideUtils.loadImage(context, url, this)
}
@@ -1,17 +1,10 @@
package com.sw.dualscreen.model.response
data class ApiResponse<T>(
val code: Int? = 0,
val message: String? = "",
val code: String,
val msg: String? = "",
val data: T? = null,
val result: T? = null,
val success: Boolean? = false,
val timestamp: Long? = 0
) {
fun isSuccess(): Boolean {
return success == true
}
}
)
@@ -0,0 +1,160 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
data class FoodSearchReq(
var nameList: List<String>,
//"订单来源权举:1-营养秤,2-档口
var deviceType: Int = 2
)
data class FoodOrder(
//设备id
var deviceId: String,
//菜品id
var foodId: String,
//菜品名称
var foodName: String,
//菜品营养表id
var foodMaterialId: String,
//餐品规格ID
var specId: String,
//用户id(档口机,内部员工直接取餐:获取的用户ID/memberId
var userId: String? = null,
//食物重量
var foodWeight: Int,
//食用重量
var eatWeight: Int,
//份数
var eatNum: Int,
//是否需要付款
var notPay: Boolean,
//备注
var remark: String? = "",
//即放即取-1,称重-2
var mode: Int
)
@Parcelize
data class MemberInfo(
//会员id
val id: String?,
//刷脸识别用户ID
val faceUserId: String?,
//手机号
val phone: String?,
//姓名
val name: String?,
//头像
val faceUrl: String?,
//充值余额
val topUpBalance: Double?,
//赠送余额
val rewardBalance: Double?,
//积分余额
val integralBalance: Int?
) : Parcelable
data class TextBean(
var text: String,
var textSize: Int = 0,
var textColor: String? = null,
var isBold: Boolean = false
)
data class UserNutrition(
//姓名
val name: String?,
//推荐能量
// val recommendEnergy: Double?,
//热量
val calorie: Double?,
//果蔬
val fruitsVegetables: Double?,
//肉蛋
val meatEggs: Double?,
//主食
val stapleFood: Double?,
//能量最大值
val maxCalorie: Double? = null,
//能量最小值
val minCalorie: Double? = null,
//推荐能量
val recommendCalorie: Double? = null,
//主食推荐值
val stapleFoodRecommend: String? = null,
//主食即将超量
val stapleFoodNearExcess: String? = null,
//主食超量
val stapleFoodExcess: String? = null,
//果蔬推荐值
val fruitsVegetablesRecommend: String? = null,
//果蔬即将超量
val fruitsVegetablesNearExcess: String? = null,
//果蔬超量
val fruitsVegetablesExcess: String? = null,
//肉蛋推荐值
val meatEggsRecommend: String? = null,
//肉蛋即将超量
val meatEggsNearExcess: String? = null,
//肉蛋超量
val meatEggsExcess: String? = null,
)
//{"code":"00000","data":"{\"orderType\":0,\"orderPayFrom\":2,\"payType\":\"1\",\"paySuc\":0,\"totalFee\":0.01,\"orderCode\":\"1998193614899372032\",\"message\":\"支付成功\",\"payCode\":\"1998193614899372032508227\"}","msg":"成功","total":0}
data class PayResult(
val paySuc: String? = null
)
data class UserEnergy(
var calorie: Double,
var grain: Double,
var fruitsVegetables: Double,
var meatEggs: Double,
)
data class RespCodeMsg(
val code: String?,
val msg: String?
)
data class PostEvent(var name: String = "")
data class UpdateRefreshEvent(var name:String = "")
data class PaySuccessEvent(
var name: String = ""
)
data class ChargeModeEvent(
var chargeMode: Int = 0
)
data class ClickBackEvent(
var name: String = ""
)
data class DeviceConfig(
var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null
)
data class FoodVector(
val id: String? = null,
val foodId: String? = null,
val foodName: String? = null,
val foodVector: String? = null,
val version: String? = null
)
data class ResetBoxEvent(
var num: Int = 0
)
data class WeightRecord(
var foodInfo: FoodInfo? = null,
var lastWeight: Int = 0,
var currentWeight: Int = 0
)
@@ -1,42 +1,47 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class DinnerTypeInfo(
@SerializedName("dinnerType")
val dinnerType: DinnerType? = DinnerType()
) : Parcelable {
@Parcelize
data class DinnerType(
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("delFlag")
val delFlag: Int? = 0,
@SerializedName("dinnerType")
val dinnerType: String? = "",
@SerializedName("endTime")
val endTime: String? = "",
@SerializedName("id")
val id: Int? = 0,
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("isSyncCopy")
val isSyncCopy: Int? = 0,
@SerializedName("restId")
val restId: String? = "",
@SerializedName("startTime")
val startTime: String? = "",
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = ""
) : Parcelable
}
//{"id":"1992804086323437569","dinnerType":"晚餐","chargeType":1,"fixedAmount":3.0000}
data class DinnerType(
val id: String? = "",
//餐次
val dinnerType: String? = "",
//餐次收费模式(0称重,1固定,2不收费)
val chargeType: Int = 0,
//固定收费(元) 仅charge_type = 1 时
val fixedAmount: Double?=null
)
//@Parcelize
//data class DinnerTypeInfo(
// @SerializedName("dinnerType")
// val dinnerType: DinnerType? = DinnerType()
//) : Parcelable
//
//@Parcelize
//data class DinnerType(
// @SerializedName("createBy")
// val createBy: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("delFlag")
// val delFlag: Int? = 0,
// @SerializedName("dinnerType")
// val dinnerType: String? = "",
// @SerializedName("endTime")
// val endTime: String? = "",
// @SerializedName("id")
// val id: Int? = 0,
// @SerializedName("isSync")
// val isSync: Int? = 0,
// @SerializedName("isSyncCopy")
// val isSyncCopy: Int? = 0,
// @SerializedName("restId")
// val restId: String? = "",
// @SerializedName("startTime")
// val startTime: String? = "",
// @SerializedName("sysOrgCode")
// val sysOrgCode: String? = "",
// @SerializedName("updateBy")
// val updateBy: String? = "",
// @SerializedName("updateTime")
// val updateTime: String? = ""
//) : Parcelable
@@ -12,11 +12,11 @@ data class EquipmentInfo(
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
var arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
var arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
var arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
@@ -24,9 +24,9 @@ data class EquipmentInfo(
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
var canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
var canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
@@ -1,6 +1,5 @@
package com.sw.dualscreen.model.response
import android.net.Uri
import android.os.Parcelable
import android.text.TextUtils
@@ -12,291 +11,315 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class FoodInfo(
@SerializedName("foodLabel")
val foodLabel: String? = "",
@SerializedName("foodName")
val foodName: String? = "",
@SerializedName("foodTypeAndRealIntakeVoList")
val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
/**
* 食物id
*/
//菜品id
val foodId: String,
//菜品名称
val foodName: String? = null,
//能量
val calorie: Double? = null,
//蛋白质
val protein: Double? = null,
//脂肪
val fat: Double? = null,
//碳水化合物
val carbohydrate: Double? = null,
//规格售卖价格(元)
val specPrice: Double? = null,
//VIP售卖价(元)
val vipPrice: Double? = null,
//餐品营养id
val foodMaterialId: String? = null,
//菜品规格id
val specId: String? = null,
//规格重量(g)
val specWeight: Double? = null,
//主食
val stapleFood: Double? = null,
//果蔬
val fruitsVegetables: Double? = null,
//肉蛋
val meatEggs: Double? = null,
//推荐能量
val recommendCalorie: Double? = null,
//图片
var foodImg: String? = "",
//-----------------------------------
var score: Int = 0,
var isChecked: Boolean = false,
var photoUri: Uri? = null,
//true-搜索结果数据,false-识别查询数据
var isFromSearch: Boolean? = null
// @SerializedName("foodTypeAndRealIntakeVoList")
// val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo>? = listOf(),
// @SerializedName("stFoodInfoMaterial")
// val stFoodInfoMaterial: StFoodInfoMaterial? = StFoodInfoMaterial(),
// @SerializedName("stFoodInfoPagoda")
// val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
// @SerializedName("stFoodInfoPagodaAPPVO")
// val stFoodInfoPagodaAPPVO: StFoodInfoPagodaAPPVO? = StFoodInfoPagodaAPPVO(),
// @SerializedName("stFoodInfoSetting")
// val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
// @SerializedName("stFoodInfoSpecificationList")
// val stFoodInfoSpecificationList: List<StFoodInfoSpecification>? = listOf(),
) : Parcelable
//@Parcelize
//data class FoodTypeAndRealIntakeVo(
// @SerializedName("childMaterClassName")
// val childMaterClassName: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("goodsId")
// val goodsId: String? = "",
// @SerializedName("goodsName")
// val goodsName: String? = "",
// @SerializedName("materClassName")
// val materClassName: String? = "",
// @SerializedName("materId")
// val materId: String? = "",
// @SerializedName("materialType")
// val materialType: String? = "",
// @SerializedName("realityIntake")
// val realityIntake: String? = ""
//) : Parcelable
@Parcelize
data class StFoodInfoMaterial(
@SerializedName("ash")
val ash: Double? = 0.0,
@SerializedName("avitE")
val avitE: Double? = 0.0,
@SerializedName("ca")
val ca: Double? = 0.0,
@SerializedName("cho")
val cho: Double? = 0.0,
@SerializedName("cholesterol")
val cholesterol: Double? = 0.0,
@SerializedName("cu")
val cu: Double? = 0.0,
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("dietFiber")
val dietFiber: Double? = 0.0,
@SerializedName("elementI")
val elementI: Double? = 0.0,
@SerializedName("elementK")
val elementK: String? = "",
@SerializedName("elementP")
val elementP: String? = "",
@SerializedName("energyKcal")
val energyKcal: Double? = 0.0,
@SerializedName("energyKj")
val energyKj: Double? = 0.0,
@SerializedName("fat")
val fat: Double? = 0.0,
@SerializedName("fe")
val fe: Double? = 0.0,
@SerializedName("folate")
val folate: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("historyStatus")
val historyStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("imgUrl")
var imgUrl: String? = "",
/**
* 拍照结果
*/
var photoUri: Uri? = null,
@SerializedName("stFoodInfoMaterial")
val stFoodInfoMaterial: StFoodInfoMaterial? = StFoodInfoMaterial(),
@SerializedName("stFoodInfoPagoda")
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
@SerializedName("stFoodInfoPagodaAPPVO")
val stFoodInfoPagodaAPPVO: StFoodInfoPagodaAPPVO? = StFoodInfoPagodaAPPVO(),
@SerializedName("stFoodInfoSetting")
val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
@SerializedName("stFoodInfoSpecificationList")
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf(),
var score: Int = 0
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("mg")
val mg: Double? = 0.0,
@SerializedName("mn")
val mn: Double? = 0.0,
@SerializedName("na")
val na: Double? = 0.0,
@SerializedName("naicin")
val naicin: Double? = 0.0,
@SerializedName("protein")
val protein: Double? = 0.0,
@SerializedName("retionl")
val retionl: Double? = 0.0,
@SerializedName("riboflavin")
val riboflavin: Double? = 0.0,
@SerializedName("se")
val se: Double? = 0.0,
@SerializedName("thiamin")
val thiamin: Double? = 0.0,
@SerializedName("totCarotene")
val totCarotene: Double? = 0.0,
@SerializedName("vitA")
val vitA: Double? = 0.0,
@SerializedName("vitB12")
val vitB12: Double? = 0.0,
@SerializedName("vitB6")
val vitB6: Double? = 0.0,
@SerializedName("vitC")
val vitC: Double? = 0.0,
@SerializedName("vitE")
val vitE: Double? = 0.0,
@SerializedName("water")
val water: Double? = 0.0,
@SerializedName("zn")
val zn: Double? = 0.0
) : Parcelable
//@Parcelize
//data class StFoodInfoPagoda(
// @SerializedName("aquatic")
// val aquatic: String? = "",
// @SerializedName("birds")
// val birds: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("egg")
// val egg: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("fruits")
// val fruits: String? = "",
// @SerializedName("grain")
// val grain: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("livestock")
// val livestock: String? = "",
// @SerializedName("milk")
// val milk: String? = "",
// @SerializedName("nuts")
// val nuts: String? = "",
// @SerializedName("oil")
// val oil: String? = "",
// @SerializedName("potato")
// val potato: String? = "",
// @SerializedName("salt")
// val salt: String? = "",
// @SerializedName("soya")
// val soya: String? = "",
// @SerializedName("sugar")
// val sugar: String? = "",
// @SerializedName("vegetable")
// val vegetable: String? = ""
//) : Parcelable
@Parcelize
data class StFoodInfoPagodaAPPVO(
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable {
@Parcelize
data class FoodTypeAndRealIntakeVo(
@SerializedName("childMaterClassName")
val childMaterClassName: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("goodsId")
val goodsId: String? = "",
@SerializedName("goodsName")
val goodsName: String? = "",
@SerializedName("materClassName")
val materClassName: String? = "",
@SerializedName("materId")
val materId: String? = "",
@SerializedName("materialType")
val materialType: String? = "",
@SerializedName("realityIntake")
val realityIntake: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoMaterial(
@SerializedName("ash")
val ash: Double? = 0.0,
@SerializedName("avitE")
val avitE: Double? = 0.0,
@SerializedName("ca")
val ca: Double? = 0.0,
@SerializedName("cho")
val cho: Double? = 0.0,
@SerializedName("cholesterol")
val cholesterol: Double? = 0.0,
@SerializedName("cu")
val cu: Double? = 0.0,
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("dietFiber")
val dietFiber: Double? = 0.0,
@SerializedName("elementI")
val elementI: Double? = 0.0,
@SerializedName("elementK")
val elementK: String? = "",
@SerializedName("elementP")
val elementP: String? = "",
@SerializedName("energyKcal")
val energyKcal: Double? = 0.0,
@SerializedName("energyKj")
val energyKj: Double? = 0.0,
@SerializedName("fat")
val fat: Double? = 0.0,
@SerializedName("fe")
val fe: Double? = 0.0,
@SerializedName("folate")
val folate: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("historyStatus")
val historyStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("mg")
val mg: Double? = 0.0,
@SerializedName("mn")
val mn: Double? = 0.0,
@SerializedName("na")
val na: Double? = 0.0,
@SerializedName("naicin")
val naicin: Double? = 0.0,
@SerializedName("protein")
val protein: Double? = 0.0,
@SerializedName("retionl")
val retionl: Double? = 0.0,
@SerializedName("riboflavin")
val riboflavin: Double? = 0.0,
@SerializedName("se")
val se: Double? = 0.0,
@SerializedName("thiamin")
val thiamin: Double? = 0.0,
@SerializedName("totCarotene")
val totCarotene: Double? = 0.0,
@SerializedName("vitA")
val vitA: Double? = 0.0,
@SerializedName("vitB12")
val vitB12: Double? = 0.0,
@SerializedName("vitB6")
val vitB6: Double? = 0.0,
@SerializedName("vitC")
val vitC: Double? = 0.0,
@SerializedName("vitE")
val vitE: Double? = 0.0,
@SerializedName("water")
val water: Double? = 0.0,
@SerializedName("zn")
val zn: Double? = 0.0
) : Parcelable
fun fruitsValue(): Double =
if (TextUtils.isEmpty(fruits))
0.0
else fruits!!.toDouble()
@Parcelize
data class StFoodInfoPagoda(
@SerializedName("aquatic")
val aquatic: String? = "",
@SerializedName("birds")
val birds: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("egg")
val egg: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("livestock")
val livestock: String? = "",
@SerializedName("milk")
val milk: String? = "",
@SerializedName("nuts")
val nuts: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("potato")
val potato: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = ""
) : Parcelable
fun grainValue(): Double =
if (TextUtils.isEmpty(grain))
0.0
else grain!!.toDouble()
@Parcelize
data class StFoodInfoPagodaAPPVO(
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable {
fun vegetableValue(): Double =
if (TextUtils.isEmpty(vegetable))
0.0
else vegetable!!.toDouble()
fun fruitsValue(): Double =
if (TextUtils.isEmpty(fruits))
0.0
else fruits!!.toDouble()
fun meatValue(): Double =
if (TextUtils.isEmpty(meat))
0.0
else meat!!.toDouble()
}
fun grainValue(): Double =
if (TextUtils.isEmpty(grain))
0.0
else grain!!.toDouble()
//@Parcelize
//data class StFoodInfoSetting(
// @SerializedName("addFoodWeight")
// val addFoodWeight: String? = "",
// @SerializedName("bowlPlateWeight")
// val bowlPlateWeight: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("foodStatus")
// val foodStatus: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("inventoryStatus")
// val inventoryStatus: Boolean? = false,
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("replenishWeight")
// val replenishWeight: String? = "",
// @SerializedName("residueType")
// val residueType: String? = "",
// @SerializedName("tablewareStatus")
// val tablewareStatus: Boolean? = false,
// @SerializedName("tablewareWeight")
// val tablewareWeight: String? = "",
// @SerializedName("warningStatus")
// val warningStatus: String? = "",
// @SerializedName("weighStatus")
// val weighStatus: String? = "",
// @SerializedName("weighUnitId")
// val weighUnitId: String? = "",
// @SerializedName("weighUnitName")
// val weighUnitName: String? = ""
//) : Parcelable
fun vegetableValue(): Double =
if (TextUtils.isEmpty(vegetable))
0.0
else vegetable!!.toDouble()
fun meatValue(): Double =
if (TextUtils.isEmpty(meat))
0.0
else meat!!.toDouble()
}
@Parcelize
data class StFoodInfoSetting(
@SerializedName("addFoodWeight")
val addFoodWeight: String? = "",
@SerializedName("bowlPlateWeight")
val bowlPlateWeight: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodStatus")
val foodStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("inventoryStatus")
val inventoryStatus: Boolean? = false,
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("replenishWeight")
val replenishWeight: String? = "",
@SerializedName("residueType")
val residueType: String? = "",
@SerializedName("tablewareStatus")
val tablewareStatus: Boolean? = false,
@SerializedName("tablewareWeight")
val tablewareWeight: String? = "",
@SerializedName("warningStatus")
val warningStatus: String? = "",
@SerializedName("weighStatus")
val weighStatus: String? = "",
@SerializedName("weighUnitId")
val weighUnitId: String? = "",
@SerializedName("weighUnitName")
val weighUnitName: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoSpecification(
@SerializedName("bowlPlateWeight")
val bowlPlateWeight: Double? = 0.0,
@SerializedName("defaultStatus")
val defaultStatus: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("specId")
val specId: String? = "",
@SerializedName("specName")
val specName: String? = "",
@SerializedName("specPrice")
val specPrice: String? = "",
@SerializedName("specWeight")
val specWeight: Double? = 0.0
) : Parcelable
}
//@Parcelize
//data class StFoodInfoSpecification(
// @SerializedName("bowlPlateWeight")
// val bowlPlateWeight: Double? = 0.0,
// @SerializedName("defaultStatus")
// val defaultStatus: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("specId")
// val specId: String? = "",
// @SerializedName("specName")
// val specName: String? = "",
// @SerializedName("specPrice")
// val specPrice: String? = "",
// @SerializedName("specWeight")
// val specWeight: Double? = 0.0
//) : Parcelable
@@ -10,14 +10,43 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class UserFaceModel(
@SerializedName("faceFeature")
val faceFeature: String? = "",
@SerializedName("faceFeatureString")
// @SerializedName("faceFeature")
// val faceFeature: String? = "",
// @SerializedName("faceFeatureString")
// val faceFeatureString: String? = "",
// @SerializedName("faceType")
// val faceType: String? = "",
// @SerializedName("userFaceId")
// val userFaceId: String? = "",
// @SerializedName("userId")
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp:Long?=null,
val faceDeleted: Boolean? = false,
/**
* 会员编号
*/
val cardNo: String,
/**
* 是否会员
*/
val isMember: Boolean
) : Parcelable
@Parcelize
data class UserFaceModel2(
val userId: String? = "",
val faceFeatureString: String? = "",
@SerializedName("faceType")
val faceType: String? = "",
@SerializedName("userFaceId")
val userFaceId: String? = "",
@SerializedName("userId")
val userId: String? = ""
) : Parcelable
val face: String? = ""
) : Parcelable
data class FaceData(
val nextPageIndex:Int,
val total:Int,
val size:Int,
val current:Int,
val pages:Int,
val records: List<UserFaceModel2>?=null
)
@@ -1,253 +1,253 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import android.text.TextUtils
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 就餐数据
*/
@Parcelize
data class UserNutritionData(
/**
* 总胆固醇
*/
@SerializedName("cho")
val cho: String? = "",
/**
* 饮食纤维
*/
@SerializedName("dietFiber")
val dietFiber: String? = "",
/**
* 能量
*/
@SerializedName("energy")
val energy: String? = "",
/**
* 脂肪
*/
@SerializedName("fat")
val fat: String? = "",
/**
* 食物类型和实际摄入量清单
*/
@SerializedName("foodTypeAndRealIntakeVoList")
val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("message")
val message: String? = "",
/**
* 钠
*/
@SerializedName("na")
val na: String? = "",
/**
* 蛋白质
*/
@SerializedName("protein")
val protein: String? = "",
/**
* 最大推荐热量
*/
@SerializedName("recommendMax")
val recommendMax: String? = "",
/**
* 最小推荐热量
*/
@SerializedName("recommendMin")
val recommendMin: String? = "",
/**
* 热量详情
*/
@SerializedName("stFoodInfoPagoda")
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
@SerializedName("stUserFoodInfoList")
val stUserFoodInfoList: List<StUserFoodInfo?>? = listOf(),
/**
* 每日总热量
*/
@SerializedName("totalEnergyCalculateScore")
val totalEnergyCalculateScore: String? = "",
/**
* 用户所在部门
*/
@SerializedName("userDept")
val userDept: String? = "",
/**
* 用户id
*/
@SerializedName("userId")
val userId: String? = "",
/**
* 姓名
*/
@SerializedName("userName")
val userName: String? = ""
) : Parcelable {
fun recommendMaxValue(): Double =
if (TextUtils.isEmpty(recommendMax)) 0.0
else recommendMax!!.toDouble()
fun recommendMinValue(): Double =
if (TextUtils.isEmpty(recommendMin)) 0.0
else recommendMin!!.toDouble()
fun energyValue(): Double =
if (TextUtils.isEmpty(energy)) 0.0
else energy!!.toDouble()
fun totalEnergyCalculateScoreValue(): Double =
if (TextUtils.isEmpty(totalEnergyCalculateScore)) 0.0
else totalEnergyCalculateScore!!.toDouble()
@Parcelize
data class FoodTypeAndRealIntakeVo(
@SerializedName("childMaterClassName")
val childMaterClassName: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("goodsId")
val goodsId: String? = "",
@SerializedName("goodsName")
val goodsName: String? = "",
@SerializedName("materClassName")
val materClassName: String? = "",
@SerializedName("materId")
val materId: String? = "",
@SerializedName("materialType")
val materialType: String? = "",
@SerializedName("realityIntake")
val realityIntake: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoPagoda(
/**
* 水果
*/
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
/**
* 粮食
*/
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
/**
* 肉
*/
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
/**
* 油
*/
@SerializedName("oil")
val oil: String? = "",
/**
* 盐
*/
@SerializedName("salt")
val salt: String? = "",
/**
* 大豆
*/
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
/**
* 糖
*/
@SerializedName("sugar")
val sugar: String? = "",
/**
* 蔬菜
*/
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable {
fun fruitsValue(): Double =
if (TextUtils.isEmpty(fruits))
0.0
else fruits!!.toDouble()
fun grainValue(): Double =
if (TextUtils.isEmpty(grain))
0.0
else grain!!.toDouble()
fun vegetableValue(): Double =
if (TextUtils.isEmpty(vegetable))
0.0
else vegetable!!.toDouble()
fun meatValue(): Double =
if (TextUtils.isEmpty(meat))
0.0
else meat!!.toDouble()
}
@Parcelize
data class StUserFoodInfo(
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("dataSource")
val dataSource: String? = "",
@SerializedName("dataType")
val dataType: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("deviceId")
val deviceId: String? = "",
@SerializedName("dinnerType")
val dinnerType: String? = "",
@SerializedName("eatDay")
val eatDay: String? = "",
@SerializedName("eatNum")
val eatNum: String? = "",
@SerializedName("eatWeight")
val eatWeight: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodImg")
val foodImg: String? = "",
@SerializedName("foodMaterialId")
val foodMaterialId: String? = "",
@SerializedName("foodName")
val foodName: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("specId")
val specId: String? = "",
@SerializedName("stBasicDiningInformationUserESVo")
val stBasicDiningInformationUserESVo: String? = "",
@SerializedName("stallType")
val stallType: String? = "",
@SerializedName("type")
val type: String? = "",
@SerializedName("userId")
val userId: String? = ""
) : Parcelable
}
//package com.sw.dualscreen.model.response
//
//
//import android.os.Parcelable
//import android.text.TextUtils
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
//
///**
// * 就餐数据
// */
//@Parcelize
//data class UserNutritionData(
// /**
// * 总胆固醇
// */
// @SerializedName("cho")
// val cho: String? = "",
// /**
// * 饮食纤维
// */
// @SerializedName("dietFiber")
// val dietFiber: String? = "",
// /**
// * 能量
// */
// @SerializedName("energy")
// val energy: String? = "",
// /**
// * 脂肪
// */
// @SerializedName("fat")
// val fat: String? = "",
// /**
// * 食物类型和实际摄入量清单
// */
// @SerializedName("foodTypeAndRealIntakeVoList")
// val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
// @SerializedName("foodWeight")
// val foodWeight: String? = "",
// @SerializedName("message")
// val message: String? = "",
// /**
// * 钠
// */
// @SerializedName("na")
// val na: String? = "",
// /**
// * 蛋白质
// */
// @SerializedName("protein")
// val protein: String? = "",
// /**
// * 最大推荐热量
// */
// @SerializedName("recommendMax")
// val recommendMax: String? = "",
// /**
// * 最小推荐热量
// */
// @SerializedName("recommendMin")
// val recommendMin: String? = "",
// /**
// * 热量详情
// */
// @SerializedName("stFoodInfoPagoda")
// val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
// @SerializedName("stUserFoodInfoList")
// val stUserFoodInfoList: List<StUserFoodInfo?>? = listOf(),
// /**
// * 每日总热量
// */
// @SerializedName("totalEnergyCalculateScore")
// val totalEnergyCalculateScore: String? = "",
// /**
// * 用户所在部门
// */
// @SerializedName("userDept")
// val userDept: String? = "",
// /**
// * 用户id
// */
// @SerializedName("userId")
// val userId: String? = "",
// /**
// * 姓名
// */
// @SerializedName("userName")
// val userName: String? = ""
//) : Parcelable {
// fun recommendMaxValue(): Double =
// if (TextUtils.isEmpty(recommendMax)) 0.0
// else recommendMax!!.toDouble()
//
// fun recommendMinValue(): Double =
// if (TextUtils.isEmpty(recommendMin)) 0.0
// else recommendMin!!.toDouble()
//
// fun energyValue(): Double =
// if (TextUtils.isEmpty(energy)) 0.0
// else energy!!.toDouble()
//
// fun totalEnergyCalculateScoreValue(): Double =
// if (TextUtils.isEmpty(totalEnergyCalculateScore)) 0.0
// else totalEnergyCalculateScore!!.toDouble()
//
// @Parcelize
// data class FoodTypeAndRealIntakeVo(
// @SerializedName("childMaterClassName")
// val childMaterClassName: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("goodsId")
// val goodsId: String? = "",
// @SerializedName("goodsName")
// val goodsName: String? = "",
// @SerializedName("materClassName")
// val materClassName: String? = "",
// @SerializedName("materId")
// val materId: String? = "",
// @SerializedName("materialType")
// val materialType: String? = "",
// @SerializedName("realityIntake")
// val realityIntake: String? = ""
// ) : Parcelable
//
// @Parcelize
// data class StFoodInfoPagoda(
// /**
// * 水果
// */
// @SerializedName("fruits")
// val fruits: String? = "",
// @SerializedName("fruitsRecommend")
// val fruitsRecommend: String? = "",
// /**
// * 粮食
// */
// @SerializedName("grain")
// val grain: String? = "",
// @SerializedName("grainRecommend")
// val grainRecommend: String? = "",
// /**
// * 肉
// */
// @SerializedName("meat")
// val meat: String? = "",
// @SerializedName("meatRecommend")
// val meatRecommend: String? = "",
// /**
// * 油
// */
// @SerializedName("oil")
// val oil: String? = "",
// /**
// * 盐
// */
// @SerializedName("salt")
// val salt: String? = "",
// /**
// * 大豆
// */
// @SerializedName("soya")
// val soya: String? = "",
// @SerializedName("soyaRecommend")
// val soyaRecommend: String? = "",
// /**
// * 糖
// */
// @SerializedName("sugar")
// val sugar: String? = "",
// /**
// * 蔬菜
// */
// @SerializedName("vegetable")
// val vegetable: String? = "",
// @SerializedName("vegetableRecommend")
// val vegetableRecommend: String? = ""
// ) : Parcelable {
// fun fruitsValue(): Double =
// if (TextUtils.isEmpty(fruits))
// 0.0
// else fruits!!.toDouble()
//
// fun grainValue(): Double =
// if (TextUtils.isEmpty(grain))
// 0.0
// else grain!!.toDouble()
//
// fun vegetableValue(): Double =
// if (TextUtils.isEmpty(vegetable))
// 0.0
// else vegetable!!.toDouble()
//
// fun meatValue(): Double =
// if (TextUtils.isEmpty(meat))
// 0.0
// else meat!!.toDouble()
//
// }
//
// @Parcelize
// data class StUserFoodInfo(
// @SerializedName("canteenId")
// val canteenId: String? = "",
// @SerializedName("createBy")
// val createBy: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("dataSource")
// val dataSource: String? = "",
// @SerializedName("dataType")
// val dataType: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("deviceId")
// val deviceId: String? = "",
// @SerializedName("dinnerType")
// val dinnerType: String? = "",
// @SerializedName("eatDay")
// val eatDay: String? = "",
// @SerializedName("eatNum")
// val eatNum: String? = "",
// @SerializedName("eatWeight")
// val eatWeight: Double? = 0.0,
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("foodImg")
// val foodImg: String? = "",
// @SerializedName("foodMaterialId")
// val foodMaterialId: String? = "",
// @SerializedName("foodName")
// val foodName: String? = "",
// @SerializedName("foodWeight")
// val foodWeight: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("specId")
// val specId: String? = "",
// @SerializedName("stBasicDiningInformationUserESVo")
// val stBasicDiningInformationUserESVo: String? = "",
// @SerializedName("stallType")
// val stallType: String? = "",
// @SerializedName("type")
// val type: String? = "",
// @SerializedName("userId")
// val userId: String? = ""
// ) : Parcelable
//}
@@ -3,21 +3,30 @@ package com.sw.dualscreen.network.api
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DinnerTypeInfo
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.objbox.CollectedFoodInfo
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.QueryMap
import retrofit2.http.Url
interface ApiService {
@@ -25,76 +34,119 @@ interface ApiService {
/**
* device获取token
*/
@GET("sys/getEquipmentToken")
suspend fun getDeviceToken(
@Query("qrcodeId") qrcodeId: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion
): ApiResponse<String>
// @GET("sys/getEquipmentToken")
// suspend fun getDeviceToken(
// @Query("qrcodeId") qrcodeId: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion
// ): ApiResponse<String>
/**
*获取配置信息
*/
@GET("equipment/stEquipment/queryByEquipmentCode")
suspend fun getDeviceInfo(
@Query("equipmentCode") equipmentCode: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Header("X-Access-Token") token: String
): ApiResponse<EquipmentInfo>
// @GET("equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Header("X-Access-Token") token: String
// ): ApiResponse<EquipmentInfo>
/**
* 获取业务服务器token
*/
@GET
suspend fun getEquipmentToken(
@Url url: String = "${GlobalData.appBaseUrl}/sys/getEquipmentToken",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("qrcodeId") qrcodeId: String
): ApiResponse<String>
// @GET
// suspend fun getEquipmentToken(
// @Url url: String = "${GlobalData.appBaseUrl}/sys/getEquipmentToken",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("qrcodeId") qrcodeId: String
// ): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/stapi/cquser/getUserFaceCache/v2",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("pageIndex") pageIndex: Int
): ApiResponse<UserFaceInfo>
// @Url url: String = "${GlobalData.appBaseUrl}/stapi/cquser/getUserFaceCache/v2",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("pageIndex") pageIndex: Int
@Body param: Map<String, Int>
// @Query("pageNum") pageNum: Int,
// @Query("pageSize") pageSize: Int
): ApiResponse<List<UserFaceModel>?>
@GET
suspend fun getUserFaceCache2(
@Url url: String = "http://192.168.1.230:2223/userface/swUserFaceimgSub/list",
@Query("pageNum") pageNum: Int,
@Query("pageSize") pageSize: Int
): ApiResponse<FaceData>
/**
* 获取已采集数据列表
*/
@POST
suspend fun getCollectedFoodList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/dishPage",
@Body param: Map<String, String>
): ApiResponse<List<CollectedFoodInfo>>
// /**
// * 获取档口菜品信息
// */
// @GET
// suspend fun getRestInfoFoodsByType(
//// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall",
//// @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getFoodsByFoodNamePage",
// @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getFoodsByFoodNameList",
// @Query("name") foodName: String,
//// @Query("appVersion") appVersion: String = GlobalData.appVersion,
//// @Query("restId") restId: String,
//// @Query("type") type: Int,
//// @Query("foodName") foodName: String,
// ): ApiResponse<List<FoodInfo>>
/**
* 获取档口菜品信息
*/
@GET
@POST
suspend fun getRestInfoFoodsByType(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("restId") restId: String,
@Query("type") type: Int,
@Query("foodName") foodName: String,
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getFoodsByFoodNameList",
@Body param: HashMap<String, String>
): ApiResponse<List<FoodInfo>>
// /**
// * 通过用户信息获取就餐数据
// */
// @GET
// suspend fun getUserNutritionData(
// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getUserNutritionData/face",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("restId") restId: String,
// @Query("userId") userId: String,
// @Query("foodId") foodId: String,
// ): ApiResponse<UserNutritionData>
/**
* 通过用户信息获取就餐数据
*/
@GET
suspend fun getUserNutritionData(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getUserNutritionData/face",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("restId") restId: String,
@Query("userId") userId: String,
@Query("foodId") foodId: String,
): ApiResponse<UserNutritionData>
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getUserCurrentFoodDetails",
@Query("id") userId: String
): ApiResponse<UserNutrition>
/**
* 获取当前餐点类型
*/
@GET
suspend fun getDinnerType(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getCanteenDinnerType",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("canteenId") restId: String,
): ApiResponse<DinnerTypeInfo>
// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getCanteenDinnerType",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getRegionRule",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("canteenId") restId: String,
): ApiResponse<DinnerType>
/**
* 提交就餐数据
@@ -108,23 +160,145 @@ interface ApiService {
/**
* 获取菜品信息
*/
@GET
@POST
suspend fun getFoodInfo(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
@Query("restId") restId: String,
@Query("foodNames") foodName: String,
// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/goodsList",
// @Query("restId") restId: String,
// @Query("foodNames") foodName: String
@Body req: FoodSearchReq
): ApiResponse<List<FoodInfo>>
// /**
// * 查询支付结果
// */
// @POST
// suspend fun queryOrderState(
// @Url url: String = "${GlobalData.appBaseUrl}/pay/yx-check-out-pay/turnOrderInfo",
// @Body param: HashMap<String, String>
// ): ApiResponse<PayResult?>
/**
* 查询支付结果
*/
@GET
suspend fun queryOrderState(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/turnOrderInfo",
@Query("orderNo") orderNo: String
// @Body param: HashMap<String, String>
): ApiResponse<Any?>
/**
* 现金支付
*/
@POST
suspend fun cashPay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/cashPayment",
@Body param: HashMap<String, String>
): ApiResponse<Boolean?>
/**
* 会员支付
*/
@POST
suspend fun memberPay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/memberPay",
@Body param: HashMap<String, String>
): ApiResponse<Any?>
/**
* 获取支付二维码
*/
@GET
suspend fun getQrCodeImg(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/getQrCodeImg",
@Query("orderNo") orderNo: String,
@Query("memberId") memberId: String?,
@Query("totalFee") totalFee: String? = null
): ApiResponse<String?>
/**
* 开餐-生成订单
*/
@POST
suspend fun createOrder(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/userEatFood/doubleScale",
@Body order: FoodOrder
): ApiResponse<Any?>
/**
* APP根据id获取会员或员工信息及余额
*/
@GET
suspend fun getMemberInfoById(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getUserInfoBalanceById",
@Query("id") memberId: String,
): ApiResponse<MemberInfo?>
/**
* 根据手机尾号和密钥查询员工信息及余额
*/
@POST
suspend fun getMemberInfoByPhone(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/queryUserInfoBalanceByPhone",
@Body param: HashMap<String, String>
): ApiResponse<MemberInfo?>
/**
* 绑定用户与订单号
*/
@GET
suspend fun bindOrder(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/bingOrder",
@Query("userId") userId: String,
@Query("orderNo") orderId: String,
@Query("mode") mode: Int
): ApiResponse<Any?>
/**
* 提交采集图片数据
*/
@Multipart
@POST
suspend fun postImageData(
@Url url: String = "http://192.168.1.201:14801/terminal/neglect/dishCollectionVectorData/add",
@PartMap params: Map<String, @JvmSuppressWildcards RequestBody>,
@Part image: MultipartBody.Part?
): ApiResponse<String>
suspend fun uploadCollectFoodPics(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/dishCollectionVectorData/add",
@PartMap params: HashMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 档口机向量数据分页查询
*/
@POST
suspend fun getCollectedFoodVector(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/collectionVectorPage",
@Body param: MutableMap<String, String>
): ApiResponse<List<FoodVector>?>
/**
* 档口机向量数据分页查询
*/
@DELETE
suspend fun deleteCollectFood(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/deleteBoothMachineCollection",
@QueryMap param: MutableMap<String, String?>
): ApiResponse<Any?>
}
@@ -1,6 +1,7 @@
package com.sw.dualscreen.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.utils.SPUtil
import com.sw.plate.App
@@ -18,7 +19,11 @@ class RequestInterceptor : Interceptor {
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
.header("X-Access-Token", getToken(originalRequest))
// .header("X-Access-Token", getToken(originalRequest))
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
// .header("X-DEVICE-CODE", "bcf396ed-78f6-3864-9837-7c37c5b2ec41")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
@@ -2,6 +2,7 @@ package com.sw.dualscreen.objbox
import android.graphics.Bitmap
import android.net.Uri
import java.io.File
data class FoodClassInfo(
var class_names: List<String>,
@@ -17,13 +18,49 @@ data class FoodClassInfo(
//)
data class FoodCollectionBean(
var imageFile: File? = null,
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false
)
var isFinish:Boolean = false,
var uploadSuccess:Boolean = false
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
data class CollectedFoodBean(
var foodName: String?,
other as FoodCollectionBean
if (isShowCamera != other.isShowCamera) return false
if (isFinish != other.isFinish) return false
if (uploadSuccess != other.uploadSuccess) return false
if (imageFile != other.imageFile) return false
if (imageUri != other.imageUri) return false
if (bitmap != other.bitmap) return false
if (!imageVector.contentEquals(other.imageVector)) return false
return true
}
override fun hashCode(): Int {
var result = isShowCamera.hashCode()
result = 31 * result + isFinish.hashCode()
result = 31 * result + uploadSuccess.hashCode()
result = 31 * result + (imageFile?.hashCode() ?: 0)
result = 31 * result + (imageUri?.hashCode() ?: 0)
result = 31 * result + (bitmap?.hashCode() ?: 0)
result = 31 * result + (imageVector?.contentHashCode() ?: 0)
return result
}
}
data class CollectedFoodInfo(
// var id: String? = null,
// var placeId: String? = null,
var foodId: String? = null,
var foodName: String? = null,
// var foodPic: Any? = null,
// var foodVector: Any? = null,
var foodCount: Int = 0
)
@@ -1,15 +1,22 @@
package com.sw.dualscreen.objbox
import com.sw.dualscreen.utils.DateTimeUtil
import io.objectbox.annotation.Entity
import io.objectbox.annotation.HnswIndex
import io.objectbox.annotation.Id
import io.objectbox.annotation.VectorDistanceType
import java.time.LocalDateTime
@Entity
data class Food(
@Id var id: Long = 0,
var name: String? = null,
var foodIdx: Int = 0,
var collectId: String? = null,
var foodId: String? = null,
var foodName: String? = null,
var version: String? = null,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()),
var isDel: Boolean = false,
var otherField: String? = null,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null
) {
@@ -20,8 +27,13 @@ data class Food(
other as Food
if (id != other.id) return false
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (isDel != other.isDel) return false
if (collectId != other.collectId) return false
if (foodId != other.foodId) return false
if (foodName != other.foodName) return false
if (version != other.version) return false
if (createTime != other.createTime) return false
if (otherField != other.otherField) return false
if (!foodVector.contentEquals(other.foodVector)) return false
return true
@@ -29,9 +41,15 @@ data class Food(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + isDel.hashCode()
result = 31 * result + (collectId?.hashCode() ?: 0)
result = 31 * result + (foodId?.hashCode() ?: 0)
result = 31 * result + (foodName?.hashCode() ?: 0)
result = 31 * result + (version?.hashCode() ?: 0)
result = 31 * result + createTime.hashCode()
result = 31 * result + (otherField?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
return result
}
}
@@ -1,18 +1,18 @@
package com.sw.dualscreen.objbox
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.SparseLongArray
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import androidx.core.graphics.scale
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.utils.AssetsTool
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
@@ -23,86 +23,143 @@ import java.io.IOException
import java.io.InputStream
object FoodModule {
private const val TAG = "FoodModule"
private const val THRESHOLD = 0.8
private lateinit var module_mobile: Module
private lateinit var box: Box<Food>
private lateinit var embeddingsList: List<List<Float>>
private lateinit var labelsList: IntArray
private lateinit var classInfo: FoodClassInfo
// private const val THRESHOLD = 0.0
private var module: Module? = null
// private lateinit var embeddingsList: List<List<Float>>
// private lateinit var labelsList: IntArray
// private lateinit var classInfo: FoodClassInfo
val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
val DEFAULT_FOOD_INDEX = -1
fun init(context: Context) {
Thread {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
//if (box.all.isNotEmpty()) {
// box.removeAll()
//}
// val DEFAULT_FOOD_INDEX = -1
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
private const val MODEL_INPUT_WIDTH = 224
private const val MODEL_INPUT_HEIGHT = 224
@SuppressLint("SuspiciousIndentation")
suspend fun init(context: Context, block: () -> Unit = {}) {
// Thread {}.start()
withContext(Dispatchers.IO) {
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
module = Module.load(modelPath)
//初始化默认重新拉取数据,先清空本地数据
val list = ObjectBox.getAll()
if (list.isNotEmpty()) {
ObjectBox.removeAll()
}
//if (box.all.isEmpty()) {
// initDefFoodData(context)
//}
}.start()
block()
}
}
fun uri2FloatArray(uri: Uri): FloatArray? {
return MyApp.instance?.let { context ->
ImageUtil.uriToBitmap(context, uri)?.let {
bitmap2FloatArray(it)
// fun uri2FloatArray(uri: Uri): FloatArray? {
// return MyApp.instance?.let { context ->
// ImageUtil.uriToBitmap(context, uri)?.let {
// bitmap2FloatArray(it)
// }
// }
// }
fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
var rgb565Bitmap: Bitmap? = null
try {
val scaledBitmap = originBitmap.scale(
MODEL_INPUT_WIDTH,
MODEL_INPUT_HEIGHT
)
if (isRecycle) {
originBitmap.recycle()
}
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
scaledBitmap.recycle()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
rgb565Bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
if (module == null) {
return null
}
val outputTensor = module?.forward(IValue.from(inputTensor))?.toTensor()
return outputTensor?.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
fun bitmap2FloatArray(bitmap: Bitmap): FloatArray {
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
}
fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
return uri2FloatArray(uri)?.let {
queryFood(it, queryCount)
}
}
// fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
// return uri2FloatArray(uri)?.let {
// queryFood(it, queryCount)
// }
// }
/**
* 返回识别物品名称列表
*/
fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
val floatArray = bitmap2FloatArray(bitmap)
return queryFood(floatArray, queryCount)
}
// fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFood(floatArray, queryCount)
// }
/**
* 返回识别物品IdNameScore对象列表
*/
fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap)
return queryFoodNameScore(floatArray, queryCount)
}
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFoodNameScore(floatArray, queryCount)
// }
fun queryFoodNameScore(floatArray: FloatArray, queryCount: Int = 15): List<IdNameScore> {
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
suspend fun queryFoodNameScore(
floatArray: FloatArray?,
queryCount: Int = 15
): List<IdNameScore> {
if (floatArray == null) return emptyList()
val startTime = System.currentTimeMillis()
val size = ObjectBox.getAll().filter { it.isDel.not() }.size
Timber.tag(TAG).d("queryFoodNameScore-已采集向量总数:${size}")
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
val idScoreList = query.findIdsWithScores()
// val idScoreList = query.findIdsWithScores()
val objScoreList = ObjectBox.query(floatArray, queryCount)
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}")
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
objScoreList.forEach {
val food = it.get()
nameScoreList.add(
IdNameScore(
id = food.id,
name = food.foodName ?: "",
score = it.score
)
)
}
Timber.tag("FoodModule").d("registerDataChange,queryFood数据:${GsonUtils.toJson(nameScoreList)}")
val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap)
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap, false)
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
}
val maxScoreList = nameScoreList
.filter { it.score < 1 - THRESHOLD }
.groupBy { it.name }
@@ -122,69 +179,84 @@ object FoodModule {
val sortedScoreList = maxScoreList.sortedWith(compareBy {
orderList.indexOf(it.name)
})
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
val map = mutableMapOf<String, Int>()
query.findIdsWithScores().forEach {
val food = box.get(it.id)
Timber.d("${food.name}|${food.foodIdx}|${it.score}")
if (1 - it.score >= THRESHOLD) {
//FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
food.name?.let { key ->
val count = map[key] ?: 0
map.put(key, count + 1)
}
}
}
val list = map.entries.sortedByDescending { it.value }.map { it.key }
return list
// fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
// val query = box.query()
// .equal(Food_.isDel, false)
// .and()
// .nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// .build()
//// val query: Query<Food> =
//// box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
// //查询比较分数
//// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// val map = mutableMapOf<String, Int>()
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// nameScoreList.filter { it.score < 0.05 }.forEach {
// val count = map[it.name] ?: 0
// map[it.name] = count + 1
// query.findIdsWithScores().forEach {
// val food = box.get(it.id)
// Timber.d("${food.foodName}|${food.foodId}|${food.collectId}|${food.version}|${food.otherField}|${it.score}")
// if (1 - it.score >= THRESHOLD) {
// //FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
// food.foodName?.let { key ->
// val count = map[key] ?: 0
// map.put(key, count + 1)
// }
// }
// }
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
// return list
}
//
//// val map = mutableMapOf<String, Int>()
//// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
//// nameScoreList.filter { it.score < 0.05 }.forEach {
//// val count = map[it.name] ?: 0
//// map[it.name] = count + 1
//// }
//// val list = map.entries.sortedByDescending { it.value }.map { it.key }
//// return list
// }
data class IdNameScore(
val id:Long,
var name:String,
val id: Long,
var name: String,
val score: Double
)
fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
embeddingsList =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
classInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
}
action()
}
// fun initDefFoodData(context: Context, action: () -> Unit = {}) {
// //val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
// //if (count > 0) {
// // return
// //}
// val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
// val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
// val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
//
// embeddingsList =
// Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
// labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
// classInfo =
// Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
//
// val foodMap = classInfo.idx_to_class
// embeddingsList.forEachIndexed { index, floatList ->
// val classIdx = labelsList[index]
// val foodName = foodMap["$classIdx"]
// val array = floatList.toFloatArray()
// box.put(
// Food(
// foodName = foodName,
// foodVector = array,
// //foodIdx = DEFAULT_FOOD_INDEX
// )
// )
// }
// action()
// }
/**
@@ -195,47 +267,75 @@ object FoodModule {
*
* 不可能反正SD
*/
fun copyAssetToCache(context: Context, fileName: String): String? {
// 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
val cacheDir = context.getCacheDir()
if (!cacheDir.exists()) {
cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
}
val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
if (outPath.exists()) {
outPath.delete() // TODO 如果该文件已经存在,就删掉
}
var `is`: InputStream? = null // 读取
var fos: FileOutputStream? = null // 写入
try {
// 创建文件,如果创建成功,就返回true
val res = outPath.createNewFile()
if (res) {
`is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
val buf = ByteArray(`is`.available()) // 缓存区
var byteCount: Int
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.cacheDir
// if (!cacheDir.exists()) {
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
// }
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
// if (outPath.exists()) {
// outPath.delete() // TODO 如果该文件已经存在,就删掉
// }
// var `is`: InputStream? = null // 读取
// var fos: FileOutputStream? = null // 写入
// try {
// // 创建文件,如果创建成功,就返回true
// val res = outPath.createNewFile()
// if (res) {
// `is` = context.assets.open(fileName) // 拿到main/assets目录的输入流,用于读取字节
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
// val buf = ByteArray(`is`.available()) // 缓存区
// var byteCount: Int
//
// // 开始循环读取
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
// fos.write(buf, 0, byteCount)
// }
// return outPath.absolutePath
// }
// } catch (e: IOException) {
// e.printStackTrace()
// } finally {
// try {
// // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos!!.flush()
// `is`!!.close()
// fos.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
// return null
// }
// 开始循环读取
while ((`is`.read(buf).also { byteCount = it }) != -1) {
fos.write(buf, 0, byteCount)
}
return outPath.getAbsolutePath()
fun copyAssetToCache(context: Context, fileName: String): String? {
val cacheFile = File(context.cacheDir, fileName)
val buffer = ByteArray(8 * 1024)
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
inputStream = context.assets.open(fileName)
outputStream = FileOutputStream(cacheFile)
var byteCount: Int
while (inputStream.read(buffer).also { byteCount = it } != -1) {
outputStream.write(buffer, 0, byteCount)
}
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
return cacheFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
// TODO 一定要记得关闭资源,为了不去性能的磨损
fos!!.flush()
`is`!!.close()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
Timber.tag("FoodModule").d("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
}
@@ -17,18 +17,33 @@
package com.sw.dualscreen.objbox
import android.content.Context
import android.os.Environment
import android.util.Log
import com.sw.plate.App
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
import io.objectbox.android.Admin
import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.config.DebugFlags
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import io.objectbox.sync.Sync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date
import java.util.zip.GZIPOutputStream
import kotlin.also
import kotlin.and
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
@@ -49,8 +64,7 @@ object ObjectBox {
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
private set
var boxStore: BoxStore? = null
/**
* If building the [boxStore] failed, contains the thrown error message.
@@ -62,10 +76,11 @@ object ObjectBox {
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
.androidContext(context.applicationContext)
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build()
} catch (e: DbException) {
if (e.javaClass.equals(DbException::class.java) || e is FileCorruptException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
// checked in NoteListActivity to notify user.
dbExceptionMessage = e.toString()
@@ -77,11 +92,12 @@ object ObjectBox {
}
// if (BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG)
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
// }
}
@@ -94,7 +110,7 @@ object ObjectBox {
// Do not copy if database file is still in use.
// If it would be open, the copy will likely get corrupted
// as BoxStore may currently write data to the file.
Log.e(TAG, "Database file is still in use, can not copy.")
Timber.tag(TAG).e("Database file is still in use, can not copy.")
return false
}
@@ -109,4 +125,166 @@ object ObjectBox {
return true
}
// 获取指定实体的Box,自动关联BoxStore
inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
// 协程安全执行数据库操作(推荐所有操作使用此方法)
suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
return withContext(Dispatchers.IO) {
try {
dbMutex.withLock { operation() }
} catch (e: FileCorruptException) {
// 操作中触发损坏,尝试重建数据库
//val context = boxStore.context
//deleteDbFiles(context)
init(App.getContext()!!)
null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
val query: Query<Food>? = getBox<Food>()?.query()
?.equal(Food_.isDel, false)
?.and()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
?.build()
try {
query?.findWithScores()
} finally {
// 先关闭Query,释放Cursor
query?.close()
}
} ?: emptyList()
suspend fun get(id: Long) = safeDbOp {
getBox<Food>()?.get(id)
}
suspend fun put(entity: Food) = safeDbOp {
getBox<Food>()?.put(entity)
}
suspend fun putAll(entities: List<Food>) = safeDbOp {
getBox<Food>()?.put(entities)
}
suspend fun getAll() = safeDbOp {
getBox<Food>()?.all
} ?: emptyList()
suspend fun filter(name: String?, isDistinct: Boolean = false) = safeDbOp {
val list = if (isDistinct) {
getBox<Food>()?.all?.distinctBy { it.foodName }
} else {
getBox<Food>()?.all
}
if (name.isNullOrBlank().not()) {
list?.filter { it.foodName?.contains(name) == true }
}
list
} ?: emptyList()
suspend fun removeAll() = safeDbOp {
getBox<Food>()?.removeAll()
}
suspend fun remove(name: String) = safeDbOp {
getBox<Food>()?.run {
val filterIdList = all.filter { it.foodName == name }.map { it.id }
removeByIds(filterIdList)
}
}
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
// 检查存储是否可读写(操作数据库前调用)
fun isStorageAvailable(context: Context): Boolean {
return try {
val state = Environment.getExternalStorageState()
val innerDir = context.filesDir
Environment.MEDIA_MOUNTED == state && innerDir.canRead() && innerDir.canWrite()
} catch (e: Exception) {
false
}
}
// 备份数据库到应用私有目录(无权限要求,推荐)
suspend fun backupDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
return@withContext copyDir(dbDir, backupDir)
}
// 从备份恢复数据库(恢复后会重建BoxStore)
suspend fun restoreDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
if (!backupDir.exists()) return@withContext false
// 先关闭旧的BoxStore,删除损坏文件,再恢复备份
boxStore?.close()
deleteDbFiles(context)
val isSuccess = copyDir(backupDir, dbDir)
// 重新初始化
init(context)
return@withContext isSuccess
}
// 递归删除数据库文件
private fun deleteDbFiles(context: Context) {
val dbDir = File(context.filesDir, DB_DIR_NAME)
if (dbDir.exists()) deleteDirRecursively(dbDir)
}
// 递归删除目录
private fun deleteDirRecursively(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach { deleteDirRecursively(it) }
}
file.delete()
}
// 递归复制目录(核心备份/恢复逻辑)
private fun copyDir(srcDir: File, destDir: File): Boolean {
return try {
if (!srcDir.exists()) return false
if (!destDir.exists()) destDir.mkdirs()
srcDir.listFiles()?.forEach { srcFile ->
val destFile = File(destDir, srcFile.name)
if (srcFile.isDirectory) {
copyDir(srcFile, destFile)
} else {
copyFile(srcFile, destFile)
}
}
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
// 复制单个文件(使用NIO,高效稳定)
private fun copyFile(srcFile: File, destFile: File) {
FileInputStream(srcFile).channel.use { srcChannel ->
FileOutputStream(destFile).channel.use { destChannel ->
destChannel.transferFrom(srcChannel, 0, srcChannel.size())
}
}
}
// 关闭BoxStore(应用退出时调用,可选)
fun close() {
boxStore?.close()
boxStore = null
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,8 @@ package com.sw.dualscreen.presentation
import android.text.TextUtils
import com.sw.dualscreen.ext.toSafeDouble
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.UserEnergy
import com.sw.dualscreen.model.response.UserNutrition
import timber.log.Timber
import kotlin.math.max
@@ -16,64 +17,112 @@ object UserNutritionUtils {
/**
* 计算热量信息
*/
fun calculateNutrition(
fun calculateNutrition2(
foodInfo: FoodInfo,
userModel: UserNutritionData,
nutrition: UserNutrition,
weight: Double,
dinnerType: String
): CalcResultInfo {
): UserEnergy {
// 初始化变量
var foodKcal = 0.0
var totalKcal = 0.0
var (vegetable, meat, fruits, grain) = List(4) { 0.0 }
//var totalKcal = 0.0
var (calorie, grain, fruitsVegetables, meatEggs) = List(4) { 0.0 }
// 处理食物信息
foodKcal = calculateValue(foodInfo.stFoodInfoMaterial?.energyKcal, weight)
Timber.d("calculateNutrition foodKcal = ${foodKcal}, energyKcal = ${foodInfo.stFoodInfoMaterial?.energyKcal}, weight = $weight")
grain = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.grainValue(), weight)
fruits = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.fruitsValue(), weight)
vegetable = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.vegetableValue(), weight)
meat = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.meatValue(), weight)
if (weight > 0.0) {
calorie = calculateValue2(foodInfo.calorie,weight)
grain = calculateValue2(foodInfo.stapleFood, weight)
fruitsVegetables = calculateValue2(foodInfo.fruitsVegetables, weight)
meatEggs = calculateValue2(foodInfo.meatEggs, weight)
}
// 合并用户数据
grain += userModel.stFoodInfoPagoda?.grainValue() ?: 0.0
//val maxKcal = (nutrition.calorie?:0.0) * calculateDinnerTypeRatio(dinnerType) / 10.0
calorie += nutrition.calorie ?: 0.0
grain += nutrition.stapleFood ?: 0.0
fruitsVegetables += nutrition.fruitsVegetables ?: 0.0
meatEggs += nutrition.meatEggs ?: 0.0
fruits += userModel.stFoodInfoPagoda?.fruitsValue() ?: 0.0
vegetable = vegetable.plus(userModel.stFoodInfoPagoda?.vegetableValue() ?: 0.0)
meat += userModel.stFoodInfoPagoda?.meatValue() ?: 0.0
totalKcal = foodKcal + userModel.energyValue()
Timber.d("calculateNutrition totalKcal = ${totalKcal}, energyValue = ${userModel.energyValue()}")
if (totalKcal < 0){
totalKcal = 0.0
}
//totalKcal = max(calorie, 0.0)
// 判断当餐最大热量
val maxKcal =
userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
val pagoda = userModel.stFoodInfoPagoda
val fruitsInfo = parseRecommend(pagoda?.fruitsRecommend, fruits)
val vegetableInfo = parseRecommend(pagoda?.vegetableRecommend, vegetable)
val calcResult = CalcResultInfo(
totalKcal = CalcInfo(
max = maxKcal,
min = 0.0,
current = totalKcal
),
grain = parseRecommend(pagoda?.grainRecommend, grain),
fruits = CalcInfo( // 果蔬 = 水果+蔬菜
max = fruitsInfo.max + vegetableInfo.max,
min = fruitsInfo.min + vegetableInfo.min,
current = fruitsInfo.current + vegetableInfo.current
),
meat = parseRecommend(pagoda?.meatRecommend, meat)
//val maxKcal = userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
//Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
// val calcResult = CalcResultInfo(
// totalKcal = CalcInfo(
// max = maxKcal,
// min = 0.0,
// current = totalKcal
// ),
// grain = parseRecommend(foodInfo.stapleFoodRecommend, grain),
// fruits = parseRecommend(foodInfo.fruitsVegetablesRecommend, fruitsVegetables),
// meat = parseRecommend(foodInfo.meatEggsRecommend, meatEggs)
// )
return UserEnergy(
calorie = calorie,
grain = grain,
fruitsVegetables = fruitsVegetables,
meatEggs = meatEggs
)
return calcResult
}
// fun calculateNutrition(
// foodInfo: FoodInfo,
// userModel: UserNutritionData,
//// userModel: UserNutrition,
// weight: Double,
// dinnerType: String
// ): CalcResultInfo {
//
// // 初始化变量
// var foodKcal = 0.0
// var totalKcal = 0.0
// var (vegetable, meat, fruits, grain) = List(4) { 0.0 }
//
// // 处理食物信息
// foodKcal = calculateValue(foodInfo.stFoodInfoMaterial?.energyKcal, weight)
// Timber.d("calculateNutrition foodKcal = ${foodKcal}, energyKcal = ${foodInfo.stFoodInfoMaterial?.energyKcal}, weight = $weight")
// grain = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.grainValue(), weight)
// fruits = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.fruitsValue(), weight)
// vegetable = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.vegetableValue(), weight)
// meat = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.meatValue(), weight)
//
// // 合并用户数据
// grain += userModel.stFoodInfoPagoda?.grainValue() ?: 0.0
//
// fruits += userModel.stFoodInfoPagoda?.fruitsValue() ?: 0.0
// vegetable = vegetable.plus(userModel.stFoodInfoPagoda?.vegetableValue() ?: 0.0)
// meat += userModel.stFoodInfoPagoda?.meatValue() ?: 0.0
//
// totalKcal = foodKcal + userModel.energyValue()
// Timber.d("calculateNutrition totalKcal = ${totalKcal}, energyValue = ${userModel.energyValue()}")
// if (totalKcal < 0) {
// totalKcal = 0.0
// }
// // 判断当餐最大热量
// val maxKcal =
// userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
// Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
// val pagoda = userModel.stFoodInfoPagoda
// val fruitsInfo = parseRecommend(pagoda?.fruitsRecommend, fruits)
// val vegetableInfo = parseRecommend(pagoda?.vegetableRecommend, vegetable)
// val calcResult = CalcResultInfo(
// totalKcal = CalcInfo(
// max = maxKcal,
// min = 0.0,
// current = totalKcal
// ),
// grain = parseRecommend(pagoda?.grainRecommend, grain),
// fruits = CalcInfo( // 果蔬 = 水果+蔬菜
// max = fruitsInfo.max + vegetableInfo.max,
// min = fruitsInfo.min + vegetableInfo.min,
// current = fruitsInfo.current + vegetableInfo.current
// ),
// meat = parseRecommend(pagoda?.meatRecommend, meat)
// )
// return calcResult
// }
fun parseRecommend(recommend: String?, current: Double): CalcInfo {
var newCurrent = if (current < 0) 0.0 else current
val newCurrent = if (current < 0) 0.0 else current
if (TextUtils.isEmpty(recommend) || !recommend!!.contains("-")) {
Timber.e("parseRecommend recommend 格式错误")
return CalcInfo(
@@ -93,7 +142,10 @@ object UserNutritionUtils {
else -> 0
}
}
private fun calculateValue2(nutrient: Double?, weight: Double): Double {
if ((nutrient?:0.0) == 0.0) return 0.0
return nutrient!! * weight / 100.0
}
/**
* 计算每百克含量
*/
@@ -106,6 +158,36 @@ object UserNutritionUtils {
return max(max1, calcResultInfo.meat.max).toInt()
}
fun getCalorieArray(left:String?, mid:String?, right: String?):DoubleArray {
val leftArray = (left?.ifBlank { "0-0" } ?: "0-0").split("-").map { it.toSafeDouble() }
val midArray = (mid?.ifBlank { "0-0" } ?: "0-0").split("-").map { it.toSafeDouble() }
val rightArray = (right?.ifBlank { "0-0" } ?: "0-0").split("-").map { it.toSafeDouble() }
return doubleArrayOf(
0.0, leftArray[0], midArray[0], rightArray[0], rightArray[1]
)
}
fun findCalorieIndex(num: Double, array: DoubleArray): Int {
if (array.isEmpty()) return 0
if (num < array[0]) return 0
if (num >= array[array.lastIndex]) return array.lastIndex
var left = 0
var right = array.lastIndex
while (left < right) {
val mid = left + (right - left) / 2
if (array[mid] <= num && num < array[mid + 1]) {
return mid
} else if (num < array[mid]) {
right = mid
} else {
left = mid + 1
}
}
return 0
}
data class CalcInfo(
var max: Double,
var min: Double,
@@ -0,0 +1,49 @@
package com.sw.dualscreen.presentation.pay
import android.R
import android.app.Presentation
import android.os.Bundle
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import android.view.Display
import androidx.core.text.buildSpannedString
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.databinding.PresentationCashPayBinding
import com.sw.dualscreen.ext.load
class CashPayPresentation(
val activity: PayActivity,
display: Display,
private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display) {
private lateinit var binding: PresentationCashPayBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = PresentationCashPayBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setBackgroundDrawableResource(R.color.transparent)
initView()
}
var foodName: String? = null
var payAmount: String? = null
private fun initView() {
binding.tvFoodName.text = foodName
binding.tvRealAmount.text = getAmountText(payAmount ?: "")
}
private fun getAmountText(amount: String): SpannedString {
return buildSpannedString {
append("¥", AbsoluteSizeSpan(32, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
append(amount, AbsoluteSizeSpan(48, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun onDisplayRemoved() {
super.onDisplayRemoved()
onDismissListener()
}
}
@@ -0,0 +1,585 @@
package com.sw.dualscreen.presentation.pay
import android.Manifest
import android.app.Dialog
import android.app.Presentation
import android.content.pm.PackageManager
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.Display
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.databinding.PresentationFacePayBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpannedUtils
import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
class FacePayPresentation(
val activity: PayActivity,
display: Display,
val userViewModel: UserViewModel,
val recognizeViewModel: RecognizeViewModel,
// val //parentTextureView: TextureView,
private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
companion object {
private const val TAG = "FacePayPresentation"
}
private lateinit var binding: PresentationFacePayBinding
// 虹软人脸配置 ⬇
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.IR
private var openRectInfoDraw = false
private var irCameraHelper: DualCameraHelper? = null
private var irFaceRectTransformer: FaceRectTransformer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = PresentationFacePayBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setBackgroundDrawableResource(android.R.color.white)
initView()
setupArcCamera()
registerDataChange()
pauseCamera()
}
override fun show() {
super.show()
binding.root.postDelayed({
resumeCamera()
}, 1500)
}
var foodName: String? = null
var payAmount: String? = null
private fun initView() {
binding.tvFoodName.text = foodName
binding.tvRealAmount.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 32),
TextBean(text = payAmount ?: "", textSize = 48),
)
)
// binding.ivPreviewImage.let {
// it.outlineProvider = object : ViewOutlineProvider() {
// override fun getOutline(view: View, outline: Outline) {
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
// }
// }
// it.clipToOutline = true
// }
}
override fun onGlobalLayout() {
Timber.tag(TAG).d("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
//parentTextureView.getViewTreeObserver().removeOnGlobalLayoutListener(this)
openCamera()
}
private fun openCamera() {
Timber.tag(TAG).d("openCamera")
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED
) {
ToastUtils.showToast("无摄像头权限")
return
}
try {
val cameraCount = Camera.getNumberOfCameras()
if (cameraCount < 3) {
ToastUtils.showToast("摄像头数量异常")
return
}
recognizeViewModel.init(
PreviewConfig(
2, 1, 90, 90
)
)
initRgbCamera()
if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
initIrCamera()
}
} catch (e: Exception) {
Timber.tag(TAG).e(e, "打开摄像头失败")
}
}
/**
* 启用虹软人脸失败
*/
private fun setupArcCamera() {
initArcViewModel()
initArcView()
openRectInfoDraw = true
}
private fun registerDataChange() {
activity.lifecycleScope.launch {
userViewModel.loadFaceResult.collect { needUpdate ->
if (needUpdate) {
recognizeViewModel.refreshFaceList()
}
}
}
}
override fun onStop() {
rgbCameraHelper?.release()
rgbCameraHelper = null
irCameraHelper?.release()
irCameraHelper = null
recognizeViewModel.destroy()
super.onStop()
}
fun resumeCamera() {
Timber.tag(TAG).d("resumeCamera isRecognition = $isRecognition")
isRecognition = true
if (rgbCameraHelper?.isStopped == true) {
rgbCameraHelper?.start()
}
}
fun pauseCamera() {
Timber.tag(TAG).d("pauseCamera isRecognition = $isRecognition")
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
private fun initArcViewModel() {
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(activity, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
Timber.tag(TAG).e("ftInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.frInitCode.observe(activity, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.tag(TAG).e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(activity, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.tag(TAG).e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(activity, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.tag(TAG)
.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
Timber.tag(TAG).i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(
activity,
Observer { compareResult: CompareResult ->
Timber.tag(TAG)
.i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}")
// recognitionTime = System.currentTimeMillis()
// recognitionWeight = lastWeight
lastFaceTrackId = compareResult.trackId
val faceEntity = compareResult.faceEntity
val userId = faceEntity.userName
if (userId == null) return@Observer
// TODO: 测试支付用户id
// userId = "1987710988425662466"
faceRecSuccess(userId)
})
recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
Timber.tag(TAG).i("drawRectInfoText observe info = $info")
})
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
//parentTextureView.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
/**
* 调整View的宽高,使预览显示正常且采集框固定为
*
* @param rgbPreview RGB预览View
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @param scale 缩放比例
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val w = (600.dp * 1.5).toInt()
val h = 1068.dp
val layoutParams = FrameLayout.LayoutParams(w, h)
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.tag(TAG)
.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
activity.runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 0.6F
)
Timber.tag(TAG)
.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.tag(TAG)
.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
Timber.tag(TAG).d(
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror(
context
)
}, isDrawRgbRectVerticalMirror = ${
ConfigUtil.isDrawRgbRectVerticalMirror(
context
)
}"
)
// 调整识别窗口位置
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
// layoutParams.width,
// layoutParams.height,
layoutParams.width,
layoutParams.height,
90,
cameraId,
isMirror,
true,
true
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.tag(TAG).i("initRgbCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.tag(TAG).i("initRgbCamera onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.tag(TAG)
.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.tag(TAG)
.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth
val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight
Timber.tag(TAG)
.i("initRgbCamera measuredWidth=$measuredWidthmeasuredHeight=$measuredHeight")
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(Point(measuredWidth, measuredHeight))
.rotation(activity.windowManager.defaultDisplay.rotation)
.additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.setSurfaceFrameCallback { frame ->
activity.facePayFragment?.loadBitmap(frame)
}
rgbCameraHelper!!.init()
// rgbCameraHelper!!.start()
}
/**
* 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
*/
private fun initIrCamera() {
Timber.tag(TAG).d("initIrCamera: livenessType = $livenessType")
if (livenessType === LivenessType.RGB) {
return
}
val irCameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.tag(TAG)
.d("initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
val previewSizeIr = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr,
previewSizeIr, displayOrientation, 0.25f
)
irFaceRectTransformer = FaceRectTransformer(
previewSizeIr.width, previewSizeIr.height,
// layoutParams.width, layoutParams.height,
layoutParams.width, layoutParams.height,
displayOrientation, cameraId, isMirror,
ConfigUtil.isDrawIrRectHorizontalMirror(context),
ConfigUtil.isDrawIrRectVerticalMirror(context)
)
recognizeViewModel.onIrCameraOpened(camera)
recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer)
}
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
recognizeViewModel.refreshIrPreviewData(nv21)
}
override fun onCameraClosed() {
Timber.tag(TAG).i("initIrCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.tag(TAG).i("initIrCamera onCameraError: ${e.message}")
e.printStackTrace()
}
override fun onCameraConfigurationChanged(
cameraID: Int,
displayOrientation: Int
) {
if (irFaceRectTransformer != null) {
irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.tag(TAG)
.i("initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation")
}
}
val previewConfig = recognizeViewModel.previewConfig
irCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewIr.measuredWidth,
binding.dualCameraTexturePreviewIr.measuredHeight
)
)
.rotation(activity.windowManager.defaultDisplay.rotation)
.specificCameraId(previewConfig.irCameraId)
.previewOn(binding.dualCameraTexturePreviewIr)
.cameraListener(irCameraListener)
.isMirror(true)
.previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小
.additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度
.build()
irCameraHelper!!.init()
try {
irCameraHelper!!.start()
} catch (e: RuntimeException) {
ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice))
}
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
// Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
if (facePreviewInfoList.isEmpty() || (lastFaceTrackId != facePreviewInfoList[0]!!.trackId)) {
if (lastFaceTrackId != -1) {
// mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// Timber.tag(TAG).i("$lastFaceTrackId 用户离开")
// lastFaceTrackId = -1
// postUserData()
// if (mealPickupMode == 0) {
// step1FoodRecognizing()
// } else {
// step2FaceRecognizing(currentFood!!)
// }
resumeCamera()
}
}
}
private var lastFaceTrackId: Int = -1 // 上一次的人脸信息
override fun onDisplayRemoved() {
super.onDisplayRemoved()
onDismissListener()
}
private fun faceRecSuccess(userId: String) {
activity.runOnUiThread {
activity.showWaitingDialog("加载中,请稍后……")
}
activity.getMemberInfoById(userId) { memberInfo ->
if (memberInfo == null) {
activity.hideWaitingDialog()
//ToastUtils.showToast("查询会员信息失败,请稍后重试")
return@getMemberInfoById
}
activity.bindOrder(memberInfo.faceUserId?:"") { bindResult ->
if (bindResult.not()) {
activity.hideWaitingDialog()
//ToastUtils.showToast("订单绑定失败")
return@bindOrder
}
binding.root.postDelayed({
activity.hideWaitingDialog()
activity.showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
activity.hidePayTab()
binding.root.postDelayed({
dismiss()
}, 500)
}, 1000)
}
}
}
// private fun bindOrder(userId: String, block: () -> Unit) {
// val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
// val mode = if (pickupMode == 1) 1 else 2
// userViewModel.bindOrder(userId, activity.foodOrderId, mode = mode) { bindResult ->
// activity.runOnUiThread {
// if (bindResult.not()) {
// activity.hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@runOnUiThread
// }
// block()
// }
// }
// }
// private fun getMemberInfo(userId: String, block:(MemberInfo)-> Unit) {
// userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
// activity.runOnUiThread {
// if (memberInfo == null) {
// activity.hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@runOnUiThread
// }
// block(memberInfo)
// }
// }
// }
}
@@ -0,0 +1,202 @@
package com.sw.dualscreen.presentation.pay
import android.annotation.SuppressLint
import android.app.Presentation
import android.os.Bundle
import android.view.Display
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.databinding.PresentationScanQrcodePayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.SpannedUtils
class ScanQrCodePayPresentation(
val activity: PayActivity,
display: Display,
val type: Int,
private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display) {
//type = 0,扫码支付默认显示二维码
//type = 1,会员结算显示二维码,是会员则显示名字、头像、手机号、可用余额
//type = 2,支付成功,是会员则显示名字、头像、手机号、可用余额
private lateinit var binding: PresentationScanQrcodePayBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = PresentationScanQrcodePayBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setBackgroundDrawableResource(android.R.color.transparent)
initView()
}
var foodName: String? = null
var payQrCodePic: Any? = null
//总价格
var totalPrice = 0.0
//余额
private var balance = 0.0
//实际支付金额
private var realPayPrice = 0.0
//扣除余额
private var expensesBalance = 0.0
@SuppressLint("SetTextI18n")
private fun initView() {
when (type) {
0 -> {
binding.layoutVip.gone()
binding.layoutPaySuccess.gone()
binding.layoutScanQrCode.visible()
binding.tvPayTip.text = "请扫码支付或出示付款码"
binding.tvFoodName.text = foodName
binding.tvRealAmount.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 32),
TextBean(text = totalPrice.format2String(2), textSize = 48),
)
)
binding.ivPayQrCode.load(payQrCodePic)
}
1 -> {
val memberInfo = activity.memberInfo
binding.layoutVip.run {
if (memberInfo != null) visible() else gone()
}
binding.layoutPaySuccess.gone()
binding.layoutScanQrCode.visible()
balance = (memberInfo?.topUpBalance ?: 0.0) + (memberInfo?.rewardBalance ?: 0.0)
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
expensesBalance = if (balance >= totalPrice) totalPrice else balance
memberInfo?.let { loadUserInfo(it) }
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 20),
TextBean(text = balance.format2String(2), textSize = 32),
)
)
binding.tvFoodName.text = foodName
binding.tvRealAmount.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 32),
TextBean(text = totalPrice.format2String(2), textSize = 48),
)
)
binding.ivPayQrCode.run {
// load(payQrCodePic)
if (balance >= totalPrice) invisible() else visible()
}
binding.tvPayTip.text = SpannedUtils.getAmountText(getAmountList())
}
2 -> {
val memberInfo = activity.memberInfo
binding.layoutVip.run {
if (memberInfo != null) visible() else gone()
}
binding.layoutPaySuccess.visible()
binding.layoutScanQrCode.gone()
balance = if (memberInfo != null) (memberInfo.topUpBalance
?: 0.0) + (memberInfo.rewardBalance ?: 0.0) else 0.0
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
expensesBalance = if (balance >= totalPrice) totalPrice else balance
memberInfo?.let { loadUserInfo(it) }
val remainBalance = if (balance >= totalPrice) balance - totalPrice else 0.0
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 24),
TextBean(text = remainBalance.format2String(2), textSize = 36),
)
)
val showBalance = expensesBalance.format2String(2)
val showTotal = totalPrice.format2String(2)
binding.tvPayInfo.text = "余额扣除 $showBalance 元,在线支付 $showTotal"
}
}
}
fun loadQrCodeImage(qrCodeUrl: String?) {
binding.ivPayQrCode.load(qrCodeUrl)
}
private fun loadUserInfo(item: MemberInfo) {
binding.ivHeadPic.load(
if(item.faceUrl.isNullOrBlank()) R.drawable.ic_avatar_default
else item.faceUrl
)
binding.tvUserName.text = item.name
val phone = item.phone ?: ""
binding.tvUserPhone.text =
if (phone.length == 11) phone.replace(phone.substring(3, 7), "****")
else phone
}
private fun getAmountList(): List<TextBean> {
val list: MutableList<TextBean> = mutableListOf()
list.add(TextBean(text = "余额扣除 ", textSize = 30, textColor = "#FF5E7585"))
list.add(
TextBean(
text = "-${expensesBalance.format2String(2)}",
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
if (balance >= totalPrice) {
//余额大于等于总价格,使用余额支付
list.add(TextBean(text = " 元,扣除后可用余额 ", textSize = 30, textColor = "#FF5E7585"))
val remainingBalance = balance - totalPrice
list.add(
TextBean(
text = remainingBalance.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
} else {
//余额小于总价格,使用余额+扫码支付
//实际支付金额
list.add(TextBean(text = " 元,还需支付 ", textSize = 30, textColor = "#FF5E7585"))
list.add(
TextBean(
text = realPayPrice.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
}
list.add(TextBean(text = "", textSize = 30, textColor = "#FF5E7585"))
return list
}
override fun onDisplayRemoved() {
super.onDisplayRemoved()
onDismissListener()
}
}
@@ -0,0 +1,12 @@
package com.sw.dualscreen.presentation.pay
import android.view.TextureView
import androidx.camera.lifecycle.ProcessCameraProvider
import java.util.concurrent.ExecutorService
class TransmitScreen {
}
@@ -2,6 +2,8 @@ package com.sw.dualscreen.repository
import com.google.gson.JsonParseException
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.RespCodeMsg
import com.sw.dualscreen.utils.GsonUtils
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
@@ -18,31 +20,37 @@ abstract class BaseRepository {
when (e) {
is HttpException -> {
ApiResponse(code = e.code(), message = e.message())
val respData = e.response()?.errorBody()?.string()
val respCodeMsg = GsonUtils.fromJson(respData, RespCodeMsg::class.java)
if (respCodeMsg?.msg.isNullOrBlank()) {
ApiResponse(code = "${e.code()}", msg = e.message())
} else {
ApiResponse(code = respCodeMsg.code ?:"-10", msg = respCodeMsg.msg)
}
}
is SocketTimeoutException -> {
ApiResponse(code = -2, message = "请求超时: ${e.message}")
ApiResponse(code = "-2", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = -3, message = "连接失败: ${e.message}")
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = -4, message = "SSL握手失败: ${e.message}")
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = -5, message = "JSON解析错误: ${e.message}")
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = -6, message = "网络IO错误: ${e.message}")
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = -1, message = "未知错误: ${e.message ?: "无错误信息"}")
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
@@ -1,19 +1,26 @@
package com.sw.dualscreen.repository
import android.content.Context
import android.net.Uri
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DinnerTypeInfo
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.objbox.CollectedFoodInfo
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import kotlin.Int
/**
* 远程数据处理
@@ -24,43 +31,98 @@ class RemoteRepository constructor(
/**
* 生成token
*/
suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
return safeApiCall {
apiService.getDeviceToken(
qrcodeId
)
}
}
// suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getDeviceToken(
// qrcodeId
// )
// }
// }
/**
* 获取设备信息
*/
suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
return safeApiCall {
apiService.getDeviceInfo(
equipmentCode,
token = token
)
}
}
// suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
// return safeApiCall {
// apiService.getDeviceInfo(
// equipmentCode,
// token = token
// )
// }
// }
/**
* 获取业务服务器token
*/
suspend fun getEquipmentToken(qrcodeId: String): ApiResponse<String> {
// suspend fun getEquipmentToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getEquipmentToken(qrcodeId = qrcodeId)
// }
// }
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getEquipmentToken(qrcodeId = qrcodeId)
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
/**
* 获取人脸数据
*/
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getUserFaceCache2(
pageNum: Int,
pageSize: Int = 160,
): ApiResponse<FaceData> {
return safeApiCall {
apiService.getUserFaceCache2(
pageNum = pageNum,
pageSize = pageSize
)
}
}
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageIndex: Int
): ApiResponse<UserFaceInfo> {
suspend fun getCollectedFoodList(
pageNum: Int,
pageSize: Int = 100,
foodName: String,
): ApiResponse<List<CollectedFoodInfo>> {
return safeApiCall {
apiService.getUserFaceCache(pageIndex = pageIndex)
apiService.getCollectedFoodList(
param = mapOf(
"pageNum" to "$pageNum",
"pageSize" to "$pageSize",
"foodName" to foodName,
)
)
}
}
@@ -70,33 +132,65 @@ class RemoteRepository constructor(
* @param type 0全部 1餐次
*/
suspend fun getRestInfoFoodsByType(
restId: String = GlobalData.restId,
type: Int = 0,
foodName: String,
pageNum: Int = 1,
pageSize:Int = 50
): ApiResponse<List<FoodInfo>> {
return safeApiCall {
apiService.getRestInfoFoodsByType(restId = restId, type = type, foodName = foodName)
apiService.getRestInfoFoodsByType(param = hashMapOf(
"name" to foodName,
"pageNum" to "$pageNum",
"pageSize" to "$pageSize",
//"订单来源权举:1-营养秤,2-档口
"deviceType" to "2"
))
}
}
// /**
// * 通过用户信息获取就餐数据
// */
// suspend fun getUserNutritionData(
// restId: String = GlobalData.restId,
// userId: String,
// foodId: String,
// ): ApiResponse<UserNutritionData> {
// return safeApiCall {
// apiService.getUserNutritionData(restId = restId, userId = userId, foodId = foodId)
// }
// }
/**
* 通过用户信息获取就餐数据
*/
suspend fun getUserNutritionData(
restId: String = GlobalData.restId,
userId: String,
foodId: String,
): ApiResponse<UserNutritionData> {
suspend fun getUserNutritionData(userId: String): ApiResponse<UserNutrition> {
return safeApiCall {
apiService.getUserNutritionData(restId = restId, userId = userId, foodId = foodId)
apiService.getUserNutritionData(userId = userId)
}
}
/**
* 获取支付二维码
*/
suspend fun getQrCodeImg(
orderNo: String,
memberId: String? = null,
totalFee: String? = null
): ApiResponse<String?> {
return safeApiCall {
apiService.getQrCodeImg(
orderNo = orderNo,
memberId = memberId,
totalFee = totalFee
)
}
}
suspend fun getDinnerType(
restId: String = GlobalData.restId,
): ApiResponse<DinnerTypeInfo> {
// restId: String = GlobalData.restId,
): ApiResponse<DinnerType> {
return safeApiCall {
apiService.getDinnerType(restId = restId)
apiService.getDinnerType()
}
}
@@ -114,33 +208,121 @@ class RemoteRepository constructor(
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
*/
suspend fun getFoodInfo(
restId: String = GlobalData.restId,
foodName: String,
): ApiResponse<List<FoodInfo>> {
return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) }
suspend fun getFoodInfo(foodName: String): ApiResponse<List<FoodInfo>> {
return safeApiCall {
// val map = mutableMapOf<String, List<String>>()
// map["nameList"] = foodName.split(",")
apiService.getFoodInfo(req = FoodSearchReq(nameList = foodName.split(",")))
}
}
/**
* 获取菜品信息
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
* 创建订单
*/
suspend fun postImageData(
context: Context,
restId: String = GlobalData.restId,
foodId: String,
foodName: String,
foodVector: String,
uri: Uri,
): ApiResponse<String> {
suspend fun createOrder(order: FoodOrder): ApiResponse<Any?> {
return safeApiCall {
apiService.createOrder(order = order)
}
}
val params = HashMap<String, RequestBody>()
params["placeId"] = restId.toRequestBody()
params["foodId"] = foodId.toRequestBody()
params["foodName"] = foodName.toRequestBody()
params["foodVector"] = foodVector.toRequestBody()
val imagePart = ImageUtil.uriToMultipart(context, uri, "foodPic")
/**
* 现金支付
*/
suspend fun cashPay(param: HashMap<String, String>): ApiResponse<Boolean?> {
return safeApiCall {
apiService.cashPay(param = param)
}
}
return safeApiCall { apiService.postImageData(params = params, image = imagePart) }
/**
* 查询订单状态
* @param orderNo 订单号
*/
suspend fun queryOrderState(orderNo: String): ApiResponse<Any?> {
return safeApiCall {
apiService.queryOrderState(orderNo = orderNo)
}
}
/**
* 会员支付
*/
suspend fun memberPay(param: HashMap<String, String>): ApiResponse<Any?> {
return safeApiCall {
apiService.memberPay(param = param)
}
}
/**
* 绑定订单
*/
suspend fun bindOrder(userId: String, orderId: String, mode: Int): ApiResponse<Any?> {
return safeApiCall {
apiService.bindOrder(userId = userId, orderId = orderId, mode = mode)
}
}
/**
* 根据id查询会员信息
*/
suspend fun getMemberInfoById(memberId: String): ApiResponse<MemberInfo?> {
return safeApiCall {
apiService.getMemberInfoById(memberId = memberId)
}
}
/**
* 根据id查询会员信息
*/
suspend fun getMemberInfoByPhone(phone: String, key: String): ApiResponse<MemberInfo?> {
return safeApiCall {
apiService.getMemberInfoByPhone(param = hashMapOf(
"phone" to phone,
"password" to key
))
}
}
/**
* 上传采集菜品信息
*/
suspend fun uploadCollectFoodPics(
fileList: List<File>,
params: HashMap<String, RequestBody>
): ApiResponse<List<String>?> {
// 准备文件参数
val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEachIndexed { index, file ->
val requestFile = file
.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val filePart = MultipartBody.Part.createFormData(
"foodPics",
file.name,
requestFile
)
fileParts.add(filePart)
}
return safeApiCall {
apiService.uploadCollectFoodPics(params = params, foodPics = fileParts)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun getCollectedFoodVector(param: MutableMap<String, String>): ApiResponse<List<FoodVector>?> {
return safeApiCall {
apiService.getCollectedFoodVector(param = param)
}
}
suspend fun deleteCollectFood(foodId:String?, version: String?): ApiResponse<Any?> {
return safeApiCall {
apiService.deleteCollectFood(param = mutableMapOf(
"foodId" to foodId,
"version" to version
))
}
}
}
@@ -4,8 +4,10 @@ import com.sw.plate.utils.ToastUtils
import com.wabon.wbintelligenthardwaresdk.api.SensorScale
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
import timber.log.Timber
import kotlin.math.roundToInt
typealias Callback = (Double) -> Unit
typealias WeightCallback = (Double) -> Unit
private const val TAG = "SensorScaleUtils"
@@ -41,6 +43,10 @@ object SensorScaleUtils {
lastWeight = value
callback?.invoke(value)
}
weightCallbackList.forEach {
it.invoke(value)
}
}
/**
@@ -60,7 +66,7 @@ object SensorScaleUtils {
* 开启称重
* @param autoScale 是否开启自动读取
*/
fun startScale(autoScale: Boolean = true, callback: Callback?) {
fun startScale(autoScale: Boolean = true, callback: Callback? = null) {
Timber.d("startScale isOpened = $isOpened")
if (isOpened) {
startContinuousRead(callback = callback)
@@ -79,7 +85,9 @@ object SensorScaleUtils {
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化
mSensorScale?.startContinuousRead()
// Thread.sleep(1000)
zero()
// TODO: 暂时不标定---------
//zero()
}.start()
}
@@ -112,6 +120,13 @@ object SensorScaleUtils {
mSensorScale?.readWeight()
}
private var weightCallbackList: MutableList<WeightCallback> = mutableListOf()
fun addWeightListener(weightCallback: WeightCallback) {
if (weightCallbackList.contains(weightCallback).not()) {
weightCallbackList.add(weightCallback)
}
}
/**
* 零位标定
*/
@@ -0,0 +1,300 @@
package com.sw.dualscreen.socket;
import android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送
* 可用于 Android 和 JVM 程序。
*/
public class LanCommunicationManager {
// ============ 监听配置 ============
private final int port;
private final long HEARTBEAT_TIMEOUT_MS;
private final int MAX_CLIENT_THREADS;
// ============ 状态 ============
private volatile boolean running = false;
private ServerSocket serverSocket;
// ============ 线程池 ============
private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService clientExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// ============ 客户端会话 ============
private final ConcurrentHashMap<String, ClientSession> clients = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Socket, ClientSession> unAuthSessions = new ConcurrentHashMap<>();
// ============ 回调接口 ============
public interface Listener {
// 新客户端完成 AUTH / 认证
void onClientConnected(String clientId);
// 客户端断开
void onClientDisconnected(String clientId);
// 收到业务消息(type != heartbeat/auth)
void onMessageReceived(String clientId, JSONObject message);
}
private Listener listener;
// ============ 构造 ============
public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) {
this.port = port;
this.MAX_CLIENT_THREADS = maxClientThreads;
this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs;
this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads));
}
public void setListener(Listener listener) {
this.listener = listener;
}
// ============ 启动服务端 ============
@SuppressLint("DiscouragedApi")
public void start() throws IOException {
if (running) return;
running = true;
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(2000);
acceptExecutor.execute(this::acceptLoop);
scheduler.scheduleAtFixedRate(this::heartbeatCheck,
HEARTBEAT_TIMEOUT_MS,
HEARTBEAT_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
System.out.println("LanCommunicationManager started on port " + port);
}
private void acceptLoop() {
while (running) {
try {
Socket socket = serverSocket.accept();
socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2);
ClientSession session = new ClientSession(socket);
unAuthSessions.put(socket, session);
clientExecutor.execute(() -> clientReadLoop(session));
} catch (SocketTimeoutException ignore) {
} catch (Exception e) {
if (running) e.printStackTrace();
}
}
}
// ============ 处理客户端数据读取 ============
private void clientReadLoop(ClientSession session) {
Socket socket = session.socket;
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
while (running && !socket.isClosed()) {
int len;
try {
len = in.readInt();
} catch (SocketTimeoutException ste) {
continue;
}
if (len <= 0 || len > 10 * 1024 * 1024) break;
byte[] buf = new byte[len];
in.readFully(buf);
session.updateLastSeen();
JSONObject msg = new JSONObject(new String(buf));
handleMessage(session, msg);
}
} catch (Exception ignored) {
} finally {
closeSession(session);
}
}
private void handleMessage(ClientSession session, JSONObject msg) {
String type = msg.optString("type", "");
switch (type) {
case "auth":
handleAuth(session, msg);
break;
case "heartbeat":
session.updateLastSeen();
break;
default:
if (listener != null && session.clientId != null) {
listener.onMessageReceived(session.clientId, msg);
}
break;
}
}
private void handleAuth(ClientSession session, JSONObject msg) {
String clientId = msg.optString("clientId", null);
if (clientId == null) return;
session.clientId = clientId;
// 移动到已认证 map
unAuthSessions.remove(session.socket);
clients.put(clientId, session);
if (listener != null) listener.onClientConnected(clientId);
sendToSession(session, ack("auth_ok"));
}
// ============ 心跳超时 ============
private void heartbeatCheck() {
long now = System.currentTimeMillis();
for (Map.Entry<String, ClientSession> e : clients.entrySet()) {
ClientSession s = e.getValue();
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) {
closeSession(s);
}
}
for (ClientSession s : unAuthSessions.values()) {
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) {
closeSession(s);
}
}
}
// ============ 发送 ============
public boolean sendToClient(String clientId, JSONObject json) {
ClientSession s = clients.get(clientId);
return s != null && sendToSession(s, json);
}
public void broadcast(JSONObject json) {
for (ClientSession s : clients.values()) {
sendToSession(s, json);
}
}
private boolean sendToSession(ClientSession s, JSONObject json) {
try {
DataOutputStream out = s.out;
synchronized (out) {
byte[] data = json.toString().getBytes();
out.writeInt(data.length);
out.write(data);
out.flush();
}
return true;
} catch (Exception e) {
closeSession(s);
return false;
}
}
// ============ ACK ============
private JSONObject ack(String type) {
JSONObject j = new JSONObject();
try {
j.put("type", "ack");
j.put("ack", type);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return j;
}
// ============ 停止 ============
public void stop() {
running = false;
try {
serverSocket.close();
} catch (Exception ignored) {
}
for (ClientSession s : clients.values()) closeSession(s);
for (ClientSession s : unAuthSessions.values()) closeSession(s);
acceptExecutor.shutdownNow();
clientExecutor.shutdownNow();
scheduler.shutdownNow();
System.out.println("LanCommunicationManager stopped");
}
// ============ 会话类 ============
public static class ClientSession {
public final Socket socket;
public final DataOutputStream out;
public volatile long lastSeen = System.currentTimeMillis();
public volatile String clientId;
public ClientSession(Socket socket) throws IOException {
this.socket = socket;
this.out = new DataOutputStream(socket.getOutputStream());
}
public void updateLastSeen() {
lastSeen = System.currentTimeMillis();
}
}
public void closeSession(ClientSession session) {
if (session == null) return;
try {
Socket socket = session.socket;
// 1. 从已认证表移除
if (session.clientId != null) {
ClientSession removed = clients.remove(session.clientId);
if (removed != null && listener != null) {
listener.onClientDisconnected(session.clientId);
}
}
// 2. 从未认证表移除
unAuthSessions.remove(socket);
// 3. 关闭输出流
try {
session.out.close();
} catch (Exception ignored) {
}
// 4. 关闭 socket
try {
if (!socket.isClosed()) socket.close();
} catch (Exception ignored) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,21 @@
package com.sw.dualscreen.socket;
public class LanServer {
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
if (instance == null) {
synchronized (LanServer.class) {
if (instance == null) {
instance = new LanCommunicationManager(
5000, // 监听端口
20, // 最大客户端数
10_000 // 心跳超时时间 10 秒
);
}
}
}
return instance;
}
}
@@ -0,0 +1,21 @@
package com.sw.dualscreen.socket
import android.util.Log
import org.json.JSONObject
open class LanServerListenerImpl: LanCommunicationManager.Listener {
companion object {
private const val TAG = "LanServerListenerImpl"
}
override fun onClientConnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId")
}
override fun onClientDisconnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId")
}
override fun onMessageReceived(clientId: String?, message: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientIdmessage = $message")
}
}
@@ -0,0 +1,330 @@
package com.sw.dualscreen.socket;
import android.util.Log;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* TcpClient - Android ready
* <p>
* Features:
* - length-prefix protocol (int length + bytes)
* - separate read thread and write queue & writer thread
* - auto-reconnect with exponential backoff
* - heartbeat scheduler
* - send queue with optional callback for send result
* - auto send "auth" JSON after connection
*/
public class TcpClient {
private static final String TAG = "TcpClient";
// configuration
private final String serverIp;
private final int serverPort;
private final String clientId; // will be sent in auth message
private final int connectTimeoutMs;
private final long heartbeatIntervalMs;
private final long heartbeatTimeoutMs;
// socket + streams
private Socket socket;
private DataOutputStream out;
private DataInputStream in;
// threads & executors
private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer"));
private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader"));
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler"));
private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect"));
// send queue
private final BlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<>();
// state
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicBoolean connected = new AtomicBoolean(false);
private final AtomicBoolean authSent = new AtomicBoolean(false);
// reconnection/backoff
private final long baseReconnectDelayMs = 1000; // 1s
private final long maxReconnectDelayMs = 30_000; // 30s
private final AtomicInteger reconnectAttempt = new AtomicInteger(0);
// heartbeat task future
private ScheduledFuture<?> heartbeatFuture;
// listener
public interface Listener {
void onConnected();
void onDisconnected(Exception e);
void onMessage(JSONObject json);
void onSendSuccess(JSONObject json);
void onSendFailed(JSONObject json, Exception e);
}
private Listener listener;
public void setListener(Listener l) {
this.listener = l;
}
// ctor
public TcpClient(String serverIp, int serverPort, String clientId,
int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.clientId = clientId;
this.connectTimeoutMs = connectTimeoutMs;
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
}
// start client (will attempt connect)
public void start() {
if (running.getAndSet(true)) return;
scheduleConnect(0);
// writer thread drains sendQueue
writerExecutor.execute(this::writerLoop);
}
// stop client and cleanup
public void stop() {
running.set(false);
cancelHeartbeat();
closeSocketQuiet();
writerExecutor.shutdownNow();
readerExecutor.shutdownNow();
scheduler.shutdownNow();
connectExecutor.shutdownNow();
sendQueue.clear();
}
// send JSON (queued). Non-blocking.
public void send(JSONObject json) {
if (!running.get()) return;
sendQueue.offer(json);
}
// AUTH shortcut (immediately send auth JSON)
private void sendAuth() {
try {
JSONObject auth = new JSONObject();
auth.put("type", "auth");
auth.put("clientId", clientId);
sendQueue.offer(auth);
authSent.set(true);
} catch (Exception ignored) {
}
}
// writer thread loop (serializes sends)
private void writerLoop() {
while (running.get()) {
try {
JSONObject json = sendQueue.take(); // blocks
if (connected.get() && out != null) {
try {
byte[] data = json.toString().getBytes();
synchronized (out) {
out.writeInt(data.length);
out.write(data);
out.flush();
}
if (listener != null) listener.onSendSuccess(json);
} catch (Exception e) {
if (listener != null) listener.onSendFailed(json, e);
// on write failure, attempt reconnect
safeCloseAndScheduleReconnect(e);
}
} else {
// not connected: requeue it and wait for connection
sendQueue.offer(json);
Thread.sleep(500); // avoid busy loop
}
} catch (InterruptedException ignored) {
break;
}
}
}
// reader loop (runs in readerExecutor)
private void startReaderLoop() {
readerExecutor.execute(() -> {
try {
while (running.get() && connected.get() && in != null) {
int length;
try {
length = in.readInt(); // will throw SocketTimeoutException if set
} catch (SocketTimeoutException ste) {
// used to detect socket liveness; continue loop
continue;
}
if (length <= 0 || length > 10 * 1024 * 1024) {
// invalid length, break
throw new RuntimeException("Invalid message length: " + length);
}
byte[] buf = new byte[length];
in.readFully(buf);
String s = new String(buf);
try {
JSONObject json = new JSONObject(s);
// update last seen time via heartbeat ack if needed
if ("heartbeat".equals(json.optString("type"))) {
// optionally respond or update time
} else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) {
// ignore or process ack
} else {
if (listener != null) listener.onMessage(json);
}
} catch (Exception je) {
Log.w(TAG, "Invalid JSON from server: " + s, je);
}
}
} catch (Exception e) {
if (running.get()) {
safeCloseAndScheduleReconnect(e);
}
}
});
}
// schedule connect attempt with delay (ms)
private void scheduleConnect(long delayMs) {
connectExecutor.execute(() -> {
try {
if (delayMs > 0) Thread.sleep(delayMs);
} catch (InterruptedException ignored) {
}
if (!running.get()) return;
tryConnect();
});
}
// connect logic
private void tryConnect() {
if (!running.get()) return;
closeSocketQuiet(); // ensure closed
try {
Socket s = new Socket();
s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs);
s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000));
socket = s;
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
connected.set(true);
reconnectAttempt.set(0);
authSent.set(false);
// start reader
startReaderLoop();
// send auth immediately
sendAuth();
// start heartbeat
startHeartbeat();
if (listener != null) listener.onConnected();
Log.i(TAG, "Connected to " + serverIp + ":" + serverPort);
} catch (Exception e) {
Log.w(TAG, "Connect failed: " + e.getMessage());
scheduleReconnectWithBackoff();
}
}
// start heartbeat scheduler
private void startHeartbeat() {
cancelHeartbeat();
heartbeatFuture = scheduler.scheduleAtFixedRate(() -> {
if (!running.get() || !connected.get()) return;
try {
JSONObject hb = new JSONObject();
hb.put("type", "heartbeat");
hb.put("time", System.currentTimeMillis());
sendQueue.offer(hb);
} catch (Exception ignored) {
}
}, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS);
}
private void cancelHeartbeat() {
if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
heartbeatFuture.cancel(true);
heartbeatFuture = null;
}
}
// close socket quietly and notify listener
private void safeCloseAndScheduleReconnect(Exception cause) {
closeSocketQuiet();
if (listener != null) listener.onDisconnected(cause);
scheduleReconnectWithBackoff();
}
private void scheduleReconnectWithBackoff() {
int attempt = reconnectAttempt.incrementAndGet();
long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs);
Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms");
scheduleConnect(delay);
}
// compute exponential backoff
private long maxReconnectDelayMsFromAttempt(int attempt) {
long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30));
if (d < 0) d = maxReconnectDelayMs;
return Math.min(d, maxReconnectDelayMs);
}
// close socket and streams
private void closeSocketQuiet() {
connected.set(false);
cancelHeartbeat();
try {
if (out != null) {
out.close();
}
} catch (Exception ignored) {
}
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (Exception ignored) {
}
out = null;
in = null;
socket = null;
}
// helper: when manually call reconnect (immediately)
public void reconnectNow() {
scheduleConnect(0);
}
// helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered)
public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException {
return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS);
}
}
@@ -0,0 +1,32 @@
package com.sw.dualscreen.socket
import android.util.Log
import org.json.JSONObject
import java.lang.Exception
open class TcpClientListenerImpl : TcpClient.Listener {
companion object {
private const val TAG = "TcpClientListenerImpl"
}
override fun onConnected() {
Log.d(TAG, "addSocketListener,onConnected: ")
}
override fun onDisconnected(e: Exception?) {
Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}")
}
override fun onMessage(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessage: $json")
}
override fun onSendSuccess(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onSendSuccess: $json")
}
override fun onSendFailed(json: JSONObject?, e: Exception?) {
Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}")
}
}
@@ -0,0 +1,47 @@
package com.sw.dualscreen.socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* Simple UDP discovery client:
* Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip"
*/
public class UdpDiscoveryClient {
public interface Listener {
void onFound(String ip, String serverName);
void onError(Exception e);
}
public void discover(int discoveryPort, int timeoutMs, Listener listener) {
new Thread(() -> {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
byte[] data = "DISCOVER_SERVER".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort);
socket.send(packet);
socket.setSoTimeout(timeoutMs);
byte[] buf = new byte[512];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
socket.receive(resp);
String msg = new String(resp.getData(), 0, resp.getLength());
if (msg.startsWith("SERVER_FOUND")) {
// format: SERVER_FOUND:serverName:ip
String[] parts = msg.split(":", 3);
if (parts.length >= 3) {
listener.onFound(parts[2], parts[1]);
return;
}
}
listener.onError(new Exception("Invalid response"));
} catch (Exception e) {
listener.onError(e);
}
}).start();
}
}
@@ -0,0 +1,113 @@
package com.sw.dualscreen.socket;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class test {
LanCommunicationManager manager = new LanCommunicationManager(
9999, // 端口
20, // 最大客户端线程
30_000 // 心跳超时 30 秒
);
public void test() {
manager.setListener(new LanCommunicationManager.Listener() {
@Override
public void onClientConnected(String clientId) {
System.out.println("新的客户端上线:" + clientId);
}
@Override
public void onClientDisconnected(String clientId) {
System.out.println("客户端离线:" + clientId);
}
@Override
public void onMessageReceived(String clientId, JSONObject msg) {
System.out.println("收到 " + clientId + " 的消息:" + msg);
}
});
// 启动
try {
manager.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 给某个客户端发送
JSONObject j = new JSONObject();
try {
j.put("type", "cmd");
j.put("content", "hello");
} catch (JSONException e) {
throw new RuntimeException(e);
}
manager.sendToClient("device123", j);
// 广播
manager.broadcast(j);
}
public void clientTets() {
// 1) discover server (optional)
UdpDiscoveryClient disc = new UdpDiscoveryClient();
disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() {
@Override
public void onFound(String ip, String serverName) {
startClient(ip);
}
@Override
public void onError(Exception e) { /* fallback to manual IP */ }
});
}
// 2) start client
private TcpClient client;
private void startClient(String serverIp) {
client = new TcpClient(
serverIp,
9999,
"device123", // clientId
5000, // connectTimeoutMs
10_000, // heartbeatIntervalMs
30_000 // heartbeatTimeoutMs
);
client.setListener(new TcpClient.Listener() {
@Override
public void onConnected() {
Log.i("APP", "connected");
}
@Override
public void onDisconnected(Exception e) {
Log.i("APP", "disconnected", e);
}
@Override
public void onMessage(JSONObject json) {
Log.i("APP", "msg:" + json);
}
@Override
public void onSendSuccess(JSONObject json) {
}
@Override
public void onSendFailed(JSONObject json, Exception e) {
}
});
client.start();
}
}
@@ -0,0 +1,57 @@
package com.sw.dualscreen.utils
import android.app.Activity
import android.os.Process
import java.util.Stack
object ActivityManager {
private val activityStack = Stack<Activity>()
// 添加Activity到栈
fun addActivity(activity: Activity) {
activityStack.add(activity)
}
// 移除指定Activity
fun removeActivity(activity: Activity) {
activityStack.remove(activity)
}
// 获取当前Activity
fun currentActivity(): Activity? {
return if (activityStack.isEmpty()) null else activityStack.lastElement()
}
// 结束指定Activity
fun finishActivity(activity: Activity) {
if (!activity.isFinishing) {
activity.finish()
}
}
// 结束所有Activity
fun finishAllActivity() {
activityStack.forEach {
if (!it.isFinishing) {
it.finish()
}
}
activityStack.clear()
}
// 退出应用程序
fun exitApp() {
finishAllActivity()
Process.killProcess(Process.myPid())
}
fun getActivityStack(): Stack<Activity>{
return activityStack
}
fun isEmpty() : Boolean {
return activityStack.isEmpty()
}
}
@@ -1,6 +1,7 @@
package com.sw.dualscreen.utils
import android.graphics.Bitmap
object BitmapCropper {
/**
* 裁剪Bitmap中心区域为指定尺寸
@@ -9,30 +10,33 @@ object BitmapCropper {
* @param targetHeight 目标高度
* @return 裁剪后的Bitmap
*/
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int, offsetX:Int = 0, offsetY:Int = 0): Bitmap {
val originalWidth = original.width
val originalHeight = original.height
// 计算中心点坐标
var startX = (originalWidth - targetWidth) / 2
var startY = (originalHeight - targetHeight) / 2
var startX = (originalWidth - targetWidth) / 2 + offsetX
var startY = (originalHeight - targetHeight) / 2 + offsetY
// 边界检查
startX = startX.coerceAtLeast(0)
startY = startY.coerceAtLeast(0)
val actualWidth = minOf(targetWidth, originalWidth - startX)
val actualHeight = minOf(targetHeight, originalHeight - startY)
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight)
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight).also {
// it.setConfig(Bitmap.Config.RGB_565)
// it.density = DisplayMetrics.DENSITY_LOW
}
}
}
// 使用示例
fun main() {
// 假设这是从资源加载的Bitmap
val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
// 裁剪中心500x500区域
val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
}
//fun main() {
// // 假设这是从资源加载的Bitmap
// val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
// // 裁剪中心500x500区域
// val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
// println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
//}
@@ -11,16 +11,18 @@ class CameraUtils(private var activity: ComponentActivity) {
private var cameraController: LifecycleCameraController? = null
private var photoCaptureHelper: PhotoCaptureHelper? = null
private var failCallback: ((msg: String) -> Unit)? = null
// private var isCameraReady = false
fun takePhoto(callback: (Uri) -> Unit) {
fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) {
this.failCallback = failCallback
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(callback)
it.addSuccessCallback(succCallback)
it.bindCameraCallback {
bind()
}
@@ -35,6 +37,7 @@ class CameraUtils(private var activity: ComponentActivity) {
onSuccess = {},
onError = { msg ->
//toast(msg)
failCallback?.invoke(msg)
}
)
}
@@ -0,0 +1,32 @@
package com.sw.dualscreen.utils
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
object DateTimeUtil {
const val YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"
fun formatDateTime(dateTime: LocalDateTime, pattern: String = YYYY_MM_DD_HH_MM_SS): String {
val formatter = DateTimeFormatter.ofPattern(pattern)
return dateTime.format(formatter)
}
fun convert(dateStr: String, pattern: String = YYYY_MM_DD_HH_MM_SS): Date {
val formatter = DateTimeFormatter.ofPattern(pattern)
val ldt = LocalDateTime.parse(dateStr, formatter)
val zdt = ldt.atZone(ZoneId.systemDefault())
return Date.from(zdt.toInstant())
}
fun main() {
val now = LocalDateTime.now()
println("默认格式: ${formatDateTime(now)}")
println("自定义格式: ${formatDateTime(now, "yyyy年MM月dd日 HH时mm分ss秒")}")
}
}
@@ -0,0 +1,697 @@
package com.sw.dualscreen.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.sw.plate.App;
import com.sw.plate.utils.AppUtil;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class FileUtil {
private static boolean isSaveLog = true;
public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值
public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值
public static final int SIZETYPE_GB = 4;//获取文件大小单位为GB的double值
private static String urlNull = "原文件路径不存在";
private static String isFile = "原文件不是文件";
private static String canRead = "原文件不能读";
private static String copyFalse = "备份失败!";
private static String cFromFile = "创建原文件出错:";
private static String ctoFile = "创建备份文件出错:";
/**
* 写入文件
* FileUtil.byteWriteFile(getExternalFilesDir("123"),"test.jpg",nv21);
*
* @param filePath
* @param fileName
* @param bytes
*/
public static void byteWriteFile(File filePath, String fileName, byte[] bytes) {
BufferedOutputStream bout = null;
try {
File file = new File(filePath, fileName);
if (file.exists() == false) {
if (file.getParentFile().exists() == false) {
file.getParentFile().mkdirs();
}
file.createNewFile();
}
bout = new BufferedOutputStream(new FileOutputStream(file, false));
bout.write(bytes);
bout.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取文件名
*
* @param path 路径
* @return 文件名
*/
public static String getFileName(String path) {
int index = path.lastIndexOf("/");
return path.substring(index + 1);
}
/**
* 根据文件路径获取文件
*
* @param path 路径
* @return
*/
public static File getFileByPath(String path) {
File file = new File(path);
if (file.exists()) {
return new File(path);
} else {
return null;
}
}
/**
* 重命名文件
*
* @param filePath 文件路径
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(String filePath, String newName) {
return rename(getFileByPath(filePath), newName);
}
/**
* 重命名文件
*
* @param file 文件
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(File file, String newName) {
// 文件为空返回false
if (file == null) return false;
// 文件不存在返回false
if (!file.exists()) return false;
// 新的文件名为空返回false
if (AppUtil.isEmpty(newName)) return false;
// 如果文件名没有改变返回true
if (newName.equals(file.getName())) return true;
File newFile = new File(file.getParent() + File.separator + newName);
// 如果重命名的文件已存在返回false
return !newFile.exists()
&& file.renameTo(newFile);
}
/**
* 判断文件是否存在
*
* @param path 文件的路径,含文件后缀和文件名
* @return 是否存在
*/
public static boolean isFileExists(String path) {
File file = new File(path);
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 判断文件是否存在
*
* @return 是否存在
*/
public static boolean isFileExists(File file) {
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param path 文件夹路径
* @return 返回是否删除成功
*/
public static boolean deleteFiles(String path) {
if (getFileByPath(path) != null) {
return deleteFiles(getFileByPath(path));
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param file 文件
* @return 返回是否删除成功
*/
public static boolean deleteFiles(File file) {
try {
if (file.exists()) { // 判断文件是否存在
if (file.isFile()) { // 判断是否是文件
file.delete(); // delete()方法
} else if (file.isDirectory()) { // 否则如果它是一个目录
File files[] = file.listFiles(); // 声明目录下所有的文件 files[];
for (int i = 0; i < files.length; i++) { // 遍历目录下所有的文件
deleteFiles(files[i].getPath()); // 把每个文件 用这个方法进行迭代
}
file.delete();//删除目录
}
//file.delete();
return true;
} else {
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取文件的Uri
*
* @param path 文件的路径
* @return 文件的Uri
*/
public static Uri getUriFromFile(String path) {
File file = new File(path);
return Uri.fromFile(file);
}
/**
* 获取文件指定文件的指定单位的大小
*
* @param filePath 文件路径
* @param sizeType 获取大小的类型1为B、2为KB、3为MB、4为GB
* @return double值的大小
*/
public static double getFileOrFilesSize(String filePath, int sizeType) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize, sizeType);
}
/**
* 调用此方法自动计算指定文件或指定文件夹的大小
*
* @param filePath 文件路径
* @return 计算好的带B、KB、MB、GB的字符串
*/
public static String getAutoFileOrFilesSize(String filePath) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize);
}
/**
* 获取指定文件大小
*
* @param file
* @return
* @throws Exception
*/
private static long getFileSize(File file) throws Exception {
long size = 0;
if (file.exists()) {
FileInputStream fis = null;
fis = new FileInputStream(file);
size = fis.available();
} else {
// file.createNewFile();
L.i("获取文件大小", "文件不存在!");
}
return size;
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dirPath 目录路径
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(String dirPath, String fileName) {
return searchFileInDir(getFileByPath(dirPath), fileName);
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dir 目录
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(File dir, String fileName) {
if (dir == null || !dir.isDirectory()) return null;
List<File> list = new ArrayList<>();
File[] files = dir.listFiles();
if (files != null && files.length != 0) {
for (File file : files) {
if (file.getName().toUpperCase().equals(fileName.toUpperCase())) {
list.add(file);
}
if (file.isDirectory()) {
list.addAll(searchFileInDir(file, fileName));
}
}
}
return list;
}
/**
* 获取指定文件夹
*
* @param f
* @return
* @throws Exception
*/
private static long getFileSizes(File f) throws Exception {
long size = 0;
File flist[] = f.listFiles();
for (int i = 0; i < flist.length; i++) {
if (flist[i].isDirectory()) {
size = size + getFileSizes(flist[i]);
} else {
size = size + getFileSize(flist[i]);
}
}
return size;
}
/**
* 转换文件大小
*
* @param fileS
* @return
*/
private static String FormetFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileS == 0) {
return wrongSize;
}
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + "B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + "KB";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + "MB";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + "GB";
}
return fileSizeString;
}
/**
* 转换文件大小,指定转换的类型
*
* @param fileS
* @param sizeType
* @return
*/
private static double FormetFileSize(long fileS, int sizeType) {
DecimalFormat df = new DecimalFormat("#.00");
double fileSizeLong = 0;
switch (sizeType) {
case SIZETYPE_B:
fileSizeLong = Double.valueOf(df.format((double) fileS));
break;
case SIZETYPE_KB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1024));
break;
case SIZETYPE_MB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1048576));
break;
case SIZETYPE_GB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1073741824));
break;
default:
break;
}
return fileSizeLong;
}
/**
* 复制文件
*
* @param fromFilePath 旧文件地址和名称
* @param toFilePath 新文件地址和名称
* @return 返回备份文件的信息,ok是成功,其它就是错误
*/
public static File copyFile(String fromFilePath, String toFilePath) {
File fromFile = null;
File toFile = null;
try {
fromFile = new File(fromFilePath);
} catch (Exception e) {
L.i(cFromFile + e.getMessage());
return null;
}
try {
toFile = new File(toFilePath);
if (toFile.isDirectory()) {
toFile = new File(toFilePath.endsWith("/") ? toFilePath + fromFile.getName() : toFilePath + "/" + fromFile.getName());
}
} catch (Exception e) {
L.i(ctoFile + e.getMessage());
return null;
}
if (!fromFile.exists()) {
L.i(urlNull);
return null;
}
if (!fromFile.isFile()) {
L.i(isFile);
return null;
}
if (!fromFile.canRead()) {
L.i(canRead);
return null;
}
// 复制到的路径如果不存在就创建
if (!toFile.getParentFile().exists()) {
toFile.getParentFile().mkdirs();
}
if (toFile.exists()) {
toFile.delete();
}
if (!toFile.canWrite()) {
//return notWrite;
}
try {
FileInputStream fosfrom = new FileInputStream(
fromFile);
FileOutputStream fosto = new FileOutputStream(toFile);
byte bt[] = new byte[1024];
int c;
while ((c = fosfrom.read(bt)) > 0) {
fosto.write(bt, 0, c); // 将内容写到新文件当中
}
//关闭数据流
fosfrom.close();
fosto.close();
} catch (Exception e) {
e.printStackTrace();
L.i(copyFalse + e.getMessage());
return null;
}
return toFile;
}
/**
* 创建文件
*
* @param path 文件路径
* @return 创建的文件
*/
public static synchronized File createNewFile(String path) {
File file = new File(path);
String parentPath = path.substring(0, path.lastIndexOf("/") + 1);
File parentFile = new File(parentPath);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return file;
}
/***
* 根据文件后缀回去MIME类型
****/
private static String getMIMEType(File file) {
String type = "*/*";
String fName = file.getName();
//获取后缀名前的分隔符"."在fName中的位置。
int dotIndex = fName.lastIndexOf(".");
if (dotIndex < 0) {
return type;
}
/* 获取文件的后缀名*/
String end = fName.substring(dotIndex, fName.length()).toLowerCase();
if (end == "") return type;
//在MIME和文件类型的匹配表中找到对应的MIME类型。
for (int i = 0; i < MIME_MapTable.length; i++) { //MIME_MapTable??在这里你一定有疑问,这个MIME_MapTable是什么?
if (end.equals(MIME_MapTable[i][0]))
type = MIME_MapTable[i][1];
}
return type;
}
/**
* 获取全路径中的文件拓展名
*
* @param file 文件
* @return 文件拓展名
*/
public static String getFileExtension(File file) {
if (file == null) return null;
return getFileExtension(file.getPath());
}
/**
* 获取全路径中的文件拓展名
*
* @param filePath 文件路径
* @return 文件拓展名
*/
public static String getFileExtension(String filePath) {
if (AppUtil.isEmpty(filePath)) return filePath;
int lastPoi = filePath.lastIndexOf('.');
int lastSep = filePath.lastIndexOf(File.separator);
if (lastPoi == -1 || lastSep >= lastPoi) return "";
return filePath.substring(lastPoi + 1);
}
/**
* 调用系统应用打开文件
*
* @param context 上下文
* @param file 文件
*/
public static void openFile(Context context, File file) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//设置intent的Action属性
intent.setAction(Intent.ACTION_VIEW);
//获取文件file的MIME类型
String type = getMIMEType(file);
//设置intent的data和Type属性。
intent.setDataAndType(Uri.fromFile(file), type);
//跳转
try {
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
L.i("找不到打开此文件的应用!");
}
}
/**
* 保存文本文件
* FileUtil.saveStrFile("测试", "log.txt", FILE_STR_PATH, true);
*
* @param res 写入内容
* @param fileName 文件名
* @param filePath 路径
* @param append true新增 false替换
* @return
*/
public static boolean saveStrFile(String res, String fileName, String filePath, boolean append) {
boolean flag = true;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
File file = new File(filePath, fileName);
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
bufferedReader = new BufferedReader(new StringReader(res));
bufferedWriter = new BufferedWriter(new FileWriter(file, append));
char buf[] = new char[1024]; //字符缓冲区
int len;
while ((len = bufferedReader.read(buf)) != -1) {
bufferedWriter.write(buf, 0, len);
}
bufferedWriter.flush();
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
flag = false;
return flag;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return flag;
}
public static void saveLog(String... logs) {
if (!isSaveLog) {
return;
}
new Thread() {
@Override
public void run() {
super.run();
StringBuilder sb = new StringBuilder();
for (String log : logs) {
sb.append("===")
.append((AppUtil.formatDateGetCurrentTime()))
.append("===")
.append("\n")
.append(log)
.append("\n");
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
FileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
}
}.start();
}
private static final String[][] MIME_MapTable = {
// {后缀名,MIME类型}
{".3gp", "video/3gpp"},
{".apk", "application/vnd.android.package-archive"},
{".asf", "video/x-ms-asf"},
{".avi", "video/x-msvideo"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".c", "text/plain"},
{".class", "application/octet-stream"},
{".conf", "text/plain"},
{".cpp", "text/plain"},
{".doc", "application/msword"},
{".docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".xls", "application/vnd.ms-excel"},
{".xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".exe", "application/octet-stream"},
{".gif", "image/gif"},
{".gtar", "application/x-gtar"},
{".gz", "application/x-gzip"},
{".h", "text/plain"},
{".htm", "text/html"},
{".html", "text/html"},
{".jar", "application/java-archive"},
{".java", "text/plain"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "application/x-javascript"},
{".log", "text/plain"},
{".m3u", "audio/x-mpegurl"},
{".m4a", "audio/mp4a-latm"},
{".m4b", "audio/mp4a-latm"},
{".m4p", "audio/mp4a-latm"},
{".m4u", "video/vnd.mpegurl"},
{".m4v", "video/x-m4v"},
{".mov", "video/quicktime"},
{".mp2", "audio/x-mpeg"},
{".mp3", "audio/x-mpeg"},
{".mp4", "video/mp4"},
{".mpc", "application/vnd.mpohun.certificate"},
{".mpe", "video/mpeg"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
{".mpg4", "video/mp4"},
{".mpga", "audio/mpeg"},
{".msg", "application/vnd.ms-outlook"},
{".ogg", "audio/ogg"},
{".pdf", "application/pdf"},
{".png", "image/png"},
{".pps", "application/vnd.ms-powerpoint"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".prop", "text/plain"}, {".rc", "text/plain"},
{".rmvb", "audio/x-pn-realaudio"}, {".rtf", "application/rtf"},
{".sh", "text/plain"}, {".tar", "application/x-tar"},
{".tgz", "application/x-compressed"}, {".txt", "text/plain"},
{".wav", "audio/x-wav"}, {".wma", "audio/x-ms-wma"},
{".wmv", "audio/x-ms-wmv"},
{".wps", "application/vnd.ms-works"}, {".xml", "text/plain"},
{".z", "application/x-compress"},
{".zip", "application/x-zip-compressed"}, {"", "*/*"}};
}
@@ -0,0 +1,32 @@
package com.sw.dualscreen.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
// 倒计时Flow扩展函数
fun countDownByFlow(
total: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onStart: (() -> Unit)? = null,
onFinish: (() -> Unit)? = null
): Job {
return flow {
for (i in total downTo 0) {
emit(i)
if (i != 0) delay(1000)
}
}.flowOn(Dispatchers.Main)
.onStart { onStart?.invoke() }
.onCompletion { onFinish?.invoke() }
.onEach { onTick.invoke(it) }
.launchIn(scope)
}
@@ -104,6 +104,21 @@ object GlideUtils {
.into(imageView)
}
fun loadRoundCornerWitBitmap(
context: Context,
url: Bitmap,
imageView: ImageView,
radius: Int,
@DrawableRes placeholderResId: Int = 0,
@DrawableRes errorResId: Int = 0
) {
val requestBuilder = Glide.with(context)
.load(url)
.apply(RequestOptions.bitmapTransform(RoundedCorners(radius)))
applyPlaceholder(requestBuilder, placeholderResId, errorResId)
.into(imageView)
}
/**
* 加载图片并获取Bitmap
* @param context 上下文
@@ -0,0 +1,38 @@
package com.sw.dualscreen.utils
import com.sw.dualscreen.objbox.FoodCollectionBean
import java.util.concurrent.atomic.AtomicInteger
class ImageUploader(
private val totalList: List<FoodCollectionBean>,
private val uploadImage: suspend (List<FoodCollectionBean>)-> List<String>?,
private val onProgress: (Int, List<FoodCollectionBean>, List<String>) -> Unit,
private val onError:(List<FoodCollectionBean>) -> Unit,
private val onComplete:() -> Unit
) {
companion object {
private const val BATCH_SIZE = 3
}
private val uploadedCount = AtomicInteger(0)
suspend fun processUploads() {
val batches = totalList.filter { it.imageVector!=null }.chunked(BATCH_SIZE)
for (batch in batches) {
val idList = uploadImage(batch)
if (idList.isNullOrEmpty()) {
//println("上传失败,终止流程")
onError(batch)
return
}
val fileCount = batch.count { it.imageFile!=null }
uploadedCount.addAndGet(fileCount)
onProgress(uploadedCount.get(), batch, idList)
//println("已上传 ${uploadedCount.get()}/$totalImages")
}
onComplete()
//println("流程完成,总计上传 ${uploadedCount.get()} 张图片")
}
}
@@ -15,6 +15,15 @@ object ImageUtil {
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
return try {
val options = BitmapFactory.Options()
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2
options.inJustDecodeBounds = false
// options.inPreferredConfig = Bitmap.Config.ARGB_8888
options.inPreferredConfig = Bitmap.Config.RGB_565
context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream, null, options)
// BitmapFactory.decodeStream(stream)
}
context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream)
}
@@ -0,0 +1,64 @@
package com.sw.dualscreen.utils
import kotlinx.coroutines.*
class IntervalExecutor {
/**
* 启动定时任务
* @param delayMillis 延迟时间(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
action()
delay(delayMillis)
}
}
}
/**
* 启动定时任务(带初始延迟)
* @param initialDelay 初始延迟时间(毫秒)
* @param delayMillis 后续执行间隔(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTaskWithInitialDelay(
initialDelay: Long,
delayMillis: Long,
action: suspend () -> Unit
): Job {
return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay)
while (isActive) {
action()
delay(delayMillis)
}
}
}
}
// 使用示例
fun main() = runBlocking {
val executor = IntervalExecutor()
// 示例1:每隔10秒执行一次
val job1 = executor.startIntervalTask(10000) {
println("定时任务执行: ${System.currentTimeMillis()}")
// 这里可以执行你的业务逻辑
}
// 示例2:先延迟5秒,然后每隔3秒执行一次
val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
}
// 运行30秒后取消任务
delay(30000)
job1.cancel()
job2.cancel()
println("所有定时任务已取消")
}
@@ -0,0 +1,57 @@
package com.sw.dualscreen.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -0,0 +1,66 @@
package com.sw.dualscreen.utils
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.text.contains
import kotlin.text.format
import kotlin.text.replace
/**
* 去除小数点后无效零的工具类
* 功能示例:
* 1230.00 => 1230
* 1230.10 => 1230.1
* 3.40 => 3.4
* 3.0 => 3
*/
class RemoveZeroUtils {
companion object {
/**
* 方法1:使用字符串格式化(最简单)
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByFormat(number: Double): String {
return "%.10f".format(number) // 先格式化为固定小数位
.replace(Regex("0*$"), "") // 移除末尾的零
.replace(Regex("\\.$"), "") // 如果小数点后全为零,移除小数点
}
/**
* 方法2:使用DecimalFormat(推荐)
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByDecimalFormat(number: Double): String {
val format = DecimalFormat("0.##########")
format.roundingMode = RoundingMode.FLOOR
return format.format(number)
}
/**
* 方法3:使用正则表达式处理字符串
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByRegex(number: Double): String {
var str = number.toString()
// 如果包含小数点,处理末尾的零
if (str.contains(".")) {
str = str.replace(Regex("0+?$"), "") // 移除末尾的零
.replace(Regex("[.]$"), "") // 如果小数点后全为零,移除小数点
}
return str
}
}
}
// 扩展函数方式,更符合Kotlin风格
fun Double?.removeTrailingZeros(): String {
if (this == null) return ""
return RemoveZeroUtils.removeZeroByDecimalFormat(this)
}
@@ -0,0 +1,19 @@
package com.sw.dualscreen.utils;
import com.sw.dualscreen.MyApp;
public class SpTool {
public static final String LAST_FACE_TIMESTAMP = "faceTimestamp";
public static long getLastFaceTimestamp() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(LAST_FACE_TIMESTAMP, 0L);
}
public static void setLastFaceTimestamp(long timestamp) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(LAST_FACE_TIMESTAMP, timestamp);
}
}
@@ -0,0 +1,36 @@
package com.sw.dualscreen.utils
import android.graphics.Typeface
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import androidx.core.graphics.toColorInt
import androidx.core.text.buildSpannedString
import com.sw.dualscreen.model.response.TextBean
object SpannedUtils {
const val SPAN_FLAGS = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
public fun getAmountText(list: List<TextBean>): SpannedString {
return buildSpannedString {
list.forEach {
val start = length
append(it.text)
val end = length
if (it.textSize != 0) {
setSpan(AbsoluteSizeSpan(it.textSize, true), start, end, SPAN_FLAGS)
}
if (it.textColor.isNullOrBlank().not()) {
setSpan(ForegroundColorSpan(it.textColor!!.toColorInt()), start, end, SPAN_FLAGS)
}
if (it.isBold) {
setSpan(StyleSpan(Typeface.BOLD), start, end, SPAN_FLAGS)
}
}
}
}
}
@@ -9,21 +9,21 @@ object StorageUtils {
activity.requestSinglePermissionResult(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
callback()
}
// activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//// if (Environment.isExternalStorageManager()) {
//// callback()
//// } else {
//// val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
////// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE)
//// activity.launchActivityResult(intent) {
//// callback()
//// }
//// }
//// } else {
//
//// }
// }
activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// if (Environment.isExternalStorageManager()) {
// callback()
// } else {
// val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
//// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE)
// activity.launchActivityResult(intent) {
// callback()
// }
// }
// } else {
// }
}
}
fun requestCameraPermission(activity: BaseActivity<*>, callback: () -> Unit) {
@@ -1,38 +1,50 @@
package com.sw.dualscreen.view
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.adapter.DialogSearchFoodAdapter
import com.sw.dualscreen.databinding.BottomSheetDialogBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.KeyboardUtils
import com.sw.dualscreen.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import androidx.core.graphics.drawable.toDrawable
/**
* 底部搜索结果弹窗
*/
@SuppressLint("NotifyDataSetChanged")
class CustomBottomSheetDialog(
val viewModel: UserViewModel,
val itemClickCallback: (FoodInfo) -> Unit
) : BottomSheetDialogFragment() {
private lateinit var binding: BottomSheetDialogBinding
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
// lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private val searchFoodList = mutableListOf<FoodInfo>()
private val adapter by lazy {
DialogSearchFoodAdapter(searchFoodList).apply {
setOnItemClickListener { adapter, view, position ->
searchFoodList.forEachIndexed { index, info ->
searchFoodList[index].isChecked = index == position
}
val item = searchFoodList[position].also { it.isFromSearch = true }
Timber.d("itemClick ${item.foodName}, position = $position")
itemClickCallback(item)
checkedItem = item
notifyDataSetChanged()
dismissAllowingStateLoss()
}
}
}
private var checkedItem: FoodInfo? = null
private val debouncer = Debouncer(2000)
@@ -60,7 +72,7 @@ class CustomBottomSheetDialog(
private fun setDialogStyle() {
dialog?.window?.let {
it.decorView.background = ColorDrawable(Color.TRANSPARENT)
it.decorView.background = Color.TRANSPARENT.toDrawable()
it.decorView.setPadding(0, 0, 0, 0)
it.attributes?.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
@@ -83,77 +95,89 @@ class CustomBottomSheetDialog(
}
private fun initView() {
binding.viewClose.setOnClickListener { dismiss() }
adapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(30),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
//binding.recyclerview.addItemDecoration(
// GridSpacingItemDecoration(
// spanCount = 2,
// spacing = requireContext().dpToPx(30),
// includeEdge = false // 包含边缘间距
// )
//)
//adapter = createAdapter()
//binding.recyclerview.adapter = adapter
binding.viewClose.setOnClickListener { dismiss() }
binding.recyclerview.let {
it.layoutManager = GridLayoutManager(context, 2)
it.adapter = adapter
}
binding.ivSearch.setOnClickListener {
searchInfo()
searchFood()
}
KeyboardUtils.setupEditorAction(binding.etSearch) {
searchInfo()
searchFood()
}
registerDataChange()
searchFood()
}
private fun searchInfo(){
private fun searchFood(){
debouncer.debounce {
viewModel.searchByFoodName(binding.etSearch.text.toString())
viewModel.searchByFoodName(binding.etSearch.text.toString()) {
searchFoodList.clear()
searchFoodList.addAll(it)
adapter.notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun registerDataChange() {
lifecycleScope.launch {
viewModel.searchFoodInfoList.collect {
adapter.updateData(it)
}
}
// lifecycleScope.launch {
// viewModel.searchFoodInfoList.collect {
// searchFoodList.clear()
// searchFoodList.addAll(it)
// adapter.notifyDataSetChanged()
// }
// }
}
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.foodName
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.search_normal))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
} else {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
this.tvName.setTextColor(resources.getColor(R.color.search_checked))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.foodName}, position = $position")
checkedItem = item
// viewModel.updateCurrentItem(item)
itemClickCallback(item)
adapter.notifyDataSetChanged()
}
}
)
}
// private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
// return GenericItemAdapter(
// items = emptyList(),
// bindingInflater = ItemSearchFoodInfoBinding::inflate,
// bindCallback = { item, position ->
// this.tvName.text = item.foodName
// if (item.id != checkedItem?.id) {
// this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
// this.tvName.setTextColor(resources.getColor(R.color.search_normal))
// this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
// } else {
// this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
// this.tvName.setTextColor(resources.getColor(R.color.search_checked))
// this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
// }
//
// this.llRoot.setOnClickListener {
// Timber.d("itemClick ${item.foodName}, position = $position")
// checkedItem = item
//// viewModel.updateCurrentItem(item)
// itemClickCallback(item)
// adapter.notifyDataSetChanged()
// }
// }
// )
// }
override fun onCancel(dialog: DialogInterface) {
Timber.d("onCancel")
viewModel.cleanSearchFoodInfoList()
// viewModel.cleanSearchFoodInfoList()
super.onCancel(dialog)
}
override fun dismiss() {
Timber.d("dismiss")
viewModel.cleanSearchFoodInfoList()
// viewModel.cleanSearchFoodInfoList()
super.dismiss()
}
}
@@ -3,89 +3,101 @@ package com.sw.dualscreen.view;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import com.sw.dualscreen.R;
/**
* 加载进度条弹窗
*/
public class CustomDialog extends Dialog {
/**
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
*/
public CustomDialog(Context context, View layout) {
super(context, R.style.LoadingDialog);
setContentView(layout);
initConfig();
}
public CustomDialog(Context context, View layout, int style) {
super(context, style);
setContentView(layout);
initConfig();
}
private void initConfig() {
setCancelable(true);
setCanceledOnTouchOutside(true);
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 宽高由该方法的参数设置
*/
public CustomDialog(Context context, int width, int height, View layout,
int style) {
super(context, style);
// 设置内容
setContentView(layout);
// 设置窗口属性
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
// 设置宽度、高度、密度、对齐方式
float density = getDensity(context);
params.width = (int) (width * density);
params.height = (int) (height * density);
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 获取显示密度
*
* @param context
* @return
*/
public float getDensity(Context context) {
Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
return dm.density;
}
private void fullScreenImmersive(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
view.setSystemUiVisibility(uiOptions);
if (window != null) {
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
window.setAttributes(params);
}
}
//
// /**
// * 宽高由该方法的参数设置
// */
// public CustomDialog(Context context, int width, int height, View layout,
// int style) {
// super(context, style);
// // 设置内容
// setContentView(layout);
// // 设置窗口属性
// Window window = getWindow();
// WindowManager.LayoutParams params = window.getAttributes();
// // 设置宽度、高度、密度、对齐方式
// float density = getDensity(context);
// params.width = (int) (width * density);
// params.height = (int) (height * density);
// params.gravity = Gravity.CENTER;
// window.setAttributes(params);
//
// }
//
// /**
// * 获取显示密度
// *
// * @param context
// * @return
// */
// public float getDensity(Context context) {
// Resources res = context.getResources();
// DisplayMetrics dm = res.getDisplayMetrics();
// return dm.density;
// }
//
// private void fullScreenImmersive(View view) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// | View.SYSTEM_UI_FLAG_FULLSCREEN;
// view.setSystemUiVisibility(uiOptions);
// }
//
// }
@Override
// @Override
// public void show() {
// this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
// super.show();
//// fullScreenImmersive(getWindow().getDecorView());
//// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
// }
public void show() {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
// fullScreenImmersive(getWindow().getDecorView());
// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
}
@@ -13,6 +13,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import androidx.appcompat.content.res.AppCompatResources;
import com.sw.dualscreen.R;
import com.sw.plate.utils.AppUtil;
@@ -29,13 +31,14 @@ public class CustomImageView extends androidx.appcompat.widget.AppCompatImageVie
/**
* 最大高度
*/
private static final int MAX_HEIGHT = 280;
// private static final int MAX_HEIGHT = 280;
private static final int MAX_HEIGHT = 250;
private boolean isDrawText = false;
private Drawable bigRedDrawable;
private Drawable bigGreenDrawable;
private Drawable redDrawable;
private Drawable greenDrawable;
// private Drawable bigRedDrawable;
// private Drawable bigGreenDrawable;
// private Drawable redDrawable;
// private Drawable greenDrawable;
public CustomImageView(Context context) {
super(context);
@@ -59,10 +62,10 @@ public class CustomImageView extends androidx.appcompat.widget.AppCompatImageVie
mPaint.setColor(Color.WHITE); // Set your desired color
mPaint.setTextSize(mTextSize); // Set your desired text size
mPaint.setAntiAlias(true);
bigRedDrawable = context.getDrawable(R.drawable.img_big_red_column);
bigGreenDrawable = context.getDrawable(R.drawable.img_big_green_column);
redDrawable = context.getDrawable(R.drawable.img_red_column);
greenDrawable = context.getDrawable(R.drawable.img_green_column);
// bigRedDrawable = context.getDrawable(R.drawable.img_big_red_column);
// bigGreenDrawable = context.getDrawable(R.drawable.img_big_green_column);
// redDrawable = context.getDrawable(R.drawable.img_red_column);
// greenDrawable = context.getDrawable(R.drawable.img_green_column);
// Typeface typeface = getResources().getFont(R.font.dakai);
// Typeface typeface = Typeface.create(Typeface.createFromAsset(getContext().getAssets(), "fonts/dakai.TTF"),
// Typeface.BOLD); // 创建Typeface
@@ -86,23 +89,31 @@ public class CustomImageView extends androidx.appcompat.widget.AppCompatImageVie
}
}
/**
* 设置图片
*
* @param isBig 大图
* @param isMore 是否超过
*/
public void setImageDrawable(boolean isBig, boolean isMore) {
if (isBig) {
setImageDrawable(isMore ? bigRedDrawable : bigGreenDrawable);
} else {
setImageDrawable(isMore ? redDrawable : greenDrawable);
}
}
// /**
// * 设置图片
// *
// * @param isBig 大图
// * @param isMore 是否超过
// */
// public void setImageDrawable(boolean isBig, boolean isMore) {
// if (isBig) {
// setImageDrawable(isMore ? bigRedDrawable : bigGreenDrawable);
// } else {
// setImageDrawable(isMore ? redDrawable : greenDrawable);
// }
// }
//
// public void setImageDrawable2(boolean isBig, Boolean isRed) {
// if (isBig) {
// setImageDrawable(isRed ? bigRedDrawable : bigGreenDrawable);
// } else {
// setImageDrawable(isRed ? redDrawable : greenDrawable);
// }
// }
public void setCustomHeightPercent(float percent, boolean isAnimation) {
Timber.d("setCustomHeightPercent percent = %s", percent);
if (percent<= 0){
if (percent <= 0) {
percent = 0f;
}
setCustomHeight((int) (percent * MAX_HEIGHT), false);
@@ -113,7 +124,7 @@ public class CustomImageView extends androidx.appcompat.widget.AppCompatImageVie
Timber.d("setCustomHeight height = %s", height);
setVisibility(height == 0 ? View.GONE : VISIBLE);
if (height > MAX_HEIGHT){
if (height > MAX_HEIGHT) {
height = MAX_HEIGHT;
}
ViewGroup.LayoutParams layoutParams = getLayoutParams();
@@ -175,4 +186,35 @@ public class CustomImageView extends androidx.appcompat.widget.AppCompatImageVie
float scale = getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
public void setImageDrawable(boolean isBigImage, int index) {
int myIndex = index;
if (index < 0) {
myIndex = 0;
}else if (index > 4) {
myIndex = 4;
}
if (isBigImage) {
//使用大图bigImgArr
setImageDrawable(AppCompatResources.getDrawable(getContext(), bigImgArr[myIndex]));
return;
}
//使用小图smallImgArr
setImageDrawable(AppCompatResources.getDrawable(getContext(), smallImgArr[myIndex]));
}
private static final Integer[] bigImgArr = new Integer[]{
R.drawable.ic_gray_big,
R.drawable.ic_green_big,
R.drawable.ic_yellow_big,
R.drawable.ic_orange_big,
R.drawable.ic_red_big
};
private static final Integer[] smallImgArr = new Integer[]{
R.drawable.ic_gray_small,
R.drawable.ic_green_small,
R.drawable.ic_yellow_small,
R.drawable.ic_orange_small,
R.drawable.ic_red_small
};
}
@@ -0,0 +1,72 @@
package com.sw.dualscreen.view
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import kotlin.math.roundToInt
class HollowCircleRectView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 1. 先定义dp值,再统一转px(避免多次转换误差)
private val rectWidthDp = 400f
private val rectHeightDp = 510f
private val circleRadiusDp = 200f // 直径400dp → 半径200dp(固定正圆关键)
// 2. 转px后的值(只计算一次,避免onDraw重复计算)
private val rectWidthPx: Float = dp2px(rectWidthDp)
private val rectHeightPx: Float = dp2px(rectHeightDp)
private val circleRadiusPx: Float = dp2px(circleRadiusDp)
// 画笔和路径(复用避免重复创建)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val path = Path()
init {
// paint.color = "#FFE7EFF8".toColorInt()
paint.color = "#FFF4FAFF".toColorInt()
paint.style = Paint.Style.FILL
// 强制关闭硬件加速可能的渲染偏差(关键)
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 强制View尺寸为精准px值,避免父布局干扰
val rectWidthPxInt = rectWidthPx.toInt()
val rectHeightPxInt = rectHeightPx.toInt()
Log.d("HollowCircleRectView", "onMeasure: $rectWidthPxInt,$rectHeightPxInt")
setMeasuredDimension(rectWidthPxInt, rectHeightPxInt)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.reset()
// 1. 添加矩形路径(精准px值,无偏差)
path.addRect(0f, 0f, rectWidthPx, rectHeightPx, Path.Direction.CW)
// 2. 添加圆形路径(固定半径,绝对正圆)
val circleX = rectWidthPx / 2 // 水平居中:200dp转px
val circleY = rectHeightPx / 2 // 垂直居中:340dp转px
// 核心修复:用固定半径circleRadiusPx,而非直径/2(避免浮点运算偏差)
Log.d("HollowCircleRectView", "onDraw: $circleX,$circleY,$circleRadiusPx")
path.addCircle(circleX, circleY, circleRadiusPx, Path.Direction.CW)
// 3. 填充规则+绘制
path.fillType = Path.FillType.EVEN_ODD
canvas.drawPath(path, paint)
}
// 精准dp转px(四舍五入避免小数偏差)
private fun dp2px(dp: Float): Float {
val density = context.resources.displayMetrics.density
return (dp * density).roundToInt().toFloat() // 四舍五入到整数px
}
}
@@ -0,0 +1,122 @@
package com.sw.dualscreen.view
import androidx.core.graphics.toColorInt
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
/**
* 带镂空椭圆的矩形自定义View
* 矩形尺寸:400dp × 510dp,背景色 #FFF4FAFF
* 镂空椭圆尺寸:400dp × 436dp,居中显示
*/
class HollowOvalInRectView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 基础尺寸配置(dp
private val rectWidthDp = 400f // 矩形宽度
private val rectHeightDp = 510f // 矩形高度
private val ovalWidthDp = 400f // 椭圆宽度
private val ovalHeightDp = 436f // 椭圆高度
// 转换后的像素尺寸
private val rectWidth: Float
private val rectHeight: Float
private var ovalWidth: Float
private var ovalHeight: Float
// 矩形背景画笔(颜色 #FFF4FAFF)
private val rectPaint = Paint().apply {
isAntiAlias = true
color = "#FFF4FAFF".toColorInt() // 矩形背景色
// color = "#FFFFFF".toColorInt() // 矩形背景色
style = Paint.Style.FILL
}
// 镂空椭圆画笔(核心:DST_OUT模式实现镂空)
private val hollowOvalPaint = Paint().apply {
isAntiAlias = true
color = Color.BLACK // 颜色不影响镂空效果,仅需非透明即可
style = Paint.Style.FILL
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
}
// 椭圆路径(复用避免重复创建)
private val ovalPath = Path()
init {
// dp转px,保证多设备尺寸一致
val density = resources.displayMetrics.density
rectWidth = rectWidthDp * density
rectHeight = rectHeightDp * density
ovalWidth = ovalWidthDp * density
ovalHeight = ovalHeightDp * density
// 关键配置:关闭硬件加速保证Xfermode生效
setLayerType(LAYER_TYPE_HARDWARE, null)
// 视图背景透明,避免干扰
setBackgroundColor(Color.TRANSPARENT)
}
/**
* 强制设置View尺寸为指定的矩形尺寸
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val targetWidth = MeasureSpec.makeMeasureSpec(rectWidth.toInt(), MeasureSpec.EXACTLY)
val targetHeight = MeasureSpec.makeMeasureSpec(rectHeight.toInt(), MeasureSpec.EXACTLY)
super.onMeasure(targetWidth, targetHeight)
}
/**
* 核心绘制逻辑:先画矩形,再画镂空椭圆
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 保存画布图层,确保Xfermode生效
val layerId = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)
// 1. 绘制底层矩形(背景色 #FFF4FAFF)
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), rectPaint)
// 2. 计算椭圆居中坐标(椭圆宽度和矩形宽度一致,仅垂直居中)
val ovalTop = (rectHeight - ovalHeight) / 2 // 椭圆顶部偏移
val ovalBottom = ovalTop + ovalHeight // 椭圆底部坐标
// 3. 绘制镂空椭圆(居中)
ovalPath.reset()
ovalPath.addOval(
0f, // 左:和矩形左对齐(宽度一致)
ovalTop, // 上:垂直居中
rectWidth, // 右:和矩形右对齐
ovalBottom, // 下
Path.Direction.CW
)
canvas.drawPath(ovalPath, hollowOvalPaint)
// 恢复画布状态
canvas.restoreToCount(layerId)
}
// 扩展方法:支持动态修改矩形背景色
fun updateRectBackgroundColor(colorHex: String) {
try {
rectPaint.color = Color.parseColor(colorHex)
invalidate() // 重绘视图
} catch (e: IllegalArgumentException) {
e.printStackTrace()
}
}
// 扩展方法:支持动态修改椭圆尺寸(可选)
fun updateOvalSize(widthDp: Float, heightDp: Float) {
val density = resources.displayMetrics.density
ovalWidth = widthDp * density
ovalHeight = heightDp * density
invalidate()
}
}
@@ -7,6 +7,7 @@ import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.network.ApiClient
import com.sw.dualscreen.repository.RemoteRepository
import com.sw.dualscreen.utils.Debouncer
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -24,15 +25,17 @@ abstract class BaseViewModel() : ViewModel() {
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
Debouncer(1000).debounce {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
}
@@ -41,36 +44,40 @@ abstract class BaseViewModel() : ViewModel() {
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
Debouncer(1000).debounce {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()
|| response.code == 200
|| response.code == 0
) {
val code = response.code
//|| code == "200" || code == "0"
if (code == "00000" ) {
return true
}
Timber.d("msg = ${response.message}, code = ${response.code}")
ToastUtils.showToast("${response.message}(${response.code})")
val message = response.msg
Timber.d("msg = ${message}, code = $code")
ToastUtils.showToast("${message}(${code})")
return false
}
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
GlobalData.appBaseUrl="http://192.168.1.210/gateway/local"
GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
GlobalData.appId = equipmentInfo.arcsoftAppId!!
// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
GlobalData.restId = equipmentInfo.canteenId!!
}
// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
// //GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
// GlobalData.appBaseUrl = "http://192.168.1.201:14801"
// //GlobalData.appBaseUrl="http://192.168.1.210/gateway/local"
// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
// GlobalData.appId = equipmentInfo.arcsoftAppId!!
//// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
// GlobalData.restId = equipmentInfo.canteenId!!
// }
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
@@ -1,67 +1,61 @@
package com.sw.dualscreen.viewmodel
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.SPUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
/**
* 初始化的viewmodel
*/
class DeviceViewModel : BaseViewModel() {
private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
// private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
// val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
/**
* 获取token
*/
fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
launchWithLoading {
val response = repository.getDeviceToken(deviceId)
if (parseResponse(response)) {
val response1 = repository.getDeviceInfo(deviceId, response.result!!)
if (parseResponse(response1)) {
val equipmentInfo = response1.result
if (equipmentInfo == null) return@launchWithLoading
try {
parseEquipmentInfo(equipmentInfo)
SPUtil.getInstance()
.put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
_deviceInfoResult.value = true
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
}
// fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
// launchWithLoading {
// val response = repository.getDeviceToken(deviceId)
// if (parseResponse(response)) {
// val response1 = repository.getDeviceInfo(deviceId, response.result!!)
//
// if (parseResponse(response1)) {
// val equipmentInfo = response1.result
// if (equipmentInfo == null) return@launchWithLoading
// try {
// parseEquipmentInfo(equipmentInfo)
// SPUtil.getInstance()
// .put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
// _deviceInfoResult.value = true
// } catch (e: Exception) {
// Timber.e(e)
// }
// }
// }
// }
// }
/**
* 检查缓存数据
*/
fun checkEquipmentInfo(): Boolean {
val equipmentInfoStr = SPUtil.getInstance().get(GlobalKey.KEY_EQUIPMENT_INFO, "")
if (equipmentInfoStr == null) {
Timber.e("获取缓存设备信息失败")
return false
}
val equipmentInfo =
GsonUtils.fromJson<EquipmentInfo>(equipmentInfoStr, EquipmentInfo::class.java)
if (equipmentInfo == null) {
Timber.e("解析缓存设备信息失败")
return false
}
try {
parseEquipmentInfo(equipmentInfo)
return true
} catch (e: Exception) {
Timber.e(e)
return false
}
}
// fun checkEquipmentInfo(): Boolean {
// val equipmentInfoStr = SPUtil.getInstance().get(GlobalKey.KEY_EQUIPMENT_INFO, "")
// if (equipmentInfoStr == null) {
// Timber.e("获取缓存设备信息失败")
// return false
// }
// val equipmentInfo =
// GsonUtils.fromJson<EquipmentInfo>(equipmentInfoStr, EquipmentInfo::class.java)
// if (equipmentInfo == null) {
// Timber.e("解析缓存设备信息失败")
// return false
// }
// try {
// parseEquipmentInfo(equipmentInfo)
// return true
// } catch (e: Exception) {
// Timber.e(e)
// return false
// }
// }
}
@@ -1,17 +1,23 @@
package com.sw.dualscreen.viewmodel
import android.content.Context
import android.net.Uri
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.DinnerTypeInfo
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.UserFaceModel2
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
@@ -22,30 +28,35 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.RequestBody
import timber.log.Timber
import kotlin.math.atan
import java.io.File
class UserViewModel : BaseViewModel() {
companion object {
private const val TAG = "UserViewModel"
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
// 识别出的列表
private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList
// private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
// val identifiedFoodInfoList: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList
// 搜索出的食物列表
private val _searchFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
val searchFoodInfoList: StateFlow<List<FoodInfo>> = _searchFoodInfoList
// private val _searchFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
// val searchFoodInfoList: StateFlow<List<FoodInfo>> = _searchFoodInfoList
// 就餐数据
private val _nutritionData = MutableStateFlow<UserNutritionData?>(null)
val nutritionData: StateFlow<UserNutritionData?> = _nutritionData
// private val _nutritionData = MutableStateFlow<UserNutritionData?>(null)
// val nutritionData: StateFlow<UserNutritionData?> = _nutritionData
/**
* 饭点类型 早餐/午餐/晚餐
*/
private val _dinnerTypeInfo = MutableStateFlow<DinnerTypeInfo?>(null)
val dinnerTypeInfo: StateFlow<DinnerTypeInfo?> = _dinnerTypeInfo
private val _dinnerTypeInfo = MutableStateFlow<DinnerType?>(null)
val dinnerTypeInfo: StateFlow<DinnerType?> = _dinnerTypeInfo
/**
* 人脸加载完成
@@ -55,48 +66,168 @@ class UserViewModel : BaseViewModel() {
private val _identifiedFoodInfoList2 = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList2: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList2
private var lastFaceTimestamp = 0L
/**
* 获取token
*/
fun getEquipmentToken(qrcodeId: String = GlobalData.deviceId) {
Timber.d("getEquipmentToken")
launchWithLoading {
val response = repository.getEquipmentToken(qrcodeId)
if (parseResponse(response)) {
// 缓存token
SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.result)
// 首次运行获取人脸数据
if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
getUserFaceCache(index = 0)
}
}
}
// Timber.tag(TAG).d("getEquipmentToken")
// launchWithLoading {
// val response = repository.getEquipmentToken(qrcodeId)
// if (parseResponse(response)) {
// // 缓存token
// SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.result)
// // 首次运行获取人脸数据
// if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
// getUserFaceCache(pageNo = 1)
// }
// }
// }
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(index: Int = 0) {
Timber.d("getUserFaceCache index = $index")
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) {
var currentPageNo = pageNo
Timber.tag(TAG).d("getUserFaceCache index = $currentPageNo")
launch {
_loadFaceResult.value = false
val response = repository.getUserFaceCache(index)
//获取全量数据时,时间戳改为0
SpTool.setLastFaceTimestamp(0)
val response = repository.getUserFaceCache(currentPageNo)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.result?.data ?: emptyList()
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureString))
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
ToastUtils.showToast("查询人脸数据为空")
return@withContext
}
faceApi.updateFaceData(index, faceEntity)
}
val nextPageIndex = response.result?.nextPageIndex ?: -1
if (nextPageIndex > 0) {
getUserFaceCache(nextPageIndex)
} else {
// val item = list.firstOrNull { it.userId == "1951105919342936066" }
// Timber.tag(TAG).d("getUserFaceCache userId = ${item?.userId}")
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr))
}
faceApi.updateFaceData(currentPageNo, faceEntity)
if (list.size >= pageSize) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
currentPageNo++
getUserFaceCache(currentPageNo)
return@withContext
}
//查询完成所有数据,保存时间戳
if (list.isNotEmpty()) {
//非空最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
_loadFaceResult.value = true
}
// val nextPageIndex = response.result?.nextPageIndex ?: -1
// if (nextPageIndex > 0) {
// getUserFaceCache(nextPageIndex)
// } else {
// _loadFaceResult.value = true
// }
}
}
}
// fun getUserFaceCache2(index: Int = 0) {
// Timber.d("getUserFaceCache index = $index")
// launch {
// _loadFaceResult.value = false
// val response = repository.getUserFaceCache2(index)
// if (parseResponse(response)) {
// // 获取成功一次后缓存状态
// SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
// withContext(Dispatchers.Default) {
// val list: List<UserFaceModel2> = response.result?.records ?: emptyList()
// val faceEntity = list.map {
// FaceEntity(it.userId, null, Base64.decode(it.face))
// }
// faceApi.updateFaceData2(index, faceEntity)
// }
// val nextPageIndex = response.result?.nextPageIndex ?: -1
// if (nextPageIndex > 0) {
// getUserFaceCache2(nextPageIndex)
// } else {
// _loadFaceResult.value = true
// }
// }
// }
// }
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
onAllQueryFinished: () -> Unit,
onPageQueryFinished: (List<UserFaceModel>) -> Unit
) {
Timber.tag(TAG).d("getFaceIncrementList")
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
return@withContext
}
onPageQueryFinished(list)
// if (list.size >= pageSize) {
// lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
// getFaceIncrementList(
// pageNo = pageNo + 1,
// pageSize = pageSize,
// timestamp = timestamp,
// onAllQueryFinished = onAllQueryFinished,
// onPageQueryFinished = onPageQueryFinished
// )
// return@withContext
// }
//查询完成所有数据,保存时间戳
if (list.isNotEmpty()) {
//非空实现最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
onAllQueryFinished()
}
}
}
}
fun getCollectedFoodList(
pageNo: Int = 1,
pageSize: Int = 100,
foodName: String,
block: (List<CollectedFoodInfo>) -> Unit
) {
Timber.tag(TAG).d("getCollectedFoodList index = $pageNo")
launch {
val response = repository.getCollectedFoodList(
pageNum = pageNo,
pageSize = pageSize,
foodName = foodName
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<CollectedFoodInfo> = response.data ?: emptyList()
block(list)
}
} else {
block(emptyList())
}
}
}
@@ -105,7 +236,7 @@ class UserViewModel : BaseViewModel() {
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
Timber.tag(TAG).d("activeEngine")
faceApi.activeEngine(
App.getContext(),
@@ -114,7 +245,7 @@ class UserViewModel : BaseViewModel() {
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
Timber.tag(TAG).d("activeEngine activeCode = $activeCode")
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
@@ -140,60 +271,98 @@ class UserViewModel : BaseViewModel() {
})
}
fun getIdentifiedFoodList() {
Timber.d("identifiedFoodList")
launchWithLoading {
val response = repository.getRestInfoFoodsByType(foodName = "")
if (parseResponse(response)) {
_identifiedFoodInfoList.value = response.result ?: emptyList()
}
}
}
// fun getIdentifiedFoodList() {
// Timber.tag(TAG).d("identifiedFoodList")
// launchWithLoading {
// val response = repository.getRestInfoFoodsByType(foodName = "")
// if (parseResponse(response)) {
// _identifiedFoodInfoList.value = response.result ?: emptyList()
// }
// }
// }
fun cleanIdentifiedFoodInfoList() {
Timber.d("cleanIdentifiedFoodInfoList")
_identifiedFoodInfoList.value = emptyList<FoodInfo>()
}
// fun cleanIdentifiedFoodInfoList() {
// Timber.tag(TAG).d("cleanIdentifiedFoodInfoList")
// _identifiedFoodInfoList.value = emptyList<FoodInfo>()
// }
/**
* 搜索食物
*/
fun searchByFoodName(foodName: String) {
Timber.d("searchByFoodName foodName = $foodName")
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
Timber.tag(TAG).d("searchByFoodName foodName = $foodName")
launchWithLoading {
val response = repository.getRestInfoFoodsByType(foodName = foodName)
if (parseResponse(response)) {
_searchFoodInfoList.value = response.result ?: emptyList()
val list = response.data ?: emptyList()
action(list)
// _identifiedFoodInfoList.value = response.data ?: emptyList()
}
}
}
fun cleanSearchFoodInfoList() {
Timber.d("cleanSearchFoodInfoList")
_searchFoodInfoList.value = emptyList<FoodInfo>()
}
// fun cleanSearchFoodInfoList() {
// Timber.tag(TAG).d("cleanSearchFoodInfoList")
// _searchFoodInfoList.value = emptyList<FoodInfo>()
// }
// /**
// * 获取用户就餐数据
// */
// fun getUserNutritionData(userId: String, foodId: String) {
// Timber.tag(TAG).d("getUserNutritionData userId = ${userId}, foodId = $foodId")
// _nutritionData.value = null
// launch {
// val response = repository.getUserNutritionData(userId = userId, foodId = foodId)
// if (parseResponse(response)) {
// _nutritionData.value = response.result
// }
// }
// }
/**
* 获取用户就餐数据
*/
fun getUserNutritionData(userId: String, foodId: String) {
Timber.d("getUserNutritionData userId = ${userId}, foodId = $foodId")
_nutritionData.value = null
fun getUserNutritionData(userId: String, block: (UserNutrition?) -> Unit) {
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
launch {
val response = repository.getUserNutritionData(userId = userId, foodId = foodId)
val response = repository.getUserNutritionData(userId = userId)
if (parseResponse(response)) {
_nutritionData.value = response.result
block(response.data)
} else {
block(null)
}
}
}
/**
* 获取支付二维码
*/
fun getQrCodeImg(
orderId: String,
userId: String? = null,
totalFee: String? = null,
block: (String?) -> Unit
) {
Timber.tag(TAG).d("getQrCodeImg orderId = $orderId, userId = $userId")
launch {
val response =
repository.getQrCodeImg(orderNo = orderId, memberId = userId, totalFee = totalFee)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
fun getDinnerType() {
Timber.d("getDinnerType")
Timber.tag(TAG).d("getDinnerType")
_dinnerTypeInfo.value = null
launch {
val response = repository.getDinnerType()
if (parseResponse(response)) {
_dinnerTypeInfo.value = response.result
_dinnerTypeInfo.value = response.data
}
}
}
@@ -202,7 +371,7 @@ class UserViewModel : BaseViewModel() {
* 提交就餐数据
*/
fun postUserNutritionData(param: List<UserNutritionParam>) {
Timber.d("postUserNutritionData param = $param")
Timber.tag(TAG).d("postUserNutritionData param = $param")
launch {
val response = repository.postUserNutritionData(param)
if (parseResponse(response)) {
@@ -211,33 +380,217 @@ class UserViewModel : BaseViewModel() {
}
}
fun getFoodInfo(foodName: String, action:(List<FoodInfo>)-> Unit = {}) {
Timber.d("getFoodInfo")
fun getFoodInfo(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
Timber.tag(TAG).d("getFoodInfo")
launchWithLoading {
val response = repository.getFoodInfo(foodName = foodName)
if (parseResponse(response)) {
val list = response.result ?: emptyList()
_identifiedFoodInfoList2.value = list
val list = response.data ?: emptyList()
// _identifiedFoodInfoList.value = list
action(list)
} else {
action(emptyList())
}
}
}
fun postImageData(
context: Context,
foodId: String,
foodName: String,
foodVector: String,
uri: Uri
) {
Timber.d("postImageData")
fun createOrder(order: FoodOrder, block: (String) -> Unit) {
Timber.tag(TAG).d("createOrder")
launchWithLoading {
val response = repository.postImageData(
context, foodId = foodId, foodName = foodName, foodVector = foodVector,
uri = uri
)
val response = repository.createOrder(order)
if (parseResponse(response)) {
Timber.tag("mzf").d("code=${response.code}")
block(response.data.toString())
} else {
block("")
// // TODO: 临时测试----------------
// block("${System.currentTimeMillis()}")
// // TODO: 临时测试----------------
}
}
}
/**
* 现金支付
*/
fun cashPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("cashPay")
launchWithLoading {
val response = repository.cashPay(param)
if (parseResponse(response)) {
block(response.data ?: false)
} else {
block(false)
}
}
}
// /**
// * 查询订单状态
// *
// * @param queryType 查询类型 1支付2退款
// * @param orderId 订单号
// */
// fun queryOrderState(queryType:Int, orderId:String, block:(PayResult?)-> Unit) {
// Timber.tag(TAG).d("queryOrderState")
// val param = hashMapOf(
// "queryType" to "$queryType",
// "payOrderNo" to orderId
// )
// launchWithLoading {
// val response = repository.queryOrderState(param)
// if (parseResponse(response)) {
// block(response.data)
// } else {
// block(null)
// }
// }
// }
/**
* 扫码成功回调
*
* @param orderId 订单号
*/
suspend fun queryOrderState(orderId: String, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("queryOrderState")
val response = repository.queryOrderState(orderNo = orderId)
if (parseResponse(response)) {
//val respData = response.data as? String
// ?: if (response.data is Any) {
// GsonUtils.toJson(response.data)
// } else {
// ""
// }
//al payResult = GsonUtils.fromJson(respData, PayResult::class.java)
//val orderState = payResult?.paySuc == "1"
//data=1的话 是支付成功 0是待支付
block(response.data == "1")
} else {
block(false)
}
}
/**
* 会员支付
*/
fun memberPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("memberPay")
launchWithLoading {
val response = repository.memberPay(param)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
/**
* 绑定订单
*/
fun bindOrder(userId: String, orderId: String, mode: Int, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("bindOrder")
launchWithLoading {
val response = repository.bindOrder(userId = userId, orderId = orderId, mode = mode)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfo")
launchWithLoading {
val response = repository.getMemberInfoById(memberId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfo")
launchWithLoading {
val response = repository.getMemberInfoByPhone(phone, key)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
// fun uploadCollectFoodPics(files: List<File?>, params: HashMap<String, RequestBody>, callback: (Boolean) -> Unit) {
// Timber.tag(TAG).d("uploadCollectFoodPics")
// launchWithLoading {
// val fileList = files.filterNotNull()
// if (fileList.isEmpty()) {
// callback(false)
// return@launchWithLoading
// }
// val resp = repository.uploadCollectFoodPics(fileList, params)
// if (!parseResponse(resp)) {
// callback(false)
// return@launchWithLoading
// }
// callback(resp.code == "00000")
// }
// }
suspend fun uploadCollectFoodPics(
files: List<File?>,
params: HashMap<String, RequestBody>
): List<String>? {
Timber.tag(TAG).d("uploadCollectFoodPics")
val fileList = files.filterNotNull()
if (fileList.isEmpty()) {
return null
}
val resp = repository.uploadCollectFoodPics(fileList, params)
if (resp.code != "00000") {
return null
}
return resp.data
}
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
Timber.tag(TAG).d("getDeviceConfig")
launchWithLoading {
val response = repository.getDeviceConfig()
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
fun getCollectedFoodVector(param: MutableMap<String, String>, block: (List<FoodVector>?) -> Unit) {
Timber.tag(TAG).d("getCollectedFoodVector")
launchWithLoading {
val response = repository.getCollectedFoodVector(param)
if (parseResponse(response)) {
block(response.data?:emptyList())
} else {
block(null)
}
}
}
fun deleteCollectFood(foodId:String?, version: String?, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("deleteCollectFood")
launchWithLoading {
val response = repository.deleteCollectFood(foodId, version)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FF0A1428" android:state_pressed="false"/>
<item android:color="#FFFF3232" android:state_pressed="true"/>
</selector>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FF5E7585" android:state_checked="false"/>
<item android:color="#FFFF3232" android:state_checked="true"/>
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Some files were not shown because too many files have changed in this diff Show More