Author SHA1 Message Date
lvmeng a6d2441704 优化:修复眼睛图标显示效果 2026-03-18 16:14:47 +08:00
lvmeng e7d11530a6 优化:修改消费码输入框为密码模式,添加显示/隐藏密码切换功能 2026-03-18 15:55:06 +08:00
lvmeng c56769769b 优化:修改消费码输入框为密��模式,添加显示/隐藏密码切换功能 2026-03-18 15:42:50 +08:00
lvmeng 8673cec545 优化:修改人脸识别任务延迟参数和添加相机权限检查 2026-03-18 14:06:42 +08:00
lvmeng 8ec7ea6a79 优化 2026-03-13 15:42:37 +08:00
lvmeng a80e3d1ba6 ObjectBox数据库操作增加线程资源清理 2026-03-12 17:25:36 +08:00
mazengfei 6d825d7235 扫码支付增加会员id字段 2026-03-12 12:39:35 +08:00
lvmeng 4f1860eb37 存在待支付订单时停止识别 2026-03-12 11:43:20 +08:00
mazengfei 45a7bf8369 优化非支付页面扫码点击问题 2026-03-11 18:34:49 +08:00
lvmengandClaude Sonnet 4.6 f0b50ef82f 修复MainActivity中扫码枪按键事件触发页面按钮点击问题
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 14:54:52 +08:00
lvmeng 824dc5f867 优化扫码支付结果会员非会员显示 2026-03-11 13:55:29 +08:00
lvmengandClaude Sonnet 4.6 7232735073 修复MainActivity停止时CameraX NPE崩溃
1. 升级CameraX版本 1.3.0 → 1.4.1,从根本修复RequestWithCallback.abort() NPE
2. 跳转PayActivity前调用shutdownCamera(),防止in-flight拍照请求触发生命周期异常
3. onResume()顶部增加相机恢复逻辑,通过cameraProviderFuture==null判断是否需要重新初始化
4. shutdownCamera()中将cameraProviderFuture置null,作为相机已停止的可靠标志

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:05:28 +08:00
mazengfei 8a1630f2a4 优化扫码枪支付 2026-03-10 14:44:34 +08:00
lvmeng 39c0ca7d72 优化人脸识别区域效果 2026-03-10 11:10:23 +08:00
lvmeng dfde8c0fad 人脸全量接口只首次请求 2026-03-10 10:53:18 +08:00
lvmeng aca84f1e94 Rename .java to .kt 2026-03-10 10:53:18 +08:00
35 changed files with 687 additions and 273 deletions
Generated
+1
View File
@@ -0,0 +1 @@
DualScreen
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-06T02:13:05.230327500Z">
<DropdownSelection timestamp="2026-03-10T03:02:12.156794100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.170:5555;connection=1ec19d1e" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.211:5555;connection=7435869e" />
</handle>
</Target>
</DropdownSelection>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="userId" value="29face46:19ce5e8d9d0:-7fe0" />
</MTProjectMetadataState>
</option>
</component>
</project>
+4
View File
@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
+21
View File
@@ -0,0 +1,21 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.sw.dualscreen",
"variantName": "debug",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "档口双屏_1.0-debug.apk"
}
],
"elementType": "File",
"minSdkVersionForDexing": 29
}
Binary file not shown.
+77
View File
@@ -0,0 +1,77 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "11:7949771231314213889",
"name": "Food",
"properties": [
{
"id": "1:2317727855243736226",
"name": "id",
"type": 6,
"flags": 1
},
{
"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": 5
}
],
"relations": []
}
],
"lastEntityId": "1:594331511073099531",
"lastIndexId": "1:6010838397086628487",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
340951913928211738,
5484846939472684412
],
"retiredRelationUids": [],
"version": 1
}
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.sw.dualscreen",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "档口双屏_1.0-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/档口双屏_1.0-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/档口双屏_1.0-release.dm"
]
}
],
"minSdkVersionForDexing": 29
}
@@ -59,7 +59,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
override fun onDestroy() {
keyEventHelper?.onDestroy()
keyEventHelper?.release()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
@@ -153,7 +153,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
*/
fun registerKeyEvent() {
keyEventHelper =
ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
ScanGunKeyEventHelper( object : ScanGunKeyEventHelper.OnScanSuccessListener {
override fun onScanSuccess(barcode: String?) {
Timber.d("onScanSuccess barcode = $barcode")
if (barcode == null) return
@@ -166,10 +166,13 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
/**
* 处理扫描枪数据
*/
open fun scanGunKeyEventCallback(scanInfo: String) {
Timber.tag("TAG").d("handleScanKeyInfo: $scanInfo")
fun scanGunKeyEventCallback(scanInfo: String){
if (this is PayActivity) {
qrCodePay(scanInfo)
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) {
@@ -48,6 +48,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
override fun initialize() {
super.initialize()
registerKeyEvent()
binding.root.setOnClickListener { it.hideKeyboard() }
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
@@ -1,8 +1,12 @@
package com.sw.dualscreen.activity
import android.Manifest
import android.content.Intent
import android.os.CountDownTimer
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding
@@ -27,6 +31,27 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
private val userViewModel by viewModels<UserViewModel>()
private var pageNum = 1
// 相机权限请求回调
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 权限请求成功,打开 MainActivity
goMainActivity()
} else {
// 权限请求失败,显示弹窗
showPermissionDeniedDialog()
}
}
// 设置页面返回的回调
private val startSettingsLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// 从设置页面返回,重新检查权限
checkCameraPermissionAfterSettings()
}
override fun getViewModel(): BaseViewModel {
return userViewModel
}
@@ -85,6 +110,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun initialize() {
super.initialize()
registerKeyEvent()
initNetworkTimer()
binding.okButton.clickWithDebounce(500) {
checkNetworkTimer?.cancel()
@@ -127,7 +153,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
runOnUiThread {
hideWaitingDialog()
binding.root.postDelayed({
goMainActivity()
checkCameraPermissionAndGo()
}, 1000)
}
}
@@ -141,6 +167,53 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
finish()
}
// 检查相机权限
private fun checkCameraPermissionAndGo() {
val hasCameraPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (hasCameraPermission) {
// 有权限,直接打开 MainActivity
goMainActivity()
} else {
// 无权限,请求相机权限
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
// 从设置页面返回后检查权限
private fun checkCameraPermissionAfterSettings() {
val hasCameraPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (hasCameraPermission) {
// 权限已授予,打开 MainActivity
goMainActivity()
} else {
// 权限仍未授予,再次显示弹窗
showPermissionDeniedDialog()
}
}
// 显示权限被拒绝的弹窗
private fun showPermissionDeniedDialog() {
RemindDialog(
context = context,
content = "暂无相机权限,请授权后重试",
confirmBlock = {
// 打开 App 设置页面
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = android.net.Uri.fromParts("package", packageName, null)
}
startSettingsLauncher.launch(intent)
}
).show()
}
private var checkNetworkTimer: CountDownTimer? = null
private fun initNetworkTimer() {
@@ -151,12 +151,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() {
super.initialize()
isPageVisible = true
// 拦截扫码枪按键事件,防止触发页面按钮点击
registerKeyEvent()
addBackEventListener()
initView()
viewModel.getUserFaceCache(pageNo = 1)
if (SpTool.firstGetFace) {
viewModel.getUserFaceCache(pageNo = 1)
}
setupCamera()
initData()
registerKeyEvent()
}
private fun initData() {
@@ -456,6 +459,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// 3. 关闭相关资源
imageCapture = null
imageAnalysis = null
cameraProviderFuture = null
// 4. 停止副屏更新
isAnalyzing = false
@@ -554,6 +558,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onResume() {
super.onResume()
isPageVisible = true
// shutdownCamera 后 cameraProviderFuture 置为 null,需重新初始化相机
if (cameraProviderFuture == null) {
setupCamera()
}
// 启动定时器
handler.postDelayed(timeoutRunnable, TIME_OUT)
// ToastUtils.showToast("isPageVisible=$isPageVisible")
@@ -796,7 +804,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
userId = null,
) {
isRefreshPage = false
//presentation?.dismiss()
shutdownCamera()
startActivity(Intent(this, PayActivity::class.java).apply {
putExtra(PayActivity.FOOD_INFO, checkedItem!!)
putExtra(PayActivity.FOOD_EAT_NUM, eatNum)
@@ -915,16 +923,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 30 * 1000L
private val dealyMillis = 3 * 60 * 1000L
private val initialDelay = 10 * 1000L
private val dealyMillis = 5 * 1000L
// private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return@startIntervalTaskWithInitialDelay
}
val timestamp = SpTool.lastFaceTimestamp
//if (timestamp == 0L) {
// return@startIntervalTaskWithInitialDelay
//}
viewModel.getFaceIncrementList(
// pageNo = taskPageNo,
timestamp = timestamp,
@@ -1,5 +1,6 @@
package com.sw.dualscreen.activity
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import com.sw.dualscreen.GlobalKey
@@ -15,6 +16,7 @@ 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.PayPresentation
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SoundPoolUtil
import com.sw.dualscreen.viewmodel.BaseViewModel
@@ -63,6 +65,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
var foodOrderId: String = ""
var eatNum: Int = 1
var isMember: Boolean = false
val presentation by lazy {
PayPresentation(
@@ -81,7 +84,8 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
override fun initialize() {
super.initialize()
presentation.show()
registerKeyEvent()
addBackEventListener()
val soundMap = hashMapOf<String, Int>()
soundMap[PAY_SUCCESS] = R.raw.pay_success
SoundPoolUtil.getInstance().loadR(this, soundMap)
@@ -95,12 +99,14 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
binding.include.ivPageBack.setOnClickListener { finish() }
binding.include.tvPageTitle.text = "下单结算"
binding.btnPayQrCode.setOnClickListener {
isMember = false
if (binding.btnPayQrCode.isChecked) {
return@setOnClickListener
}
showScanQrCodePay()
}
binding.btnPayCash.setOnClickListener {
isMember = false
if (binding.btnPayCash.isChecked) {
return@setOnClickListener
}
@@ -156,7 +162,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
}
override fun onDestroy() {
presentation?.dismiss()
presentation.dismiss()
super.onDestroy()
}
@@ -171,14 +177,14 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
// display = displays[1],
// type = type
// ) {
// scanQrCodePayPresentation?.dismiss()
// 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()
// scanQrCodePaypresentation.show()
// }
presentation.let {
it.pageType = PayPresentation.QR_CODE_PAY
@@ -192,15 +198,19 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
}
fun updateQrCodeImage(qrCodeUrl: String?) {
// scanQrCodePayPresentation?.loadQrCodeImage(qrCodeUrl)
presentation?.loadQrCodeImage(qrCodeUrl)
// scanQrCodePaypresentation.loadQrCodeImage(qrCodeUrl)
presentation.loadQrCodeImage(qrCodeUrl)
}
private var debouncer = Debouncer(5000)
fun showPaySuccess(isVip: Boolean) {
playSoundOnPaySuccess()
EventBus.getDefault().post(PaySuccessEvent())
showPayInfo(type = 2, isVip = isVip, memberInfo = memberInfo)
hidePayTab()
debouncer.debounce {
playSoundOnPaySuccess()
EventBus.getDefault().post(PaySuccessEvent())
showPayInfo(type = 2, isVip = isVip, memberInfo = memberInfo)
hidePayTab()
}
}
fun getQrCodeImg(
@@ -232,7 +242,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
}
}
fun memberPay(param: HashMap<String, String>, block: () -> Unit) {
fun memberPay(param: HashMap<String, String?>, block: () -> Unit) {
userViewModel.memberPay(param) {
runOnUiThread {
if (it.not()) {
@@ -286,12 +296,13 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
}
}
override fun scanGunKeyEventCallback(scanInfo: String) {
super.scanGunKeyEventCallback(scanInfo)
userViewModel.qrCodePay(scanInfo, foodOrderId) { payState, backData ->
fun qrCodePay(scanInfo: String) {
showWaitingDialog("支付中……")
userViewModel.qrCodePay(scanInfo, foodOrderId, memberInfo?.id) { payState, backData ->
Timber.d("qrCodePay支付返回数据:state=$payState,backData=$backData")
hideWaitingDialog()
if (payState) {
showPaySuccess(false)
showPaySuccess(isMember)
}
}
@@ -340,4 +351,12 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
// }
// }
// }
fun addBackEventListener() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
}
}
@@ -32,6 +32,7 @@ class SettingActivity : BaseActivity<ActivitySettingBinding>() {
override fun initialize() {
super.initialize()
registerKeyEvent()
initBackDispatcher()
binding.include.let {
it.ivPageBack.setOnClickListener {
@@ -1,5 +1,7 @@
package com.sw.dualscreen.activity.fragment.pay
import android.text.InputType
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentNumberPayBinding
@@ -13,6 +15,8 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
// 密码是否显示的标志
private var isPasswordVisible = false
override fun inflateViewBinding(): FragmentNumberPayBinding {
return FragmentNumberPayBinding.inflate(layoutInflater)
}
@@ -27,6 +31,12 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
foodName = it.foodName
payAmount = (it.specPrice ?: 0.0) * payActivity.eatNum
}
// 密码显示/隐藏切换
binding.ivTogglePassword.setOnClickListener {
togglePasswordVisibility()
}
binding.btnConfirm.setOnClickListener { v ->
val phone = binding.etInputPhone.text.toString().trim()
val key = binding.etInputNumber.text.toString().trim()
@@ -41,7 +51,7 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
// ToastUtils.showToast("会员验证成功")
// }
// }
payActivity.isMember = false
payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
if (memberInfo == null) {
binding.root.postDelayed({
@@ -52,6 +62,7 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
}
//肯定是会员
memberInfo.member = true
payActivity.isMember = true
//绑定订单使用faceUserId
payActivity.bindOrder(memberInfo.faceUserId ?: "") { bindResult ->
if (bindResult.not()) {
@@ -126,6 +137,30 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
}
}
// 切换密码显示/隐藏
private fun togglePasswordVisibility() {
val editText = binding.etInputNumber
val imageView = binding.ivTogglePassword
// 保存当前光标位置
val cursorPosition = editText.selectionStart
if (isPasswordVisible) {
// 当前显示,改为隐藏
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
imageView.setImageResource(R.drawable.ic_eye_close)
isPasswordVisible = false
} else {
// 当前隐藏,改为显示
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
imageView.setImageResource(R.drawable.ic_eye_open)
isPasswordVisible = true
}
// 恢复光标位置
editText.setSelection(cursorPosition)
}
// private fun bindOrder(userId: String, block: () -> Unit) {
// payActivity.bindOrder(userId) { bindResult ->
// if (bindResult.not()) {
@@ -289,7 +289,7 @@ class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
//订单号
"orderNo" to payActivity.foodOrderId,
//用户id
"memberId" to memberInfo!!.id!!
"memberId" to memberInfo?.id
),
block = callback
)
@@ -202,7 +202,7 @@ interface ApiService {
@POST
suspend fun memberPay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/memberPay",
@Body param: HashMap<String, String>
@Body param: HashMap<String, String?>
): ApiResponse<Any?>
/**
@@ -220,12 +220,10 @@ interface ApiService {
* 二维码支付
* @param orderType 0付款 1会员充值
*/
@GET
@POST
suspend fun qrCodePay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/qrCodePay",
@Query("authCode") authCode: String,
@Query("outTradeNo") orderNo: String,
@Query("orderType") orderType:Int
@Body param: HashMap<String, String?>
): ApiResponse<String?>
/**
@@ -143,6 +143,9 @@ object ObjectBox {
} catch (e: Exception) {
e.printStackTrace()
null
} finally {
// 线程资源清理
boxStore?.closeThreadResources()
}
}
}
@@ -177,16 +180,13 @@ object ObjectBox {
getBox<Food>()?.all
} ?: emptyList()
suspend fun filter(name: String?, isDistinct: Boolean = false) = safeDbOp {
val list = if (isDistinct) {
getBox<Food>()?.all?.distinctBy { it.foodName }
suspend fun filter(name: String?) = safeDbOp {
if (!name.isNullOrBlank()) {
getBox<Food>()?.query(Food_.foodName.contains(name))?.build()?.find()
?.distinctBy { it.foodName }
} else {
getBox<Food>()?.all
}
if (name.isNullOrBlank().not()) {
list?.filter { it.foodName?.contains(name) == true }
}
list
getBox<Food>()?.all?.distinctBy { it.foodName }
} ?: emptyList()
} ?: emptyList()
suspend fun removeAll() = safeDbOp {
@@ -194,10 +194,7 @@ object ObjectBox {
}
suspend fun remove(name: String) = safeDbOp {
getBox<Food>()?.run {
val filterIdList = all.filter { it.foodName == name }.map { it.id }
removeByIds(filterIdList)
}
getBox<Food>()?.query(Food_.foodName.equal(name))?.build()?.remove()
}
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
@@ -207,9 +204,7 @@ object ObjectBox {
// 检查存储是否可读写(操作数据库前调用)
fun isStorageAvailable(context: Context): Boolean {
return try {
val state = Environment.getExternalStorageState()
val innerDir = context.filesDir
Environment.MEDIA_MOUNTED == state && innerDir.canRead() && innerDir.canWrite()
context.filesDir.canRead() && context.filesDir.canWrite()
} catch (e: Exception) {
false
}
@@ -2,7 +2,6 @@ package com.sw.dualscreen.presentation.pay
import android.Manifest
import android.annotation.SuppressLint
import android.app.Dialog
import android.app.Presentation
import android.content.pm.PackageManager
import android.graphics.Point
@@ -11,24 +10,20 @@ import android.os.Build
import android.os.Bundle
import android.text.Spanned
import android.text.SpannedString
import android.text.TextUtils
import android.text.style.AbsoluteSizeSpan
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.core.text.buildSpannedString
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.databinding.PresentationPayBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.format2String
@@ -38,9 +33,7 @@ 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.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
@@ -58,6 +51,7 @@ 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
import java.util.concurrent.atomic.AtomicBoolean
class PayPresentation(
val activity: PayActivity,
@@ -299,13 +293,16 @@ class PayPresentation(
.i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}")
// recognitionTime = System.currentTimeMillis()
// recognitionWeight = lastWeight
if (isExistNotPayOrder.get()) return@Observer
lastFaceTrackId = compareResult.trackId
val faceEntity = compareResult.faceEntity
val userId = faceEntity.userName
val isMember = faceEntity.userType == "1"
if (userId == null) return@Observer
activity.isMember = isMember
if (userId.isNullOrBlank()) return@Observer
// TODO: 测试支付用户id
// userId = "1987710988425662466"
isExistNotPayOrder.set(true)
faceRecSuccess(userId, isMember)
})
@@ -314,6 +311,7 @@ class PayPresentation(
})
}
private var isExistNotPayOrder = AtomicBoolean(false)
private fun initArcView() {
//在布局结束后才做初始化操作
@@ -605,6 +603,7 @@ class PayPresentation(
//ToastUtils.showToast("订单绑定失败")
return@bindOrder
}
binding.root.postDelayed({
activity.hideWaitingDialog()
// TODO: 测试 memberInfo.member是否返回正常数据---------------
@@ -76,7 +76,8 @@ class RemoteRepository constructor(
)
}
}
/**
/**
* 获取人脸数据
*/
suspend fun getFaceIncrementList(
@@ -134,16 +135,18 @@ class RemoteRepository constructor(
suspend fun getRestInfoFoodsByType(
foodName: String,
pageNum: Int = 1,
pageSize:Int = 50
pageSize: Int = 50
): ApiResponse<List<FoodInfo>> {
return safeApiCall {
apiService.getRestInfoFoodsByType(param = hashMapOf(
"name" to foodName,
"pageNum" to "$pageNum",
"pageSize" to "$pageSize",
//"订单来源权举:1-营养秤,2-档口
"deviceType" to "2"
))
apiService.getRestInfoFoodsByType(
param = hashMapOf(
"name" to foodName,
"pageNum" to "$pageNum",
"pageSize" to "$pageSize",
//"订单来源权举:1-营养秤,2-档口
"deviceType" to "2"
)
)
}
}
@@ -189,9 +192,20 @@ class RemoteRepository constructor(
/**
* 二维码支付
*/
suspend fun qrCodePay(authCode: String,orderNo: String): ApiResponse<String?> {
suspend fun qrCodePay(
authCode: String,
orderNo: String,
memberId: String?
): ApiResponse<String?> {
return safeApiCall {
apiService.qrCodePay(authCode = authCode, orderNo=orderNo, orderType = 0)
apiService.qrCodePay(
param = hashMapOf(
"authCode" to authCode,
"outTradeNo" to orderNo,
"memberId" to memberId,
"orderType" to "0"
)
)
}
}
@@ -256,7 +270,7 @@ class RemoteRepository constructor(
/**
* 会员支付
*/
suspend fun memberPay(param: HashMap<String, String>): ApiResponse<Any?> {
suspend fun memberPay(param: HashMap<String, String?>): ApiResponse<Any?> {
return safeApiCall {
apiService.memberPay(param = param)
}
@@ -285,10 +299,12 @@ class RemoteRepository constructor(
*/
suspend fun getMemberInfoByPhone(phone: String, key: String): ApiResponse<MemberInfo?> {
return safeApiCall {
apiService.getMemberInfoByPhone(param = hashMapOf(
"phone" to phone,
"password" to key
))
apiService.getMemberInfoByPhone(
param = hashMapOf(
"phone" to phone,
"password" to key
)
)
}
}
@@ -326,12 +342,14 @@ class RemoteRepository constructor(
}
}
suspend fun deleteCollectFood(foodId:String?, version: String?): ApiResponse<Any?> {
suspend fun deleteCollectFood(foodId: String?, version: String?): ApiResponse<Any?> {
return safeApiCall {
apiService.deleteCollectFood(param = mutableMapOf(
"foodId" to foodId,
"version" to version
))
apiService.deleteCollectFood(
param = mutableMapOf(
"foodId" to foodId,
"version" to version
)
)
}
}
}
@@ -1,19 +0,0 @@
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,30 @@
package com.sw.dualscreen.utils
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.utils.SPUtil.Companion.getInstance
object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null
init {
spUtil = getInstance(MyApp.instance!!, "default_sp")
}
var lastFaceTimestamp: Long
get() = spUtil?.get(LAST_FACE_TIMESTAMP, 0L)?:0L
set(timestamp) {
spUtil?.put(LAST_FACE_TIMESTAMP, timestamp)
}
var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true
set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value)
}
}
@@ -21,7 +21,7 @@ class HollowOvalInRectView @JvmOverloads constructor(
private val rectWidthDp = 400f // 矩形宽度
private val rectHeightDp = 510f // 矩形高度
private val ovalWidthDp = 400f // 椭圆宽度
private val ovalHeightDp = 436f // 椭圆高度
private val ovalHeightDp = 416f // 椭圆高度
// 转换后的像素尺寸
private val rectWidth: Float
@@ -96,7 +96,7 @@ class UserViewModel : BaseViewModel() {
launch {
_loadFaceResult.value = false
//获取全量数据时,时间戳改为0
SpTool.setLastFaceTimestamp(0)
SpTool.lastFaceTimestamp = 0
val response = repository.getUserFaceCache(currentPageNo)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
@@ -104,13 +104,13 @@ class UserViewModel : BaseViewModel() {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
ToastUtils.showToast("查询人脸数据为空")
//ToastUtils.showToast("查询人脸数据为空")
return@withContext
}
val item = list.map { it.member }
Timber.tag(TAG).d("getUserFaceCache item = ${item}")
//val item = list.map { it.member }
//Timber.tag(TAG).d("getUserFaceCache item = $item")
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).also { item->
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).also { item ->
item.userType = if (it.member) "1" else "2"
item.cardNo = it.cardNo
}
@@ -127,7 +127,7 @@ class UserViewModel : BaseViewModel() {
//非空最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
SpTool.lastFaceTimestamp = lastFaceTimestamp
_loadFaceResult.value = true
}
@@ -205,7 +205,7 @@ class UserViewModel : BaseViewModel() {
//非空实现最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
SpTool.lastFaceTimestamp = lastFaceTimestamp
onAllQueryFinished()
}
}
@@ -363,9 +363,14 @@ class UserViewModel : BaseViewModel() {
/**
* 二维码支付
*/
fun qrCodePay(authCode: String,orderNo: String, block:(Boolean, String?)-> Unit){
fun qrCodePay(
authCode: String,
orderNo: String,
memberId: String?,
block: (Boolean, String?) -> Unit
) {
launch {
val response = repository.qrCodePay(authCode, orderNo)
val response = repository.qrCodePay(authCode, orderNo, memberId)
if (parseResponse(response)) {
block(true, response.data)
} else {
@@ -373,6 +378,7 @@ class UserViewModel : BaseViewModel() {
}
}
}
fun getDinnerType() {
Timber.tag(TAG).d("getDinnerType")
_dinnerTypeInfo.value = null
@@ -491,7 +497,7 @@ class UserViewModel : BaseViewModel() {
/**
* 会员支付
*/
fun memberPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
fun memberPay(param: HashMap<String, String?>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("memberPay")
launchWithLoading {
val response = repository.memberPay(param)
@@ -588,19 +594,22 @@ class UserViewModel : BaseViewModel() {
}
}
fun getCollectedFoodVector(param: MutableMap<String, String>, block: (List<FoodVector>?) -> Unit) {
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())
block(response.data ?: emptyList())
} else {
block(null)
}
}
}
fun deleteCollectFood(foodId:String?, version: String?, block: (Boolean) -> Unit) {
fun deleteCollectFood(foodId: String?, version: String?, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("deleteCollectFood")
launchWithLoading {
val response = repository.deleteCollectFood(foodId, version)
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 眼睛轮廓 -->
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/black"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M 2 12 C 2 12 5 6 12 6 C 19 6 22 12 22 12 C 22 12 19 18 12 18 C 5 18 2 12 2 12 Z" />
<!-- 瞳孔(path绘制圆形) -->
<path
android:fillColor="@android:color/black"
android:pathData="M 12 9 C 13.66 9 15 10.34 15 12 C 15 13.66 13.66 15 12 15 C 10.34 15 9 13.66 9 12 C 9 10.34 10.34 9 12 9 Z" />
<!-- 斜线表示隐藏 -->
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/black"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M 4 4 L 20 20" />
</vector>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 眼睛轮廓 -->
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/black"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M 2 12 C 2 12 5 6 12 6 C 19 6 22 12 22 12 C 22 12 19 18 12 18 C 5 18 2 12 2 12 Z" />
<!-- 瞳孔(path绘制圆形) -->
<path
android:fillColor="@android:color/black"
android:pathData="M 12 9 C 13.66 9 15 10.34 15 12 C 15 13.66 13.66 15 12 15 C 10.34 15 9 13.66 9 12 C 9 10.34 10.34 9 12 9 Z" />
</vector>
+2 -5
View File
@@ -11,16 +11,14 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:background="@drawable/setting_border"
android:orientation="vertical"
android:paddingBottom="16dp">
<TextView
android:id="@+id/tvChargeSetting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_marginStart="32dp"
android:layout_marginTop="35dp"
android:layout_marginTop="30dp"
android:drawablePadding="17dp"
android:text="餐品采集"
android:textColor="@color/black"
@@ -34,7 +32,6 @@
android:id="@+id/btnCollectedFood"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="32dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
@@ -51,7 +48,7 @@
android:id="@+id/flPreview"
android:layout_width="456dp"
android:layout_height="342dp"
android:layout_marginTop="41dp"
android:layout_marginTop="30dp"
app:layout_constraintStart_toStartOf="@id/tvChargeSetting"
app:layout_constraintTop_toBottomOf="@id/tvChargeSetting">
+32 -15
View File
@@ -34,24 +34,41 @@
android:textStyle="bold"
tools:ignore="Autofill,HardcodedText,LabelFor,TextFields" />
<EditText
android:id="@+id/etInputNumber"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginHorizontal="64dp"
android:inputType="textVisiblePassword"
android:background="@drawable/setting_border_gray3"
android:hint="输入消费码"
android:textColorHint="#ffc4cfda"
android:textColor="#ff0a1428"
android:layout_marginTop="32dp"
android:textSize="36sp"
android:gravity="center"
android:maxLines="1"
android:text=""
android:paddingHorizontal="50dp"
android:textStyle="bold"
tools:ignore="Autofill,HardcodedText,LabelFor,TextFields" />
android:layout_marginTop="32dp">
<EditText
android:id="@+id/etInputNumber"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword"
android:background="@drawable/setting_border_gray3"
android:hint="输入消费码"
android:textColorHint="#ffc4cfda"
android:textColor="#ff0a1428"
android:textSize="36sp"
android:gravity="center"
android:maxLines="1"
android:text=""
android:paddingHorizontal="50dp"
android:textStyle="bold"
tools:ignore="Autofill,HardcodedText,LabelFor,TextFields" />
<ImageView
android:id="@+id/ivTogglePassword"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_vertical|end"
android:paddingEnd="16dp"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_eye_close"
android:scaleType="centerInside"
tools:ignore="HardcodedText" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
+1 -1
View File
@@ -13,7 +13,7 @@ material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
accompanistPermissions = "0.37.3"
cameraCore = "1.3.0"
cameraCore = "1.4.1"
retrofit = "3.0.0"
okhttp = "4.12.0"
timber = "5.0.1"
+2 -2
View File
@@ -1,8 +1,8 @@
#Mon Jul 14 11:28:37 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip
#distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
#distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-all.zip
#distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -5,7 +5,10 @@ import static android.content.Context.INPUT_SERVICE;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.Arrays;
@@ -14,172 +17,185 @@ import java.util.List;
/**
* 扫描枪事件处理
*/
public class ScanGunKeyEventHelper {
private final static long MESSAGE_DELAY = 500; //延迟500ms,判断扫码是否完成。
private final StringBuffer mStringBufferResult; //扫码内容
private boolean mCaps; //大小写区分
private final Handler mHandler;
private final Runnable mScanningFishedRunnable;
private OnScanSuccessListener mOnScanSuccessListener;
private final Context mContext;
// private String mDeviceName = "TMC HIDKeyBoard";
//private String mDeviceName = "Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)";
//private String mDeviceName1 = "USBKey Chip USBKey Module";
private static final long SCAN_DELAY = 200; // 扫码结束判断时间
private String[] deviceNameArray = new String[]{
"CROWN CROWN Barcode KeyBorad FS",
"Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)",
"USBKey Chip USBKey Module"
};
private long lastTime = 0L;
private final StringBuilder resultBuffer = new StringBuilder();
private boolean caps;
public ScanGunKeyEventHelper(Context context, OnScanSuccessListener onScanSuccessListener) {
mContext = context;
mOnScanSuccessListener = onScanSuccessListener;
mStringBufferResult = new StringBuffer();
mHandler = new Handler();
mScanningFishedRunnable = this::performScanSuccess;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable scanFinishRunnable;
private OnScanSuccessListener listener;
private long lastScanTime = 0;
public ScanGunKeyEventHelper(OnScanSuccessListener listener) {
this.listener = listener;
scanFinishRunnable = this::handleScanFinish;
}
/**
* 返回扫码成功后的结果
* 扫码结束
*/
private void performScanSuccess() {
String barcode = mStringBufferResult.toString();
L.d("performScanSuccess barcode = " + barcode);
long currentTime = System.currentTimeMillis();
if (currentTime - lastTime < 5 * 1000) {
L.d("performScanSuccess 操作太频繁");
mStringBufferResult.setLength(0);
private void handleScanFinish() {
String barcode = resultBuffer.toString();
if (barcode.isEmpty()) {
return;
}
lastTime = currentTime;
if (mOnScanSuccessListener != null && !TextUtils.isEmpty(barcode))
mOnScanSuccessListener.onScanSuccess(barcode);
mStringBufferResult.setLength(0);
long now = System.currentTimeMillis();
// 防止重复扫描
if (now - lastScanTime < 1000) {
resultBuffer.setLength(0);
return;
}
lastScanTime = now;
if (listener != null) {
listener.onScanSuccess(barcode);
}
resultBuffer.setLength(0);
}
/**
* 扫码枪事件解析
*
* @param event
* 解析 KeyEvent
*/
public void analysisKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
//字母大小写判断
checkLetterStatus(event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
char aChar = getInputCode(event);
if (aChar != 0) {
mStringBufferResult.append(aChar);
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
//若为回车键,直接返回
mHandler.removeCallbacks(mScanningFishedRunnable);
mHandler.post(mScanningFishedRunnable);
} else {
//延迟post,若500ms内,有其他事件
mHandler.removeCallbacks(mScanningFishedRunnable);
mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
}
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return;
}
checkCaps(event);
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_ENTER) {
handler.removeCallbacks(scanFinishRunnable);
handler.post(scanFinishRunnable);
return;
}
char c = getInputCode(event);
if (c != 0) {
resultBuffer.append(c);
}
handler.removeCallbacks(scanFinishRunnable);
handler.postDelayed(scanFinishRunnable, SCAN_DELAY);
}
//检查shift键
private void checkLetterStatus(KeyEvent event) {
/**
* 判断是否为扫码枪
*/
public boolean isScanGunEvent(KeyEvent event) {
if (event == null) return false;
InputDevice device = event.getDevice();
if (device == null) return false;
int sources = device.getSources();
// HID keyboard
boolean isKeyboard =
(sources & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD;
// 排除虚拟键盘
boolean notVirtual =
event.getDeviceId() != KeyCharacterMap.VIRTUAL_KEYBOARD;
return isKeyboard && notVirtual;
}
/**
* SHIFT 状态
*/
private void checkCaps(KeyEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
//按着shift键,表示大写
mCaps = true;
} else {
//松开shift键,表示小写
mCaps = false;
}
if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
|| keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
caps = event.getAction() == KeyEvent.ACTION_DOWN;
}
}
/**
* 获取扫描内容
*
* @param event
* @return
* KeyEvent 转字符
*/
private char getInputCode(KeyEvent event) {
int keyCode = event.getKeyCode();
char aChar;
if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
//字母
aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);
return (char) ((caps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);
} else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
//数字
aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
} else {
//其他符号
switch (keyCode) {
case KeyEvent.KEYCODE_PERIOD:
aChar = '.';
break;
case KeyEvent.KEYCODE_MINUS:
aChar = mCaps ? '_' : '-';
break;
case KeyEvent.KEYCODE_SLASH:
aChar = '/';
break;
case KeyEvent.KEYCODE_BACKSLASH:
aChar = mCaps ? '|' : '\\';
break;
default:
aChar = 0;
break;
}
return (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
}
return aChar;
switch (keyCode) {
case KeyEvent.KEYCODE_MINUS:
return caps ? '_' : '-';
case KeyEvent.KEYCODE_EQUALS:
return caps ? '+' : '=';
case KeyEvent.KEYCODE_LEFT_BRACKET:
return caps ? '{' : '[';
case KeyEvent.KEYCODE_RIGHT_BRACKET:
return caps ? '}' : ']';
case KeyEvent.KEYCODE_BACKSLASH:
return caps ? '|' : '\\';
case KeyEvent.KEYCODE_SEMICOLON:
return caps ? ':' : ';';
case KeyEvent.KEYCODE_APOSTROPHE:
return caps ? '"' : '\'';
case KeyEvent.KEYCODE_COMMA:
return caps ? '<' : ',';
case KeyEvent.KEYCODE_PERIOD:
return caps ? '>' : '.';
case KeyEvent.KEYCODE_SLASH:
return caps ? '?' : '/';
case KeyEvent.KEYCODE_SPACE:
return ' ';
default:
return 0;
}
}
public void release() {
handler.removeCallbacks(scanFinishRunnable);
listener = null;
}
public interface OnScanSuccessListener {
void onScanSuccess(String barcode);
}
public void onDestroy() {
mHandler.removeCallbacks(mScanningFishedRunnable);
mOnScanSuccessListener = null;
}
/**
* 输入设备是否存在
*
* @param deviceName
* @return
*/
public boolean isInputDeviceExist(String deviceName) {
InputManager inputManager = (InputManager) mContext.getSystemService(INPUT_SERVICE);
int[] deviceIds = inputManager.getInputDeviceIds();
for (int id : deviceIds) {
if (inputManager.getInputDevice(id).getName().equals(deviceName)) {
return true;
}
}
return false;
}
/**
* 是否为扫码枪事件(部分机型KeyEvent获取的名字错误)
*
* @param event
* @return
*/
public boolean isScanGunEvent(KeyEvent event) {
if (event == null || event.getDevice() == null) return false;
String deviceName = event.getDevice().getName();
L.e("event===" + deviceName +
"===Char===" + event.getCharacters() +
"===Action===" + event.getAction());
//return deviceName.equals(mDeviceName) || deviceName.equals(mDeviceName1);
List<String> devices = Arrays.asList(deviceNameArray);
return devices.contains(deviceName);
}
}
}