Compare commits

..
10 Commits
Author SHA1 Message Date
lvmeng 5047712198 提交历史代码 2026-03-18 14:32:26 +08:00
lvmeng 3768bd94f2 识别成功未提示及采集时间长问题优化 2026-02-28 10:03:04 +08:00
lvmeng dcfb684b66 优化 2026-02-05 15:06:00 +08:00
lvmeng e5940605c1 优化 2026-02-05 08:39:26 +08:00
lvmeng 96e795c749 优化 2026-02-03 19:01:57 +08:00
lvmeng c7cad6d8b5 备份代码 2026-02-03 17:02:28 +08:00
lvmeng 2656e89244 增量人脸数据查询优化 2025-12-31 11:53:35 +08:00
lvmeng b5daf0f5df 拉取全量和增量人脸数据,增加已采集数据提示 2025-12-29 14:29:30 +08:00
lvmeng a179c52591 gradle版本 2025-12-26 13:49:17 +08:00
lvmeng 894962e387 人脸采集代码提交 2025-12-26 13:48:50 +08:00
192 changed files with 19680 additions and 34 deletions
+14 -34
View File
@@ -1,35 +1,15 @@
# ---> Android
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+1
View File
@@ -0,0 +1 @@
/build
+84
View File
@@ -0,0 +1,84 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.sw.face.collect"
compileSdk = 36
defaultConfig {
applicationId = "com.sw.face.collect"
minSdk = 30
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
viewBinding {
enable = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
// 协程
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// camerax
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)
// 权限申请
implementation(libs.accompanist.permissions)
implementation(project(":lib_face"))
// implementation("com.github.Dimezis:BlurView:version-3.2.0")
// retrofit网络请求
implementation(libs.retrofit)
implementation(libs.converter.gson)
// okhttp
implementation(libs.okhttp)
implementation(libs.logging.interceptor)
// gson
implementation(libs.gson)
// implementation("com.google.android.material:flowlayout:1.12.0")
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.FaceCollect">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="landscape"
tools:ignore="DiscouragedApi">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.sw.face.collect.InitActivity" />
</application>
</manifest>
@@ -0,0 +1,18 @@
package com.sw.face.collect
import com.sw.face.collect.base.BaseActivity
import com.sw.face.collect.databinding.ActivityInitBinding
import com.sw.face.collect.databinding.ActivityMainBinding
class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun inflateViewBinding() = ActivityInitBinding.inflate(layoutInflater)
override fun initialize() {
super.initialize()
binding.btnStart.setOnClickListener {
finish()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,163 @@
//package com.sw.face.collect
//
//import android.Manifest
//import android.net.Uri
//import android.util.Log
//import android.view.View
//import androidx.camera.view.PreviewView
//import androidx.core.view.updateLayoutParams
//import com.sw.face.collect.base.BaseActivity
//import com.sw.face.collect.databinding.ActivityMainBakBinding
//import com.sw.face.collect.databinding.LayoutCameraPreviewBinding
//import com.sw.face.collect.ext.dp
//import com.sw.face.collect.ext.gone
//import com.sw.face.collect.ext.toast
//import com.sw.face.collect.ext.visible
//import com.sw.face.collect.socket.LanCommunicationManager
//import com.sw.face.collect.socket.LanServer
//import com.sw.face.collect.utils.Base64
//import com.sw.face.collect.utils.CameraUtils
//import com.sw.face.collect.utils.ToastUtils
//import org.json.JSONException
//import org.json.JSONObject
//
//class MainActivityBak : BaseActivity<ActivityMainBakBinding>() {
// // private val debouncer = Debouncer(2000)
//
// companion object {
// private const val TAG = "MainActivity"
// }
//
// private lateinit var previewView: PreviewView
// private val cameraUtils: CameraUtils by lazy {
// CameraUtils(this)
// }
//
// override fun inflateViewBinding() = ActivityMainBakBinding.inflate(layoutInflater)
//
// override fun initialize() {
// super.initialize()
// cameraUtils.initCamera()
// val previewBinding =
// LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
// previewView = previewBinding.previewView.also {
// it.updateLayoutParams {
// width = 680.dp
// height = 500.dp
// }
// }
// cameraUtils.setPreviewController(previewView)
//
// binding.btnClear.setOnClickListener {
// binding.ivFaceImage.gone()
// binding.flCameraPreview.visible()
// }
// binding.btnTakePhoto.setOnClickListener {
// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
// if (granted.not()) {
// toast("没有相机权限")
// return@requestSinglePermissionResult
// }
// takePhoto()
// }
// }
// binding.btnUpload.setOnClickListener {
// getFaceData { faceData ->
// Log.d(TAG, "faceData: $faceData")
// sendFaceData(faceData)
// }
// }
// binding.tvTitle.setOnClickListener { finish() }
// addSocketListener()
// }
//
// private fun sendFaceData(faceData: String) {
// // 给某个客户端发送
// val jsonObject = JSONObject().also {
// it.put("type", "cmd")
// it.put("content", faceData)
// }
// val clientId = "device12231530"
// lanManager?.sendToClient(clientId, jsonObject)
// lanManager?.broadcast(jsonObject)
// }
//
// private var lanManager: LanCommunicationManager?=null
// private fun addSocketListener() {
// lanManager = LanServer.getInstance()
// lanManager?.setListener(object : LanCommunicationManager.Listener{
// 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")
// }
// })
// try {
// lanManager?.start()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
//
// private fun takePhoto() {
// showWaitingDialog("拍照中……")
// cameraUtils.takePhoto(cameraCallback) { errMsg ->
// hideWaitingDialog()
// ToastUtils.showToast("拍照异常,请重新操作")
// }
// }
//
// private var imageUri: Uri? = null
// private fun getFaceData(block: (String) -> Unit) {
// if (imageUri == null) {
// toast("未采集到照片,请拍照")
// return
// }
// context.contentResolver.openInputStream(imageUri!!)?.use { stream ->
// val faceData = Base64.encode(stream.readBytes())
// block(faceData)
// }
// }
//
// private val cameraCallback: (Uri) -> Unit = { uri ->
// try {
// this.imageUri = uri
// binding.flCameraPreview.gone()
// binding.ivFaceImage.run {
// visible()
// setImageURI(uri)
// }
// hideWaitingDialog()
// } catch (e: Exception) {
// e.printStackTrace()
// hideWaitingDialog()
// ToastUtils.showToast("程序异常${e.message}")
// }
// }
//
// 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,887 @@
//package com.sw.face.collect
//
//import android.Manifest
//import android.content.Intent
//import android.content.pm.PackageManager
//import android.graphics.Point
//import android.hardware.Camera
//import android.util.Log
//import android.view.View
//import android.view.ViewGroup
//import android.view.ViewTreeObserver
//import android.widget.FrameLayout
//import androidx.activity.viewModels
//import androidx.core.content.ContextCompat
//import androidx.lifecycle.Observer
//import androidx.lifecycle.lifecycleScope
//import com.arcsoft.face.ErrorInfo
//import com.sw.face.collect.base.BaseActivity
//import com.sw.face.collect.base.GlobalData
//import com.sw.face.collect.databinding.ActivityMainBinding
//import com.sw.face.collect.ext.dp
//import com.sw.face.collect.ext.gone
//import com.sw.face.collect.ext.toast
//import com.sw.face.collect.ext.visible
//import com.sw.face.collect.model.UserFaceModel
//import com.sw.face.collect.socket.LanServer
//import com.sw.face.collect.socket.TcpClient
//import com.sw.face.collect.utils.Base64
//import com.sw.face.collect.utils.BitmapUtils
//import com.sw.face.collect.utils.FaceEngineUtils
//import com.sw.face.collect.utils.IntervalExecutor
//import com.sw.face.collect.utils.SpTool
//import com.sw.face.collect.utils.countDownByFlow
//import com.sw.face.collect.view.LanServerListenerImpl
//import com.sw.face.collect.view.TcpClientListenerImpl
//import com.sw.face.collect.viewmodel.MainViewModel
//import com.sw.plate.utils.L
//import com.sw.plate.utils.NV21ToBitmap
//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.facedb.FaceDatabase
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.plate.utils.arcface.model.UserFaceInfo
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
//import kotlinx.coroutines.Job
//import kotlinx.coroutines.runBlocking
//import org.json.JSONObject
//import kotlin.getValue
//import kotlin.system.exitProcess
//
//class MainActivityBak2 : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlobalLayoutListener {
// // private val debouncer = Debouncer(2000)
//
// companion object {
// private const val TAG = "MainActivity"
// }
//
// // 虹软人脸配置 ⬇
// private var isRecognition = false
// private var rgbCameraHelper: DualCameraHelper? = null
// private var rgbFaceRectTransformer: FaceRectTransformer? = null
//
// private var livenessType = LivenessType.IR
//
// // private val livenessType = LivenessType.RGB
// private var openRectInfoDraw = false
//
// private val nv21ToBitmap by lazy { NV21ToBitmap(this) }
//
// //
// private var irCameraHelper: DualCameraHelper? = null
// private var irFaceRectTransformer: FaceRectTransformer? = null
//
// private val recognizeViewModel: RecognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
//
// private val mainViewModel by viewModels<MainViewModel>()
//
// private var currentUserId: String? = null
// private var lastFaceTrackId = -1
//
// override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
//
// override fun onGlobalLayout() {
// Log.d(TAG, "onGlobalLayout")
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// openCamera()
// }
//
// private var pageNo = 1
// override fun initialize() {
// super.initialize()
// mainViewModel.getUserFaceCache(pageNo = pageNo)
// FaceEngineUtils.activeEngine()
//
// binding.btnCollectFace.setOnClickListener {
// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
// if (granted.not()) {
// toast("没有相机权限")
// return@requestSinglePermissionResult
// }
// collectFace()
// }
// }
// binding.tvTitle.setOnClickListener { finish() }
// addSocketListener()
//
// binding.flFace.visible()
// binding.flRecognizeIr.visible()
// binding.flCameraView.visible()
// binding.tvFaceTip.visible()
// binding.layoutState.gone()
//
//// resumeCamera()
// startFaceTask()
// }
//
//
// private fun openCamera() {
// Log.d(TAG, "openCamera")
// if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
// PackageManager.PERMISSION_GRANTED
// ) {
// toast("无摄像头权限")
// return
// }
// try {
//// val cameraCount = Camera.getNumberOfCameras()
//// if (cameraCount < 3) {
//// toast("摄像头数量异常"+cameraCount)
//// return
//// }
// recognizeViewModel.init(
// PreviewConfig(
// PreviewConfig.DEFAULT_IR_CAMERA_ID, PreviewConfig.DEFAULT_RGB_CAMERA_ID,
// 0, 0
// )
// )
// initRgbCamera()
// if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
// initIrCamera()
// }
// } catch (e: Exception) {
// Log.d(TAG, "打开摄像头失败")
// toast("打开摄像头失败+${e.message}")
// }
// }
//
//
// /**
// * 启用虹软人脸识别
// */
// fun setupArcCamera() {
// initArcViewModel()
// initArcView()
// openRectInfoDraw = true
// }
//
// private fun sendFaceData(faceData: String) {
// // 给某个客户端发送
// val jsonObject = JSONObject().also {
// it.put("type", "faceFeature")
// it.put("content", faceData)
// }
// tcpClient?.send(jsonObject)
//
// // TODO: 保存采集的人脸数据,暂时生成id,实际id在结算终端生成并发送到各台设备
// saveFaceInfo(faceData)
// }
//
// private fun saveFaceInfo(faceData: String) {
// Thread {
// try {
// val tempId = "${System.currentTimeMillis()}-123456789"
// val faceEntity = FaceEntity(
// tempId,
// null,
// Base64.decode(faceData)
// ).also {
// it.userType = "2"
// }
// FaceDatabase.getInstance(this).faceDao().insert(faceEntity)
// recognizeViewModel.refreshFaceList();
// runOnUiThread {
// toast("你的人脸信息已采集")
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }.start()
// }
//
// private var tcpClient: TcpClient? = null
//
// private fun addSocketListener() {
// //val ipAddress = NetworkUtils.getIPAddress(true)
// //Log.d(TAG, "addSocketListener,ipAddress: $ipAddress");
// tcpClient = TcpClient(
// "192.168.1.95",
// 5000,
// GlobalData.deviceId, // clientId
// 5000, // connectTimeoutMs
// 10 * 1000, // heartbeatIntervalMs
// 30 * 1000 // heartbeatTimeoutMs
// )
//
// tcpClient?.setListener(object : TcpClientListenerImpl() {
// override fun onSendSuccess(json: JSONObject?) {
// super.onSendSuccess(json)
//// toast("发送成功")
// }
//
// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
// super.onSendFailed(json, e)
// toast("发送失败${e?.message}")
// }
//
// override fun onMessage(json: JSONObject?) {
// super.onMessage(json)
//
// val type = json?.getInt("type")
// // 根据服务端消息类型处理删除新增临时用户人脸数据---------------------
// when (type) {
// LanServer.TYPE_ADD_FACE_DATA -> {
// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据
// }
// LanServer.TYPE_CLEAR_FACE_DATA -> {
// clearFaceData()
// }
// else-> {}
// }
// }
// })
// tcpClient?.start();
//
// try {
// LanServer.getInstance().let {
// it.setListener(object : LanServerListenerImpl() {
// override fun onMessageReceived(clientId: String?, message: JSONObject?) {
// super.onMessageReceived(clientId, message)
// toast("收到消息clientId=$clientId")
// }
// })
// it.start()
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
//
// // private var imageBitmap: Bitmap? = null
// private fun collectFace() {
// //toast("currentUserId=$currentUserId")
// if (currentUserId != null) {
// toast("您已采集过人脸信息")
// return
// }
// currentUserId = null
// userFaceInfo = null
// recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
// //showWaitingDialog("人脸信息中……")
//
// binding.tvFaceTip.gone()
// binding.btnCollectFace.gone()
// binding.layoutState.visible()
// binding.pbCollectLoading.visible()
// binding.tvCollectState.text = "采集中......"
// getFaceData { faceData ->
// Log.d(TAG, "faceData: $faceData")
// runOnUiThread {
// //hideWaitingDialog()
// //toast("人脸信息已采集")
// binding.pbCollectLoading.gone()
// //binding.tvCollectState.text = "采集完成"
// userFaceInfo = null
// countDown()
// }
// sendFaceData(faceData)
// }
// }
//
// private var job: Job? = null
//
// private fun getFaceData(block: (String) -> Unit) {
// runBlocking {
// job = executor.startIntervalTask(5) {
//// if (currentUserId != null && currentUserId!!.isNotBlank()) {
//// toast("您已采集过人脸信息")
//// return@startIntervalTask
//// }
// if (userFaceInfo == null) {
// return@startIntervalTask
// }
// val faceData = Base64.encode(userFaceInfo!!.faceFeature!!.featureData)
// block(faceData)
// job?.cancel()
// }
// }
//
// }
//
// val executor = IntervalExecutor()
//
//
// public override fun onStop() {
// rgbCameraHelper?.release()
// rgbCameraHelper = null
// irCameraHelper?.release()
// irCameraHelper = null
// recognizeViewModel.destroy()
// super.onStop()
// }
//
// fun resumeCamera() {
// Log.d(TAG, "resumeCamera isRecognition = $isRecognition")
// isRecognition = true
// if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
// rgbCameraHelper!!.start()
// }
// }
//
// fun pauseCamera() {
// Log.d(TAG, "pauseCamera isRecognition = $isRecognition")
// isRecognition = false
//
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
//
// private var userFaceInfo: UserFaceInfo? = null
// private var isCheckCamera = false
// private fun initArcViewModel() {
// if (DualCameraHelper.hasDualCamera()) {
// livenessType = LivenessType.IR
// isCheckCamera = true
// } else {
// livenessType = LivenessType.RGB
// isCheckCamera = false
// }
// recognizeViewModel.setLiveType(livenessType)
//
// recognizeViewModel.prepareRegister()
// recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
// this.userFaceInfo = userFaceInfo
// Log.d(TAG, "initArcViewModel: userFaceInfo == null : ${userFaceInfo == null}")
// }
// recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
// if (ftInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "ftEngine",
// ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
// )
// Log.d(TAG, "ftInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
// if (frInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "frEngine",
// frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
// )
// Log.d(TAG, "frInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
// if (flInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "flEngine",
// flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
// )
// Log.d(TAG, "flInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
//
// recognizeViewModel.recognizeConfiguration
// .observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
// Log.d(TAG, "recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
// })
// recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
// Log.d(TAG, "recognizeNotice observe notice = $notice")
// })
//
// recognizeViewModel.recognizeUserId.observe(
// this,
// Observer { compareResult: CompareResult ->
// Log.d(
// TAG,
// "recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
// )
// lastFaceTrackId = compareResult.trackId
// val faceEntity = compareResult.faceEntity
// currentUserId = faceEntity.userName
// if (!currentUserId.isNullOrBlank()) {
// toast("您已采集过人脸信息")
// return@Observer
// }
//// //未识别到,拍摄照片
//
// })
//
// recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
// Log.d(TAG, "drawRectInfoText observe info = $info")
// })
// }
//
// private fun initArcView() {
// //在布局结束后才做初始化操作
// binding.dualCameraTexturePreviewRgb.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 = 640.dp//300.dp
// val w = 640.dp//300.dp
// val h = 480.dp//510.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
// ) {
// Log.d(
// TAG,
// "initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation"
// )
// runOnUiThread({
// val previewSizeRgb = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraFaceRectView,
// previewSizeRgb, displayOrientation,
// 0.5F
// //0.6F
// )
//
// Log.d(
// TAG,
// "initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}"
// )
// Log.d(
// TAG,
// "initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}"
// )
// Log.d(
// TAG,
// "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
// ConfigUtil.isDrawRgbRectHorizontalMirror(
// context
// )
// }, isDrawRgbRectVerticalMirror = ${
// ConfigUtil.isDrawRgbRectVerticalMirror(
// context
// )
// }"
// )
// // 调整识别窗口位置
// rgbFaceRectTransformer = FaceRectTransformer(
// previewSizeRgb.width,
// previewSizeRgb.height,
// layoutParams.width,
// layoutParams.height,
// displayOrientation,
//// 90,
// cameraId,
// isMirror,
// true,
// false
// )
//
// recognizeViewModel.onRgbCameraOpened(camera)
// recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
// })
// }
//
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
//// faceByteArray = nv21
// if (!isRecognition) {
// return
// }
// if (isCheckCamera) {
// L.e("检查摄像头数据")
//
// isCheckCamera = false
//
// val isBw: Boolean = BitmapUtils.isBlackWhite(
// nv21ToBitmap.nv21ToBitmap(
// nv21,
// rgbFaceRectTransformer!!.previewWidth,
// rgbFaceRectTransformer!!.previewHeight,
// 0
// )
// )
// if (isBw) {
// L.e("黑白切换摄像头")
// switchCamera()
// }
// L.e("摄像头正常")
// }
// binding.dualCameraFaceRectView.clearFaceInfo()
// val facePreviewInfoList: MutableList<FacePreviewInfo>? =
// recognizeViewModel.onPreviewFrame(nv21, true)
// if (facePreviewInfoList != null
// && rgbFaceRectTransformer != null
// ) {
// drawPreviewInfo(facePreviewInfoList)
// }
// recognizeViewModel.clearLeftFace(facePreviewInfoList)
// }
//
// override fun onCameraClosed() {
// Log.d(TAG, "initRgbCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Log.d(TAG, "initRgbCamera onCameraError: ${e.message}")
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
// Log.d(
// TAG,
// "initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}"
// )
// if (rgbFaceRectTransformer != null) {
// rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Log.d(
// TAG,
// "initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation"
// )
// }
// }
// val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth
// val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight
// Log.d(TAG, "initRgbCamera measuredWidth=$measuredWidthmeasuredHeight=$measuredHeight")
//
// val previewConfig = recognizeViewModel.previewConfig
// rgbCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(Point(measuredWidth, measuredHeight))
//// .previewViewSize(Point(608.dp, 456.dp))
//// .previewViewSize(Point(1024, 768))
// .rotation(windowManager.defaultDisplay.rotation)
// .additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
// .previewSize(recognizeViewModel.loadPreviewSize())
//// .previewSize(Point(1080, 720))
// .specificCameraId(previewConfig.rgbCameraId)
// .isMirror(true)
// .previewOn(binding.dualCameraTexturePreviewRgb)
// .cameraListener(cameraListener)
// .build()
// rgbCameraHelper?.run {
// init()
//// setSurfaceFrameCallback { bitmap ->
//// imageBitmap = BitmapFlipUtil.flipHorizontally(bitmap)
////
////// binding.ivRecImage.visible()
////// binding.ivRecImage.setImageBitmap(imageBitmap)
//// }
// start()
// }
// }
//
// /**
// * 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
// */
// private fun initIrCamera() {
// Log.d(TAG, "initIrCamera: livenessType = $livenessType")
// if (livenessType === LivenessType.RGB) {
// return
// }
// val irCameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// Log.d(
// TAG,
// "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() {
// Log.d(TAG, "initIrCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Log.d(TAG, "initIrCamera onCameraError: ${e.message}")
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(
// cameraID: Int,
// displayOrientation: Int
// ) {
// if (irFaceRectTransformer != null) {
// irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Log.d(
// TAG,
// "initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation"
// )
// }
// }
//
// val previewConfig = recognizeViewModel.previewConfig
// irCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(
// Point(
// binding.dualCameraTexturePreviewIr.measuredWidth,
// binding.dualCameraTexturePreviewIr.measuredHeight
// )
// )
//// .previewViewSize(Point(4,3))
// .rotation(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)
// }
//
// val listIsEmpty = facePreviewInfoList.isEmpty()
// val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null
// Log.d(
// TAG,
// "listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId"
// )
// if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) {
// if (lastFaceTrackId != -1 && currentUserId != null) {
// Log.d(TAG, "$lastFaceTrackId 用户离开")
// lastFaceTrackId = -1
// currentUserId = null
// }
// }
// }
//
// fun switchCamera() {
// try {
// L.e("开始切换摄像头")
// if (rgbCameraHelper != null && irCameraHelper != null) {
// rgbCameraHelper!!.stop()
// irCameraHelper!!.stop()
// rgbCameraHelper!!.switchCameraId()
// irCameraHelper!!.switchCameraId()
// rgbCameraHelper!!.start()
// irCameraHelper!!.start()
// rgbFaceRectTransformer!!.cameraId =
// if (rgbFaceRectTransformer!!.getCameraId() === 1) 0 else 1
// // showLongToast(getString(R.string.notice_change_detect_degree));
// L.e("切换摄像头成功")
// } else {
// L.e("切换摄像头失败")
//
// // showToast(getString(R.string.switch_camera_failed));
// exit()
// val newIntent = Intent(this, MainActivityBak2::class.java) // 要启动的Activity
// newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// startActivity(newIntent)
// }
// } catch (e: java.lang.RuntimeException) {
// L.e("切换摄像头失败")
// toast(e.message + getString(R.string.camera_error_notice))
//
// exit()
// val newIntent = Intent(this, MainActivityBak2::class.java) // 要启动的Activity
// newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// startActivity(newIntent)
// }
// }
//
// private fun exit() {
// finish()
// exitProcess(0)
// }
//
// override fun onDestroy() {
// countDownJob?.cancel()
// faceTaskJob?.cancel()
// super.onDestroy()
// }
//
// private var countDownJob: Job? = null
// private fun countDown() {
// countDownJob = countDownByFlow(
// total = 3,
// scope = lifecycleScope,
// onStart = {
// binding.tvCollectState.text = "采集完成(3s"
// },
// onTick = { seconds ->
// binding.tvCollectState.text = "采集完成(${seconds}s"
// },
// onFinish = {
// //binding.tvCollectState.text = "采集完成"
// currentUserId = null
// binding.layoutState.gone()
// binding.tvFaceTip.visible()
// binding.btnCollectFace.visible()
// }
// )
// }
//
// private val intervalExecutor by lazy { IntervalExecutor() }
// private var faceTaskJob: Job? = null
//
// private var taskPageNo = 1
// fun startFaceTask() {
// faceTaskJob =
// intervalExecutor.startIntervalTaskWithInitialDelay(60 * 1000L, 30 * 1000L) {
// val timestamp = SpTool.getLastFaceTimestamp()
// if (timestamp == 0L) {
// return@startIntervalTaskWithInitialDelay
// }
// mainViewModel.getFaceIncrementList(
// pageNo = taskPageNo,
// timestamp = timestamp,
// onAllQueryFinished = {
// taskPageNo = 1
// },
// onPageQueryFinished = { list ->
// runOnUiThread {
// if (list.isEmpty()) {
// return@runOnUiThread
// }
// updateFaceData(list)
// }
// }
// )
// }
// }
//
// private fun updateFaceData(list: List<UserFaceModel>) {
// Thread {
// val faceList = mutableListOf<FaceEntity>()
// try {
// list.forEach { model ->
// if (model.faceDeleted == true) {
// //删除数据
// FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
// } else {
// //保存数据
// val faceEntity = FaceEntity(
// model.userId,
// null,
// Base64.decode(model.faceFeatureStr)
// ).also {
// it.userType = "1"
// }
// faceList.add(faceEntity)
// }
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// try {
// if (faceList.isNotEmpty()) {
// FaceDatabase.getInstance(this).faceDao().insert(faceList)
// }
// recognizeViewModel.refreshFaceList();
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }.start()
// }
//
//// private var dinnerTypeJob: Job? = null
//// fun startDinnerTypeTask() {
//// dinnerTypeJob =
//// intervalExecutor.startIntervalTaskWithInitialDelay(10 * 60 * 1000L, 15 * 60 * 1000L) {
//// mainViewModel.getDinnerType {
////
//// }
//// }
//// }
//
// private fun clearFaceData() {
// Thread {
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// faceDao.deleteTempUserFaceData()
// recognizeViewModel.refreshFaceList();
// }.start()
// }
//
// override fun onResume() {
// super.onResume()
// setupArcCamera()
//
// resumeCamera()
// }
//
// override fun onPause() {
// super.onPause()
// pauseCamera()
// }
//
//}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
package com.sw.face.collect
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
class MyApp: App() {
companion object {
private const val TAG = "MyApp"
var DEBUG = true
@SuppressLint("StaticFieldLeak")
var instance: Context? = null
}
override fun onCreate() {
super.onCreate()
instance = this
GlobalData.deviceId = AppUtil.getUDID(this)
Log.d(TAG, "onCreate: deviceId = ${GlobalData.deviceId}")
}
}
@@ -0,0 +1,122 @@
package com.sw.face.collect.base
import android.content.Context
import android.os.Bundle
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.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.sw.face.collect.databinding.DialogWaitingBinding
import com.sw.face.collect.view.CustomDialog
import com.sw.face.collect.R
import com.sw.face.collect.model.IActivityResult
import com.sw.face.collect.utils.PermissionHelper
import kotlin.collections.set
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: VB
protected lateinit var context: Context
private var mDialogWaiting: CustomDialog? = null
private lateinit var permissionLauncher: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// EventBus.getDefault().register(this)
enableEdgeToEdge()
context = this
//保持亮屏
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = inflateViewBinding()
setContentView(binding.root)
initActivityLauncher()
initialize()
}
override fun onDestroy() {
// EventBus.getDefault().unregister(this)
super.onDestroy()
}
protected abstract fun inflateViewBinding(): VB
open fun initialize() {
}
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?) {
runOnUiThread {
hideWaitingDialog()
val dialogBinding = DialogWaitingBinding.inflate(LayoutInflater.from(this))
dialogBinding.tvTip.text = tip
mDialogWaiting = CustomDialog(this, dialogBinding.root)
mDialogWaiting?.show()
}
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
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: ((Boolean) -> Unit)? = null
fun requestSinglePermissionResult(
permission: String,
callback: (Boolean) -> Unit
) {
this.launchPermissionCallback = callback
permissionLauncher.launch(permission)
}
private fun initActivityLauncher() {
permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
launchPermissionCallback?.invoke(isGranted)
}
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun onPostEvent(event: PostEvent) {
//
// }
}
@@ -0,0 +1,60 @@
package com.sw.face.collect.base
object GlobalData {
/**
* 设备id
*/
var deviceId: String = ""
/**
* app版本号
*/
var appVersion: String = "1"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q3WA-8QT4"
}
/**
* 全局常量
*/
object Constants {
/**
* 自动关闭时间
*/
const val AUTO_CLOSE_TIME: Long = 3
}
/**
* key
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_PICKUP_MODE = "pickupMode"
const val KEY_CHARGE_MODE = "chargeMode"
}
@@ -0,0 +1,274 @@
package com.sw.face.collect.ext
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat
import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment
import com.sw.face.collect.utils.ToastUtils
//import com.google.gson.Gson
//import com.google.gson.reflect.TypeToken
//import com.sw.inbound.utils.CustomToastUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
fun Context.toast(
message: String?,
duration: Int = Toast.LENGTH_SHORT,
) {
if (message.isNullOrBlank()) {
return
}
Handler(Looper.getMainLooper()).post {
//Toast.makeText(this, message, duration).show()
//CustomToastUtils.showToast(message, duration)
ToastUtils.showToast(message,duration)
}
}
fun Fragment.toast(
message: String?,
duration: Int = 2000
) {
activity?.toast(message, duration)
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
inline fun <reified T : Activity> Context.startActivity(
bundle: Bundle? = null,
options: ActivityOptionsCompat? = null
) {
Intent(this, T::class.java).apply {
bundle?.let { putExtras(it) }
if (options != null && this@startActivity is Activity) {
startActivity(this, options.toBundle())
} else {
startActivity(this)
}
}
}
inline fun <reified T> Context.startActivity(
block: Intent.() -> Unit = {}
) {
Intent(this, T::class.java).apply {
block()
startActivity(this)
}
}
//inline fun <reified T : Activity> Context.startActivity(action:(bundle: Bundle)-> Unit) {
// Intent(this, T::class.java).apply {
// action(Bundle())
// startActivity(this)
// }
//}
//inline fun <reified T> String.toType(gson: Gson? = null, typeToken: TypeToken<T>): T {
// return (gson ?: Gson()).fromJson(this, typeToken.type)
//}
//
//inline fun <reified T> String.toObject(gson: Gson? = null): T {
// return (gson ?: Gson()).fromJson(this, T::class.java)
//}
//
//fun Any?.toJsonString(gson: Gson? = null): String {
// return (gson ?: Gson()).toJson(this) ?: ""
//}
@SuppressLint("ApplySharedPref")
inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) editor.commit() else editor.apply()
}
fun SharedPreferences.put(vararg pairs: Pair<String, Any>) {
edit {
pairs.forEach { (key, value) ->
when (value) {
is Int -> putInt(key, value)
is String -> putString(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Long -> putLong(key, value)
}
}
}
}
val Float.dp: Float
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
val Int.dp: Int
get() = this.toFloat().dp.toInt()
fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// 处理搜索逻辑
searchCallback()
return@setOnEditorActionListener true // 阻止事件继续传递
}
return@setOnEditorActionListener false
}
}
//fun View.clickWithDebounce(delay: Long = 300, 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 {
// 用View的tag存储是否可点击的状态(默认可点击)
if (tag as? Boolean ?: true) {
tag = false // 标记为不可点击
action() // 立即执行点击逻辑
// 启动协程,延迟后恢复可点击状态
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
tag = true // 恢复可点击
}
}
}
}
fun Double.roundedDecimalPlace(num: Int): Double {
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
}
fun Double.roundedOneDecimalPlace(): Double {
return this.roundedDecimalPlace(1)
}
fun View.setShapeDrawable(
solidColor: String = "#FFFFFF",
strokeWidth: Int = 0,
strokeColor: String = "#FFFFFF",
radius: Int = 0
) {
background = GradientDrawable().apply {
setColor(solidColor.toColorInt())
setStroke(strokeWidth, strokeColor.toColorInt())
cornerRadius = radius.toFloat()
}
}
fun View.setShapeDrawable2(
solidColor: String = "#FFFFFF",
strokeWidth: Int = 0,
strokeColor: String = "#FFFFFF",
topLeftRadius: Int = 0,
topRightRadius: Int = 0,
bottomRightRadius: Int = 0,
bottomLeftRadius: Int = 0
) {
background = GradientDrawable().apply {
setColor(solidColor.toColorInt())
setStroke(strokeWidth, strokeColor.toColorInt())
setCornerRadii(
floatArrayOf(
topLeftRadius.toFloat(), topLeftRadius.toFloat(),
topRightRadius.toFloat(), topRightRadius.toFloat(),
bottomRightRadius.toFloat(), bottomRightRadius.toFloat(),
bottomLeftRadius.toFloat(), bottomLeftRadius.toFloat()
)
)
}
}
fun String?.ifNullOrBlank(defaultValue: String): String {
return if (this.isNullOrBlank()) defaultValue else this
}
fun Int?.ifNullOrZero(defaultValue: String): String {
return if (this == null || this == 0) defaultValue else this.toString()
}
fun Double?.ifNullOrZero(defaultValue: String): String {
return if (this == null || this == 0.toDouble()) defaultValue else this.toString()
}
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
this.clearFocus() // 清除焦点避免键盘再次弹出
}
/**
* 复制文本到剪贴板
* @param context 上下文
* @param text 要复制的文本内容
* @param showToast 是否显示复制成功提示,默认为true
*/
fun String.copyText(context: Context, showToast: Boolean = true) {
// 获取剪贴板管理器
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
// 创建ClipData对象,包含要复制的文本
val clipData = ClipData.newPlainText("label", this)
// 将数据设置到剪贴板
clipboardManager.setPrimaryClip(clipData)
// 显示复制成功提示
if (showToast) {
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
}
@@ -0,0 +1,24 @@
package com.sw.face.collect.ext
import android.text.SpannableStringBuilder
import android.text.Spanned
inline fun buildSpannableString(builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder {
return SpannableStringBuilder().apply(builderAction)
}
fun SpannableStringBuilder.appendText(text: String, vararg spans: Any): SpannableStringBuilder {
val start = length
append(text)
spans.forEach { span ->
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
return this
}
fun SpannableStringBuilder.withSpan(span: Any, block: SpannableStringBuilder.() -> Unit) {
val start = length
block()
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
@@ -0,0 +1,10 @@
package com.sw.face.collect.model
data class ApiResponse<T>(
val code: String,
val msg: String? = "",
val data: T? = null,
val result: T? = null,
)
@@ -0,0 +1,6 @@
package com.sw.face.collect.model
data class RespCodeMsg(
val code: String?,
val msg: String?
)
@@ -0,0 +1,24 @@
package com.sw.face.collect.model;
import androidx.activity.result.ActivityResult;
import java.util.Map;
public interface IActivityResult {
interface LaunchActivityCallback {
void callback(ActivityResult result);
}
interface LaunchTackPictureCallback {
void callback(boolean result);
}
interface RequestPermissionCallback {
void callback(boolean result);
}
interface RequestMultiplePermissionsCallback {
void callback(Map<String,Boolean> result);
}
}
@@ -0,0 +1,62 @@
package com.sw.face.collect.model
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
//@Parcelize
data class UserFaceModel(
// @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? = "",
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
)
data class DinnerType(
val id: String? = "",
//餐次
val dinnerType: String? = "",
//餐次收费模式(0称重,1固定,2不收费)
val chargeType: Int = 0,
//固定收费(元) 仅charge_type = 1 时
val fixedAmount: Double?=null
)
@@ -0,0 +1,47 @@
package com.sw.face.collect.network
import android.util.Log
import com.sw.face.collect.network.api.ApiService
import com.sw.face.collect.network.interceptor.RequestInterceptor
import com.sw.face.collect.MyApp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object ApiClient {
// private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val BASE_URL = "https://dev.yixiong-tech.com:8081"
//private const val TIME_OUT = 30L // 超时时间(秒)
//todo 临时测试改为5秒
private const val TIME_OUT = 5L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Log.d("ApiClient","okhttp logger ==>${it}")
}).apply {
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.addInterceptor(RequestInterceptor())
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
@@ -0,0 +1,66 @@
package com.sw.face.collect.network.api
import com.sw.face.collect.base.GlobalData
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiService {
/**
* 获取人脸数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取当前餐点类型
*/
@GET
suspend fun getDinnerType(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getRegionRule",
): ApiResponse<DinnerType?>
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData:String,
@Field("url") imageUr:String
): ApiResponse<UserFaceModel?>
}
@@ -0,0 +1,40 @@
package com.sw.face.collect.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.utils.SPUtil
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
/**
* 请求拦截器
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
// .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()
return chain.proceed(newRequest)
}
// private fun getToken(originRequest: Request): String {
// val tokenParam = originRequest.header("X-Access-Token")
// if (!TextUtils.isEmpty(tokenParam)) return tokenParam!!
// // 从本地获取token的逻辑
// val spUtil = SPUtil.getInstance(context = App.getContext())
// return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
//// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
// }
}
@@ -0,0 +1,19 @@
package com.sw.face.collect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.sw.face.collect.MainActivity
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Log.d("BootReceiver","设备启动完成,开始执行自启动逻辑")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,57 @@
package com.sw.dualscreen.repository
import com.google.gson.JsonParseException
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.RespCodeMsg
import com.sw.face.collect.utils.GsonUtils
import retrofit2.HttpException
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
abstract class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
return try {
apiCall()
} catch (e: Exception) {
// Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
when (e) {
is HttpException -> {
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", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
}
}
@@ -0,0 +1,78 @@
package com.sw.face.collect.repository
import com.sw.dualscreen.repository.BaseRepository
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import com.sw.face.collect.network.api.ApiService
import com.sw.face.collect.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
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 getDinnerType(): ApiResponse<DinnerType?> {
return safeApiCall {
apiService.getDinnerType()
}
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
}
@@ -0,0 +1,300 @@
package com.sw.face.collect.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,35 @@
package com.sw.face.collect.socket;
public class LanServer {
public static final int TYPE_ADD_FACE_DATA = 1;
public static final int TYPE_CLEAR_FACE_DATA = 2;
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;
}
void a() {
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_ADD_FACE_DATA);
//data.put("faceId", tempUserId);//收到采集信息,每次生成新的ID
//data.put("faceFeature", faceFeatureString);
//lanServer.broadcast(new JSONObject(data));
//
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_CLEAR_FACE_DATA);
//lanServer.broadcast(new JSONObject(data));
}
}
@@ -0,0 +1,330 @@
package com.sw.face.collect.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,47 @@
package com.sw.face.collect.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.face.collect.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,265 @@
package com.sw.face.collect.utils;
public final class Base64 {
private static final int BASELENGTH = 128;
private static final int LOOKUPLENGTH = 64;
private static final int TWENTYFOURBITGROUP = 24;
private static final int EIGHTBIT = 8;
private static final int SIXTEENBIT = 16;
private static final int FOURBYTE = 4;
private static final int SIGN = -128;
private static char PAD = '=';
private static byte[] base64Alphabet = new byte[BASELENGTH];
private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
static {
for (int i = 0; i < BASELENGTH; ++i) {
base64Alphabet[i] = -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
base64Alphabet[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
base64Alphabet[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
base64Alphabet[i] = (byte) (i - '0' + 52);
}
base64Alphabet['+'] = 62;
base64Alphabet['/'] = 63;
for (int i = 0; i <= 25; i++) {
lookUpBase64Alphabet[i] = (char) ('A' + i);
}
for (int i = 26, j = 0; i <= 51; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('a' + j);
}
for (int i = 52, j = 0; i <= 61; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('0' + j);
}
lookUpBase64Alphabet[62] = (char) '+';
lookUpBase64Alphabet[63] = (char) '/';
}
private static boolean isWhiteSpace(char octect) {
return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
}
private static boolean isPad(char octect) {
return (octect == PAD);
}
private static boolean isData(char octect) {
return (octect < BASELENGTH && base64Alphabet[octect] != -1);
}
/**
* Encodes hex octects into Base64
*
* @param binaryData Array containing binaryData
* @return Encoded Base64 array
*/
public static String encode(byte[] binaryData) {
if (binaryData == null) {
return null;
}
int lengthDataBits = binaryData.length * EIGHTBIT;
if (lengthDataBits == 0) {
return "";
}
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1
: numberTriplets;
char encodedData[] = null;
encodedData = new char[numberQuartet * 4];
byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
int encodedIndex = 0;
int dataIndex = 0;
for (int i = 0; i < numberTriplets; i++) {
b1 = binaryData[dataIndex++];
b2 = binaryData[dataIndex++];
b3 = binaryData[dataIndex++];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6)
: (byte) ((b3) >> 6 ^ 0xfc);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
}
// form integral number of 6-bit groups
if (fewerThan24bits == EIGHTBIT) {
b1 = binaryData[dataIndex];
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
encodedData[encodedIndex++] = PAD;
encodedData[encodedIndex++] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
encodedData[encodedIndex++] = PAD;
}
return new String(encodedData);
}
/**
* Decodes Base64 data into octects
*
* @param encoded string containing Base64 data
* @return Array containind decoded data.
*/
public static byte[] decode(String encoded) {
if (encoded == null) {
return null;
}
char[] base64Data = encoded.toCharArray();
// remove white spaces
int len = removeWhiteSpace(base64Data);
if (len % FOURBYTE != 0) {
return null;// should be divisible by four
}
int numberQuadruple = (len / FOURBYTE);
if (numberQuadruple == 0) {
return new byte[0];
}
byte decodedData[] = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
int i = 0;
int encodedIndex = 0;
int dataIndex = 0;
decodedData = new byte[(numberQuadruple) * 3];
for (; i < numberQuadruple - 1; i++) {
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))
|| !isData((d3 = base64Data[dataIndex++]))
|| !isData((d4 = base64Data[dataIndex++]))) {
return null;
}// if found "no data" just return null
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))) {
return null;// if found "no data" just return null
}
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
d3 = base64Data[dataIndex++];
d4 = base64Data[dataIndex++];
if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
if (isPad(d3) && isPad(d4)) {
if ((b2 & 0xf) != 0)// last 4 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 1];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
return tmp;
} else if (!isPad(d3) && isPad(d4)) {
b3 = base64Alphabet[d3];
if ((b3 & 0x3) != 0)// last 2 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 2];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
return tmp;
} else {
return null;
}
} else { // No PAD e.g 3cQl
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
return decodedData;
}
/**
* remove WhiteSpace from MIME containing encoded Base64 data.
*
* @param data the byte array of base64 data (with WS)
* @return the new length
*/
private static int removeWhiteSpace(char[] data) {
if (data == null) {
return 0;
}
// count characters that's not whitespace
int newSize = 0;
int len = data.length;
for (int i = 0; i < len; i++) {
if (!isWhiteSpace(data[i])) {
data[newSize++] = data[i];
}
}
return newSize;
}
}
@@ -0,0 +1,39 @@
package com.sw.face.collect.utils;
import android.graphics.Bitmap;
import android.graphics.Matrix;
public class BitmapFlipUtil {
/**
* 水平翻转Bitmap
* @param src 原始Bitmap
* @return 水平翻转后的Bitmap
*/
public static Bitmap flipHorizontally(Bitmap src) {
Matrix matrix = new Matrix();
matrix.preScale(-1, 1); // 水平翻转
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
}
/**
* 垂直翻转Bitmap
* @param src 原始Bitmap
* @return 垂直翻转后的Bitmap
*/
public static Bitmap flipVertically(Bitmap src) {
Matrix matrix = new Matrix();
matrix.preScale(1, -1); // 垂直翻转
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
}
/**
* 水平和垂直同时翻转(180°旋转)
* @param src 原始Bitmap
* @return 180°旋转后的Bitmap
*/
public static Bitmap flipBoth(Bitmap src) {
Matrix matrix = new Matrix();
matrix.preScale(-1, -1); // 同时水平和垂直翻转
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
}
}
@@ -0,0 +1,59 @@
package com.sw.face.collect.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.io.use
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,38 @@
package com.sw.face.collect.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.ByteArrayOutputStream
object BitmapToByteArrayUtil {
/**
* Bitmap转PNG格式byte[]
* @param bitmap 原始Bitmap
* @return PNG格式的byte[]
*/
fun bitmapToPngByteArray(bitmap: Bitmap): ByteArray {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
return stream.toByteArray()
}
/**
* Bitmap转JPEG格式byte[]
* @param bitmap 原始Bitmap
* @param quality 压缩质量(0-100
* @return JPEG格式的byte[]
*/
fun bitmapToJpegByteArray(bitmap: Bitmap, quality: Int): ByteArray {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
return stream.toByteArray()
}
/**
* byte[]转Bitmap(通用格式)
* @param data byte数组
* @return 解析后的Bitmap
*/
fun byteArrayToBitmap(data: ByteArray): Bitmap {
return BitmapFactory.decodeByteArray(data, 0, data.size)
}
}
@@ -0,0 +1,36 @@
package com.sw.face.collect.utils;
import android.graphics.Bitmap;
import android.graphics.Color;
public class BitmapUtils {
public static boolean isBlackWhite(Bitmap bmp) {
Color c = null;
//遍历图片的像素点
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
c = bmp.getColor(x, y);
// L.e(c.red() + "===" + c.green() + "===" + c.blue());
//判断像素点的色偏差值Diff
if (getRGBDiff(c.red(), c.green(), c.blue()) != 0) {
return false;
}
}
}
return true;
}
private static float getRGBDiff(float r, float g, float b) {
float test1 = Math.abs(r - g);
float test2 = Math.abs(r - b);
float test3 = Math.abs(g - b);
float max = Math.max(test1, test2);
max = Math.max(max, test3);
return max;
}
}
@@ -0,0 +1,86 @@
package com.sw.face.collect.utils
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.camera.core.CameraSelector
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
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(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) {
this.failCallback = failCallback
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(succCallback)
it.bindCameraCallback {
bind()
}
it.takePhoto()
}
}
private fun initCaptureHelper() {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
failCallback?.invoke(msg)
}
)
}
fun setPreviewController(previewView: PreviewView?) {
if (previewView?.controller == null) {
previewView?.controller = cameraController
}
}
fun initCamera() {
if (cameraController == null) {
cameraController = LifecycleCameraController(activity).apply {
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE
// or CameraController.VIDEO_CAPTURE
)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
// setWhiteBalanceMode(WhiteBalanceMode.AUTO)
}
bind()
}
// if (isCameraReady.not()) {
// try {
// cameraController!!.initializationFuture.addListener({
// isCameraReady = true
// Timber.d("Camera initialized successfully")
// }, ContextCompat.getMainExecutor(this))
// } catch (e: Exception) {
// Timber.d("Camera initialized error = ${e.message}")
// }
// }
}
fun bind() {
cameraController?.bindToLifecycle(activity)
}
fun unbind() {
cameraController?.unbind()
}
}
@@ -0,0 +1,135 @@
package com.sw.face.collect.utils
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
/**
* 无状态残留的CountDownTimer封装(解决时间残留问题)
* @param millisInFuture 总时长(毫秒)
* @param countDownInterval 倒计时间隔(毫秒)
* @param onTick 倒计时回调(剩余毫秒)
* @param onFinish 结束回调
*/
class SafeCountDownTimer(
private val millisInFuture: Long,
private val countDownInterval: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) {
// 独立Handler,避免与其他CountDownTimer复用消息队列
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
synchronized(this@SafeCountDownTimer) {
// 仅当timer未取消时处理消息,避免残留消息触发
if (!isCancelled) {
super.handleMessage(msg)
}
}
}
}
// 核心:标记是否已取消,避免残留消息触发
@Volatile
private var isCancelled = false
// 实际的CountDownTimer实例
private var timer: CountDownTimer? = null
/**
* 启动倒计时(启动前自动清空旧状态)
*/
fun start() {
// 第一步:强制取消旧实例+清空所有残留消息(核心!)
cancel()
// 第二步:重置取消标记
isCancelled = false
// 第三步:新建CountDownTimer实例,绑定独立Handler
timer = object : CountDownTimer(millisInFuture, countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
// 双重校验:仅未取消时触发回调
if (!isCancelled) {
onTick.invoke(millisUntilFinished)
}
}
override fun onFinish() {
if (!isCancelled) {
onFinish.invoke()
// 结束后主动清空,避免残留
cancel()
}
}
}
// 第四步:启动新实例
timer?.start()
Log.d("SafeCountDownTimer", "倒计时启动,总时长:$millisInFuture 毫秒")
}
/**
* 取消倒计时(彻底清空所有状态)
*/
fun cancel() {
// 标记为已取消,拦截后续消息
isCancelled = true
// 取消timer实例
timer?.cancel()
timer = null
// 清空Handler所有残留消息(核心:解决旧消息触发问题)
handler.removeCallbacksAndMessages(null)
Log.d("SafeCountDownTimer", "倒计时已取消,清空所有残留状态")
}
}
// ========== 封装成工具类,方便全局调用 ==========
object CountDownManager {
// 保存当前倒计时实例,避免重复启动
private var currentTimer: SafeCountDownTimer? = null
/**
* 启动/重启倒计时(适配人脸识别防抖场景)
* @param totalSeconds 总秒数(如15
* @param onTick 每秒回调剩余秒数
* @param onFinish 结束回调
*/
fun startCountDown(
totalSeconds: Int = 15,
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 先取消旧倒计时
cancelCountDown()
// 新建安全倒计时实例
currentTimer = SafeCountDownTimer(
millisInFuture = totalSeconds * 1000L,
countDownInterval = 1000L,
onTick = { millis ->
// 转换为秒数回调
val sec = (millis / 1000).toInt() + 1
onTick.invoke(sec)
},
onFinish = {
onFinish.invoke()
}
)
// 启动倒计时
currentTimer?.start()
}
/**
* 取消当前倒计时
*/
fun cancelCountDown() {
currentTimer?.cancel()
currentTimer = null
}
}
@@ -0,0 +1,88 @@
package com.sw.face.collect.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
/**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环)
*/
object CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
// 当前倒计时Job(用于取消)
private var currentJob: Job? = null
/**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行)
* @param totalSeconds 倒计时总秒数(如15
* @param lifecycleScope Activity/Fragment的lifecycleScope(必须)
* @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程)
*/
fun startCountDown(
total: Int = 15,
lifecycleScope: CoroutineScope,
onStart: () -> Unit = {},
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 1. 防重复:如果正在倒计时,先取消旧任务
if (isCounting.get()) {
//cancelCountDown()
return
}
// 2. 标记为正在倒计时
isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) {
emit(i)
delay(1000) // IO线程等待1秒,无死循环
}
}
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程
.onStart {
withContext(Dispatchers.Main) {
onStart.invoke()
}
}
.onEach { remaining ->
// 主线程回调剩余时间
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
}
}
.onCompletion {
// 倒计时结束:重置状态+回调
withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记
currentJob = null // 置空Job
Log.d("CountDownUtil", "倒计时正常结束")
onFinish.invoke()
}
}
.catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false)
currentJob = null
}
.launchIn(lifecycleScope)
}
/**
* 取消倒计时(手动停止)
*/
fun cancelCountDown() {
currentJob?.cancel() // 取消Job
currentJob = null // 置空,避免残留
isCounting.set(false) // 重置标记
Log.d("CountDownUtil", "倒计时被手动取消")
}
}
@@ -0,0 +1,122 @@
package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
//import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.to
/**
* 时间格式化工具类
*/
object DateTimeUtils {
/**
* 获取完整中文日期格式(示例:2025年6月11日 星期三)
*/
fun getChineseDateString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
}
/**
* 获取标准时间格式
*/
fun getDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
}
// 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题)
private val dateFormat by lazy {
SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA)
}
private val timeFormat by lazy {
SimpleDateFormat("HH:mm:ss", Locale.CHINA)
}
fun getChineseDateTimePair(date: Date = Date()): Pair<String, String> {
return dateFormat.format(date) to timeFormat.format(date)
}
// /**
// * 实时时间流(每秒更新)
// * @param intervalMillis 更新间隔(默认1秒)
// */
// fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
// while (true) {
// FlowCollector.emit(getChineseDateTimePair())
// delay(intervalMillis)
// }
// }
/**
* 解析日期时间字符串
* @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
* @return Date 对象,解析失败返回 null
*/
fun parseDateTime(timeString: String?): Date? {
return try {
if (timeString == null) return null
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
// e.printStackTrace()
// Timber.e(e.message)
null
}
}
/**
* 判断给定时间是否距离当前时间超过72小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过72小时,false 表示未超过
*/
fun isMoreThanHoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 72
}
/**
* 获取时间间隔描述
*/
fun getTimeAgo(date: Date?): String {
if (date == null) return "未知时间"
val now = Date()
val diffMillis = now.time - date.time
// 如果是未来时间
if (diffMillis < 0) {
val futureHours = abs(diffMillis) / (1000 * 60 * 60)
return if (futureHours < 24) {
"未来 $futureHours 小时"
} else {
val days = futureHours / 24
"未来 $days"
}
}
// 过去时间
val hours = diffMillis / (1000 * 60 * 60)
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
else -> {
val days = hours / 24
"${days}天前"
}
}
}
}
@@ -0,0 +1,28 @@
package com.sw.face.collect.utils
/**
* 防抖工具
*/
class Debouncer(private val delayMillis: Long = 2000) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
action()
return true
}
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}
@@ -0,0 +1,47 @@
package com.sw.face.collect.utils
import android.util.Log
import com.arcsoft.face.ErrorInfo
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
object FaceEngineUtils {
private val faceApi: FaceApi by lazy { FaceApi() }
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Log.d("FaceEngineUtils", "activeEngine activeCode = $activeCode")
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
})
}
}
@@ -0,0 +1,163 @@
package com.sw.face.collect.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
// import Timber.log.Timber
import java.io.File
import kotlin.io.copyTo
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.text.equals
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
//Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
//Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
//Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
return genRequestPart(file)
}
fun genRequestPart(file: File?): MultipartBody.Part? {
if (file == null) {
//Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
//Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
//Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
//Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
//Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
//Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
//Timber.e(e)
false
}
}
}
@@ -0,0 +1,40 @@
package com.sw.face.collect.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
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
import kotlinx.coroutines.withContext
// 倒计时Flow扩展函数
fun countDownByFlow(
total: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onStart: (() -> Unit)? = null,
onFinish: (() -> Unit)? = null
): Job {
require(total > 0) { "倒计时总秒数必须大于0" }
return flow {
for (i in total downTo 1) {
emit(i)
// if (i != 0)
delay(1000)
}
}
.flowOn(Dispatchers.IO)
// .flowOn(Dispatchers.Main)
.onStart { withContext(Dispatchers.Main) { onStart?.invoke() } }
.onCompletion {
withContext(Dispatchers.Main) { { onFinish?.invoke() } }
}
.onEach { withContext(Dispatchers.Main) { onTick.invoke(it) } }
.launchIn(CoroutineScope(scope.coroutineContext + Dispatchers.Main))
}
@@ -0,0 +1,125 @@
package com.sw.face.collect.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
import kotlin.jvm.java
import kotlin.text.isNullOrEmpty
object GsonUtils {
// 默认的 Gson 实例
private val defaultGson: Gson by lazy {
GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
// .disableHtmlEscaping() // 禁止转义HTML标签
.create()
}
/**
* 获取默认配置的 Gson 实例
*/
fun getGson(): Gson = defaultGson
/**
* 将对象转换为 JSON 字符串
* @param obj 要转换的对象
* @return JSON 字符串
*/
fun toJson(obj: Any?): String {
return if (obj == null) "" else defaultGson.toJson(obj)
}
/**
* 将 JSON 字符串转换为对象
* @param json JSON 字符串
* @param clazz 目标类
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, clazz: Class<T>): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为对象 (支持泛型)
* @param json JSON 字符串
* @param type 类型令牌,用于获取泛型类型
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, type: Type): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 List 对象
* @param json JSON 字符串
* @param clazz List 中的元素类型
* @return 转换后的 List 对象
*/
fun <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(List::class.java, clazz).type
defaultGson.fromJson<List<T>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 Map 对象
* @param json JSON 字符串
* @param keyClazz Map 的 key 类型
* @param valueClazz Map 的 value 类型
* @return 转换后的 Map 对象
*/
fun <K, V> fromJsonMap(
json: String?,
keyClazz: Class<K>,
valueClazz: Class<V>
): Map<K, V>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
defaultGson.fromJson<Map<K, V>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将对象转换为另一种类型的对象
* @param obj 源对象
* @param clazz 目标类型
* @return 转换后的对象
*/
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
if (obj == null) {
return null
}
return fromJson(toJson(obj), clazz)
}
}
@@ -0,0 +1,66 @@
package com.sw.face.collect.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
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,50 @@
package com.sw.face.collect.utils;
import androidx.annotation.RequiresPermission;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.LinkedList;
public class NetworkUtils {
@RequiresPermission("android.permission.INTERNET")
public static String getIPAddress(boolean useIPv4) {
try {
Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
LinkedList<InetAddress> adds = new LinkedList();
while(nis.hasMoreElements()) {
NetworkInterface ni = (NetworkInterface)nis.nextElement();
if (ni.isUp() && !ni.isLoopback()) {
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while(addresses.hasMoreElements()) {
adds.addFirst(addresses.nextElement());
}
}
}
for(InetAddress add : adds) {
if (!add.isLoopbackAddress()) {
String hostAddress = add.getHostAddress();
boolean isIPv4 = hostAddress.indexOf(58) < 0;
if (useIPv4) {
if (isIPv4) {
return hostAddress;
}
} else if (!isIPv4) {
int index = hostAddress.indexOf(37);
return index < 0 ? hostAddress.toUpperCase() : hostAddress.substring(0, index).toUpperCase();
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return "";
}
}
@@ -0,0 +1,159 @@
package com.sw.face.collect.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import kotlin.apply
import kotlin.collections.all
import kotlin.collections.any
import kotlin.collections.forEachIndexed
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
companion object {
/**
* 创建权限请求构建器
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
}
/**
* 检查是否已授予所有权限
*/
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
val permanentlyDeniedPermissions = mutableListOf<String>()
permissions.forEachIndexed { index, permission ->
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
permission
)
) {
permanentlyDeniedPermissions.add(permission)
}
}
}
return false
}
/**
* 跳转到应用设置页面
*/
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
class Builder(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
return this
}
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
/**
* 检查并请求权限
*/
fun checkAndRequest() {
if (checkPermissions(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
}
}
private fun requestPermissions() {
val activity = context as? Activity ?: return
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
.setPositiveButton("确定") { _, _ ->
doRequestPermissions(activity)
}
.setNegativeButton("取消", null)
.show()
} else {
doRequestPermissions(activity)
}
}
private fun doRequestPermissions(activity: Activity) {
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
}
@@ -0,0 +1,97 @@
package com.sw.face.collect.utils
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.core.content.ContextCompat
import java.io.File
/**
* 拍照工具类
* @param context Context 上下文
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
*/
class PhotoCaptureHelper(
private val context: Context,
private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {}
) {
companion object {
const val TAG = "PhotoCaptureHelper"
}
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback: (Uri) -> Unit) {
if (callbackList.contains(callback).not()) {
callbackList.add(callback)
}
}
private var bindCamera: (() -> Unit)? = null
fun bindCameraCallback(callback: () -> Unit) {
this.bindCamera = callback
}
/**
* 拍照方法
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
* @param fileExtension 文件扩展名,默认为".jpg"
*/
fun takePhoto(
fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg"
) {
Log.d(TAG, "开始拍照采集")
try {
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"${fileNamePrefix}${System.currentTimeMillis()}",
fileExtension,
cacheDir
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Log.d(TAG, "照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach {
it(photoUri)
}
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
if (errorMsg.contains("Not bound to a valid Camera")) {
if (bindCamera != null) {
bindCamera?.invoke()
//takePhoto()
}
}
Log.e(TAG, errorMsg)
onError(errorMsg)
}
}
)
} catch (e: Exception) {
val errorMsg = "创建临时文件失败: ${e.message}"
Log.e(TAG, errorMsg)
onError(errorMsg)
}
}
}
@@ -0,0 +1,15 @@
package com.sw.face.collect.utils
class RegisterCallbackHandler {
private var isCallbackCalled = false
private val lock = Any()
fun setCallback(callback: () -> Unit) {
synchronized(lock) {
if (!isCallbackCalled) {
isCallbackCalled = true
callback()
}
}
}
}
@@ -0,0 +1,114 @@
package com.sw.dualscreen.utils
import android.content.Context
import androidx.core.content.edit
import com.sw.plate.App
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* 持久化工具
*/
class SPUtil private constructor(context: Context, private val spName: String) {
companion object {
@Volatile
private var instance: SPUtil? = null
fun getInstance(
context: Context = App.getContext(),
spName: String = "default_sp"
): SPUtil {
return instance ?: synchronized(this) {
instance ?: SPUtil(context.applicationContext, spName).also { instance = it }
}
}
}
private val sharedPreferences by lazy {
context.getSharedPreferences(spName, Context.MODE_PRIVATE)
}
// 基础存储方法
fun put(key: String, value: Any?) {
when (value) {
null -> remove(key) // 存入null视为删除
is String -> sharedPreferences.edit { putString(key, value) }
is Int -> sharedPreferences.edit { putInt(key, value) }
is Long -> sharedPreferences.edit { putLong(key, value) }
is Float -> sharedPreferences.edit { putFloat(key, value) }
is Boolean -> sharedPreferences.edit { putBoolean(key, value) }
is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set<String>) }
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
}
notifyDataChanged(key)
}
@Suppress("UNCHECKED_CAST")
fun <T> get(key: String, defaultValue: T? = null): T? {
return when (defaultValue) {
is String -> sharedPreferences.getString(key, defaultValue) as T
is Int -> sharedPreferences.getInt(key, defaultValue) as T
is Long -> sharedPreferences.getLong(key, defaultValue) as T
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set<String>) as T
null -> when {
sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取
else -> null
}
else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}")
}
}
fun remove(key: String) {
if (sharedPreferences.contains(key)) {
sharedPreferences.edit { remove(key) }
notifyDataChanged(key)
}
}
fun clear() {
sharedPreferences.edit { clear() }
notifyDataChanged(null)
}
fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
// 监听变化
private val dataChangeFlow = MutableStateFlow(0)
private fun notifyDataChanged(key: String?) {
dataChangeFlow.value++
}
fun observeKey(key: String): Flow<Any?> {
return dataChangeFlow.map { get(key) }
}
// 属性委托支持
fun int(key: String, default: Int = 0) = SpProperty(key, default)
fun long(key: String, default: Long = 0L) = SpProperty(key, default)
fun float(key: String, default: Float = 0f) = SpProperty(key, default)
fun boolean(key: String, default: Boolean = false) = SpProperty(key, default)
fun string(key: String, default: String = "") = SpProperty(key, default)
fun stringSet(key: String, default: Set<String> = emptySet()) = SpProperty(key, default)
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return get(key, defaultValue) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
put(key, value)
}
}
}
@@ -0,0 +1,427 @@
package com.sw.face.collect.utils;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SoundPoolUtil {
private static final String TAG = SoundPoolUtil.class.getSimpleName();
private static SoundPoolUtil mSound;
private SoundPool mSoundPool;
private boolean isLoadC = false;
private Map<String, Integer> idCache;
private List<Integer> sidCache;
public SoundPoolUtil() {
idCache = new HashMap<>();
sidCache = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes aab = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(aab)
.build();
} else {
mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 8);
}
// mSoundPool = new SoundPool(60, AudioManager.USE_DEFAULT_STREAM_TYPE, 7);
mSoundPool.setOnLoadCompleteListener(new MyOnLoadCompleteListener());
}
public static SoundPoolUtil getInstance() {
synchronized (SoundPoolUtil.class) {
if (mSound == null) {
mSound = new SoundPoolUtil();
}
}
return mSound;
}
private int loadCompleteSize = 0;
private LoadCompletion completionListener;
public void setCompletionListener(LoadCompletion listener) {
this.completionListener = listener;
}
private class MyOnLoadCompleteListener implements SoundPool.OnLoadCompleteListener {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loadCompleteSize++;
//L.e("loadSize" + loadSize + "===" + loadCompleteSize);
if (loadSize == loadCompleteSize) {
isLoadC = true;
if (completionListener != null) {
completionListener.onCompletion();
}
}
}
}
/**
* 加载指定资源
*
* @param name
* @param path
*/
public void loadR(String name, String path) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(path, 1));
}
}
}
private int loadSize = 0;
/**
* 加载指定路径列表的资源
*
* @param map
*/
public void loadR(Map<String, String> map) {
loadSize = map.size();
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue(), 1));
}
}
}
}
/**
* 加载指定AssetFileDescriptor的资源
*
* @param name
* @param afd
*/
public void loadRF(String name, AssetFileDescriptor afd) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), 1));
}
}
}
/**
* 加载指定AssetFileDescriptor列表的资源
*
* @param map
*/
public void loadRF(Map<String, AssetFileDescriptor> map) {
Set<Map.Entry<String, AssetFileDescriptor>> entries = map.entrySet();
for (Map.Entry<String, AssetFileDescriptor> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue().getFileDescriptor(), entry.getValue().getStartOffset(), entry.getValue().getLength(), 1));
}
}
}
}
/**
* 加载指定列表资源
*
* @param context
* @param map
*/
public void loadR(Context context, Map<String, Integer> map) {
loadSize = map.size();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(context, entry.getValue(), 1));
}
}
}
}
/**
* 加载单个音频
*
* @param context
* @param name
* @param res
*/
public void loadR(Context context, String name, int res) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(context, res, 1));
}
}
}
/**
* 播放指定音频,并返用于停止、暂停、恢复的StreamId
*
* @param name
* @param times
* @return
*/
public int play(String name, int times) {
//L.e(String.format("play %s times=%s", name, times));
return this.play(name, 1, 1, 1, times, 1);
}
/**
* 播放指定音频,并指定播放次数和频率
*
* @param name
* @param times
* @param rate
* @return
*/
public int play(String name, int times, int rate) {
return this.play(name, 1, 1, 1, times, rate);
}
/**
* 播放指定音频,并指定优先级和播放频率
*
* @param name
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, int property, int times, int rate) {
return this.play(name, 1, 1, property, times, rate);
}
/**
* 播放指定音频,并指定左右声道、优先级、播放次数、播放频率
*
* @param name
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, float leftVolume, float rightVolume, int property, int times, int rate) {
int streamId = -1;
if (checkSoundPool()) {
//L.d(TAG, "play: " + name);
if (idCache.containsKey(name) && isLoadC) {
//L.d(TAG, "name:" + idCache.get(name));
streamId = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
//L.d(TAG, "streadmId:" + streamId);
sidCache.add(streamId);
}
}
return streamId;
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表
*
* @param names
* @param times
* @return
*/
public List<Integer> play(List<String> names, int times) {
return this.play(names, 1, 1, 1, times, 1);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定次数和频率
*
* @param names
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int times, int rate) {
return this.play(names, 1, 1, 1, times, rate);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定所有参数
*
* @param names
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int leftVolume, int rightVolume, int property, int times, int rate) {
List<Integer> streamIds = new ArrayList<>();
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name) && isLoadC) {
int a = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
streamIds.add(a);
sidCache.add(a);
}
}
}
return streamIds;
}
/**
* 停止指定id音频
*/
public void stop(int r) {
if (checkSoundPool()) {
mSoundPool.stop(r);
}
}
/**
* 停止指定列表音频
*/
public void stopAll() {
if (checkSoundPool()) {
for (int r : sidCache) {
mSoundPool.stop(r);
}
}
}
/**
* 暂停指定音效
*
* @param r
*/
public void pause(int r) {
if (checkSoundPool()) {
mSoundPool.pause(r);
}
}
/**
* 暂停指定列表音频
*
* @param list
*/
public void pause(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.pause(r);
}
}
}
/**
* 暂停所有音效
*/
public void pauseAll() {
mSoundPool.autoPause();
}
/**
* 恢复指定音频播放
*
* @param r
*/
public void resume(int r) {
if (checkSoundPool()) {
mSoundPool.resume(r);
}
}
/**
* 恢复指定列表的音频
*
* @param list
*/
public void resume(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.resume(r);
}
}
}
/**
* 恢复所有暂停的音频
*/
public void resumeAll() {
if (checkSoundPool()) {
mSoundPool.autoResume();
}
}
/**
* 卸载指定音频
*
* @param name
*/
public void unLoad(String name) {
if (checkSoundPool()) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
/**
* 卸载指定列表的音频
*
* @param names
*/
public void unLoad(List<String> names) {
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
}
/**
* 释放所有资源,如果想继续播放,需要重新加载资源
*/
public void release() {
if (checkSoundPool()) {
mSound = null;
mSoundPool.release();
idCache.clear();
}
}
private boolean checkSoundPool() {
if (mSoundPool != null) {
return true;
}
return false;
}
public interface LoadCompletion {
void onCompletion();
}
}
@@ -0,0 +1,20 @@
package com.sw.face.collect.utils;
import com.sw.dualscreen.utils.SPUtil;
import com.sw.face.collect.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,117 @@
package com.sw.face.collect.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.sw.face.collect.MyApp;
import com.sw.face.collect.R;
/**
* The type Toast utils.
*/
public class ToastUtils {
private static Toast toast;
@SuppressLint("StaticFieldLeak")
private static TextView textCenterView;
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text, int duration) {
if (TextUtils.isEmpty(text)) {
return;
}
runOnMainThread(()->{
Context context = MyApp.Companion.getInstance();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 20);
toast.setDuration(duration);
toast.setView(view);
}
textCenterView.setText(text);
toast.show();
});
}
public static void runOnMainThread(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text) {
showToast(text, Toast.LENGTH_SHORT);
// if (snackbar == null) {
// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
// textCenterView = view.findViewById(R.id.toast_tv);
// snackbar = Snackbar.make(view, "", Snackbar.LENGTH_SHORT);
//
// }
// textCenterView.setText(text);
// snackbar.setDuration(Snackbar.LENGTH_SHORT);
// snackbar.show();
}
// private static Snackbar snackbar = null;
//
// public static void showLongToast(String text) {
// runOnMainThread(()-> {
// Context context = App.Companion.getInstance();
// if (toast == null) {
// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
// textCenterView = view.findViewById(R.id.toast_tv);
// toast = new Toast(context);
// toast.setGravity(Gravity.TOP, 0, 20);
// toast.setDuration(Toast.LENGTH_LONG);
// toast.setView(view);
// }
// textCenterView.setText(text);
// showMyToast(toast, 1000000 * 30);
// });
// }
//
//
// //自定义Toast控件
// private static void showMyToast(final Toast toast, final int cnt) {
// final Timer timer = new Timer();
// timer.schedule(new TimerTask() {
// @Override
// public void run() {
// toast.show();
// }
// }, 0, Toast.LENGTH_LONG);
// new Timer().schedule(new TimerTask() {
// @Override
// public void run() {
// toast.cancel();
// timer.cancel();
// }
// }, cnt);
// }
//
// public static void showSystemToast(String text) {
// Context context = App.Companion.getInstance();
// Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
// }
}
@@ -0,0 +1,101 @@
package com.sw.face.collect.view;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import com.sw.face.collect.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();
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
// 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);
// }
}
@@ -0,0 +1,22 @@
package com.sw.face.collect.view
import android.util.Log
import com.sw.face.collect.socket.LanCommunicationManager
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,34 @@
package com.sw.face.collect.view
import android.util.Log
import com.sw.face.collect.socket.TcpClient
import org.json.JSONObject
import java.lang.Exception
import kotlin.math.log
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,75 @@
package com.sw.face.collect.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.arcsoft.face.FaceInfo
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
class FaceRecognizeViewModel: RecognizeViewModel() {
// 2. 新增:识别事件触发标记(即使userId不变,也能通知)
private val _recognitionTriggered = MutableLiveData<RecognitionResult?>()
val recognitionTriggered: LiveData<RecognitionResult?> = _recognitionTriggered
// 辅助:存储当前锁定的trackId(防止重复识别)
private var lockedTrackId: Int? = null
// 防抖时间(可配置,比如3秒)
private val ANTI_SHAKE_TIME = 3000L
// 存储最后一次识别时间,用于防抖
private var lastRecognitionTime = 0L
// 识别结果数据类:包含userId+是否新触发
data class RecognitionResult(
val userId: String,
val isNewRecognition: Boolean = true
)
/**
* 核心:处理识别结果(强制触发通知)
*/
fun handleRecognitionSuccess(faceInfo: FaceInfo, userId: String) {
val currentTime = System.currentTimeMillis()
// val currentTrackId = faceInfo.trackId
val currentTrackId = faceInfo.faceId
// 1. 防抖判断:同一人脸3秒内不重复触发(可根据需求调整)
if (currentTrackId == lockedTrackId && currentTime - lastRecognitionTime < ANTI_SHAKE_TIME) {
// 可选:即使防抖,也强制通知(仅更新事件,不重复执行业务)
_recognitionTriggered.postValue(RecognitionResult(userId, isNewRecognition = false))
return
}
// 2. 重置锁定状态
lockedTrackId = currentTrackId
lastRecognitionTime = currentTime
// 3. 更新userId(触发原有observe
val entity = FaceEntity(userId, null, faceInfo.faceData)
recognizeUserId.postValue(CompareResult(entity, 9.0f))
// 4. 强制触发识别事件通知(核心:即使userId不变,也通知)
_recognitionTriggered.postValue(RecognitionResult(userId))
}
/**
* 重置识别状态(人脸离开/重新进屏时调用)
*/
fun resetRecognitionState() {
lockedTrackId = null
lastRecognitionTime = 0L
// 清空LiveData,为下次识别做准备
recognizeUserId.postValue(null)
_recognitionTriggered.postValue(null)
}
/**
* 手动触发重新识别(比如按钮点击)
*/
fun triggerReRecognition() {
resetRecognitionState()
// 可选:重置虹软SDK跟踪上下文
//faceEngine?.faceEngineReset()
}
}
@@ -0,0 +1,167 @@
package com.sw.face.collect.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.face.collect.network.ApiClient
import com.sw.dualscreen.utils.SPUtil
import com.sw.face.collect.base.GlobalKey
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import com.sw.face.collect.repository.RemoteRepository
import com.sw.face.collect.utils.SpTool
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class MainViewModel() : ViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
protected val repository = RemoteRepository(ApiClient.apiService)
private var lastFaceTimestamp = 0L
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
val code = response.code
if (code == "00000" || code == "200" || code == "0") {
return true
}
val message = response.msg
// Timber.d("msg = ${message}, code = $code")
ToastUtils.showToast("${message}(${code})")
return false
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) {
var currentPageNo = pageNo
// Timber.tag(TAG).d("getUserFaceCache index = $currentPageNo")
viewModelScope.launch {
//获取全量数据时,时间戳改为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.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
ToastUtils.showToast("查询人脸数据为空")
return@withContext
}
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)
}
}
}
}
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
onAllQueryFinished: () -> Unit,
onPageQueryFinished: (List<UserFaceModel>) -> Unit
) {
viewModelScope.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()
onPageQueryFinished(list)
if (pageNo == 1 && list.isEmpty()) {
return@withContext
}
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 getDinnerType(block: (DinnerType) -> Unit) {
viewModelScope.launch {
val response = repository.getDinnerType()
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
response.data?.let {
block(it)
}
}
}
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
viewModelScope.launch {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data?:""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
val status = parseResponse(faceResp)
if (status.not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="1920dp"
android:height="1080dp"
android:drawable="@drawable/bg_collect_page3" />
<!-- <item-->
<!-- android:width="1920dp"-->
<!-- android:height="270dp">-->
<!-- <shape android:shape="rectangle">-->
<!-- <solid android:color="#D00D269F" />-->
<!-- </shape>-->
<!-- </item>-->
<!-- <item-->
<!-- android:width="1920dp"-->
<!-- android:height="810dp"-->
<!-- android:top="270dp">-->
<!-- <shape android:shape="rectangle">-->
<!-- <gradient-->
<!-- android:angle="270"-->
<!-- android:endColor="#99407ACA"-->
<!-- android:startColor="#D00D269F"-->
<!-- android:type="linear"-->
<!-- android:useLevel="false" />-->
<!-- </shape>-->
<!-- </item>-->
</layer-list>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="1920dp" android:height="1080dp">
<shape android:shape="rectangle">
<solid android:color="#0D269F" />
</shape>
</item>
<item
android:width="450dp"
android:height="430dp"
android:gravity="center_horizontal"
android:top="180dp">
<bitmap android:src="@drawable/img_init" android:scaleType="fitCenter"/>
</item>
</layer-list>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="50dp"/>
<solid android:color="#FFFFF0F0"/>
<stroke android:color="#FFFF3232" android:width="2dp"/>
</shape>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="1920dp"
android:height="1080dp"
android:drawable="@drawable/bg_collect_page" />
<!-- <item-->
<!-- android:width="1920dp"-->
<!-- android:height="1080dp"-->
<!-- android:top="160dp">-->
<!-- <shape android:shape="rectangle">-->
<!-- <gradient-->
<!-- android:angle="-90"-->
<!-- android:centerColor="#FF2E38B3"-->
<!-- android:endColor="#FF4482CD"-->
<!-- android:startColor="#660C269F"-->
<!-- android:type="linear"/>-->
<!-- </shape>-->
<!-- </item>-->
</layer-list>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/black" />
</shape>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/ivFlag"
android:layout_width="453dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="180dp"
android:scaleType="fitCenter"
android:src="@drawable/img_init"
android:visibility="invisible"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnStart"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="点击取盘"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
+229
View File
@@ -0,0 +1,229 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视摄像头,保持面部居中于取景框内"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvToInit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="60sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<LinearLayout
android:id="@+id/llCollectButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="100dp"
android:visibility="gone">
<TextView
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="再次重试"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/bg_collect_btn"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/btnCollect"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="新用户采集"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/bg_collect_btn"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<TextView
android:id="@+id/btnClear"
android:layout_width="160dp"
android:layout_height="80dp"
android:text="清除数据"
android:textSize="26sp"
android:gravity="center"
android:textColor="@color/white"
android:layout_gravity="start|bottom"/>
</FrameLayout>
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:layout_marginTop="20dp"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视屏幕"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvToInit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="人脸信息采集"
android:textColor="#FF0A1428"
android:textSize="40sp"
android:textStyle="bold"
android:layout_marginVertical="50dp"/>
<FrameLayout
android:id="@+id/flPreview"
android:layout_width="680dp"
android:layout_height="500dp">
<androidx.cardview.widget.CardView
android:id="@+id/flCameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="8dp"
app:cardElevation="0dp" />
<LinearLayout
android:id="@+id/llCameraFlag"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:background="@color/white"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:layout_width="108dp"
android:layout_height="108dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/ic_camera256"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="人脸采集"
android:textColor="#ffb4b4c8"
android:textSize="26sp" />
</LinearLayout>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivFaceImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:shapeAppearance="@style/round10dpCornerStyle"
tools:src="@mipmap/ic_launcher"
android:visibility="gone"/>
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingVertical="10dp"
android:layout_marginTop="50dp">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnClear"
android:layout_width="200dp"
android:layout_height="80dp"
android:background="@drawable/bg_take_photo"
android:text="清除"
android:textColor="#FFFF3232"
android:textSize="36sp"
android:textStyle="normal" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnTakePhoto"
android:layout_width="200dp"
android:layout_height="80dp"
android:background="@drawable/bg_take_photo"
android:text="拍照"
android:textColor="#FFFF3232"
android:textSize="36sp"
android:layout_marginHorizontal="33dp"
android:textStyle="normal" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnUpload"
android:layout_width="180dp"
android:layout_height="80dp"
android:background="@drawable/bg_take_photo"
android:text="上传"
android:textColor="#FFFF3232"
android:textSize="36sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视摄像头,保持面部居中于取景框内"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视摄像头,保持面部居中于取景框内"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvCountDown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="60sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/dialog_waiting"
android:gravity="center_vertical"
android:paddingHorizontal="20dp"
android:paddingVertical="15dp">
<ProgressBar
android:layout_width="39dp"
android:layout_height="39dp"
android:indeterminateTint="@color/teal_200"
android:indeterminateTintMode="src_atop"
android:focusable="false"/>
<TextView
android:id="@+id/tvTip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请稍后……"
android:gravity="center_vertical"
android:layout_marginStart="10dp"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="680dp"
android:layout_height="500dp"
android:layout_gravity="center"/>
</merge>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- <FrameLayout-->
<!-- android:id="@+id/flFace"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:layout_gravity="center_horizontal"-->
<!-- android:visibility="visible">-->
<!-- <TextureView-->
<!-- android:id="@+id/dual_camera_texture_preview_rgb"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:visibility="visible" />-->
<!-- <com.sw.plate.utils.arcface.FaceRectView-->
<!-- android:id="@+id/dual_camera_face_rect_view"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent" />-->
<!-- <ImageView-->
<!-- android:id="@+id/ivFaceRecMask"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="bottom"-->
<!-- android:adjustViewBounds="true"-->
<!-- android:src="@drawable/face_mask"-->
<!-- android:visibility="gone" />-->
<!-- <TextView-->
<!-- android:id="@+id/tvFaceTip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="bottom|center"-->
<!-- android:layout_marginBottom="56dp"-->
<!-- android:text="请正视屏幕进行人脸采集..."-->
<!-- android:textColor="@color/white"-->
<!-- android:textSize="22sp"-->
<!-- android:visibility="gone" />-->
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:layout_margin="28dp"-->
<!-- android:background="@drawable/ic_camera_rect" />-->
<!-- </FrameLayout>-->
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivRecImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:shapeAppearance="@style/round10dpCornerStyle"
tools:src="@mipmap/ic_launcher"
android:visibility="gone"/>
</FrameLayout>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="#000000"
app:cardPreventCornerOverlap="true">
<TextView
android:id="@+id/toast_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:textColor="#ffffff"
android:textSize="36sp"
tools:text="TextView" />
</androidx.cardview.widget.CardView>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.
Binary file not shown.

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