拉取全量和增量人脸数据,增加已采集数据提示

This commit is contained in:
2025-12-29 14:29:30 +08:00
parent a179c52591
commit b5daf0f5df
18 changed files with 805 additions and 14 deletions
+11 -1
View File
@@ -68,5 +68,15 @@ dependencies {
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(project(":lib_face")) implementation(project(":lib_face"))
implementation("com.github.Dimezis:BlurView:version-3.2.0") // 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)
} }
@@ -22,15 +22,18 @@ import com.sw.face.collect.ext.dp
import com.sw.face.collect.ext.gone import com.sw.face.collect.ext.gone
import com.sw.face.collect.ext.toast import com.sw.face.collect.ext.toast
import com.sw.face.collect.ext.visible 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.LanServer
import com.sw.face.collect.socket.TcpClient import com.sw.face.collect.socket.TcpClient
import com.sw.face.collect.utils.Base64 import com.sw.face.collect.utils.Base64
import com.sw.face.collect.utils.BitmapUtils import com.sw.face.collect.utils.BitmapUtils
import com.sw.face.collect.utils.FaceEngineUtils import com.sw.face.collect.utils.FaceEngineUtils
import com.sw.face.collect.utils.IntervalExecutor 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.utils.countDownByFlow
import com.sw.face.collect.view.LanServerListenerImpl import com.sw.face.collect.view.LanServerListenerImpl
import com.sw.face.collect.view.TcpClientListenerImpl 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.L
import com.sw.plate.utils.NV21ToBitmap import com.sw.plate.utils.NV21ToBitmap
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
@@ -46,12 +49,15 @@ 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.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration 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.model.UserFaceInfo
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.json.JSONObject import org.json.JSONObject
import kotlin.getValue
import kotlin.system.exitProcess import kotlin.system.exitProcess
class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlobalLayoutListener { class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlobalLayoutListener {
@@ -79,6 +85,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private val recognizeViewModel: RecognizeViewModel by lazy { viewModels<RecognizeViewModel>().value } private val recognizeViewModel: RecognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
private val mainViewModel by viewModels<MainViewModel>()
private var currentUserId: String? = null
override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater) override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
override fun onGlobalLayout() { override fun onGlobalLayout() {
@@ -87,8 +97,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
openCamera() openCamera()
} }
private var pageNo = 1
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
mainViewModel.getUserFaceCache(pageNo = pageNo)
FaceEngineUtils.activeEngine() FaceEngineUtils.activeEngine()
setupArcCamera() setupArcCamera()
binding.btnCollectFace.setOnClickListener { binding.btnCollectFace.setOnClickListener {
@@ -110,6 +122,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
binding.layoutState.gone() binding.layoutState.gone()
resumeCamera() resumeCamera()
startFaceTask()
} }
@@ -206,6 +219,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
// private var imageBitmap: Bitmap? = null // private var imageBitmap: Bitmap? = null
private fun collectFace() { private fun collectFace() {
currentUserId = null
userFaceInfo = null userFaceInfo = null
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY) recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
//showWaitingDialog("人脸信息中……") //showWaitingDialog("人脸信息中……")
@@ -234,6 +248,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private fun getFaceData(block: (String) -> Unit) { private fun getFaceData(block: (String) -> Unit) {
runBlocking { runBlocking {
job = executor.startIntervalTask(5) { job = executor.startIntervalTask(5) {
if (currentUserId != null && currentUserId!!.isNotBlank()) {
toast("您已采集过人脸信息")
return@startIntervalTask
}
if (userFaceInfo == null) { if (userFaceInfo == null) {
return@startIntervalTask return@startIntervalTask
} }
@@ -335,11 +353,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
TAG, TAG,
"recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}" "recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
) )
// val lastFaceTrackId = compareResult.trackId val lastFaceTrackId = compareResult.trackId
// val faceEntity = compareResult.faceEntity val faceEntity = compareResult.faceEntity
// val currentUserId = faceEntity.userName currentUserId = faceEntity.userName
// if (!currentUserId.isNullOrBlank()) { // if (!currentUserId.isNullOrBlank()) {
// toast("当前用户已采集过人脸信息") //// toast("已采集过人脸信息")
// return@Observer // return@Observer
// } // }
// //未识别到,拍摄照片 // //未识别到,拍摄照片
@@ -711,6 +729,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
override fun onDestroy() { override fun onDestroy() {
countDownJob?.cancel() countDownJob?.cancel()
faceTaskJob?.cancel()
super.onDestroy() super.onDestroy()
} }
@@ -727,7 +746,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
}, },
onFinish = { onFinish = {
//binding.tvCollectState.text = "采集完成" //binding.tvCollectState.text = "采集完成"
currentUserId = null
binding.layoutState.gone() binding.layoutState.gone()
binding.tvFaceTip.visible() binding.tvFaceTip.visible()
binding.btnCollectFace.visible() binding.btnCollectFace.visible()
@@ -735,4 +754,72 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
) )
} }
private val intervalExecutor by lazy { IntervalExecutor() }
private var faceTaskJob: Job? = null
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 60 * 1000L
private val dealyMillis = 30 * 1000L
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return@startIntervalTaskWithInitialDelay
}
mainViewModel.getFaceIncrementList(
pageNo = taskPageNo,
timestamp = timestamp,
onAllQueryFinished = {
taskPageNo = 1
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
},
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()
}
} }
@@ -11,6 +11,7 @@ class MyApp: App() {
companion object { companion object {
private const val TAG = "MyApp" private const val TAG = "MyApp"
var DEBUG = true
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
var instance: Context? = null var instance: Context? = null
} }
@@ -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,43 @@
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 faceDeleted: Boolean? = false
)
// : 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
)
@@ -0,0 +1,46 @@
package com.sw.dualscreen.network
import android.util.Log
import com.sw.face.collect.network.api.ApiService
import com.sw.dualscreen.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 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,36 @@
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.UserFaceModel
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
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>?>
}
@@ -0,0 +1,41 @@
package com.sw.dualscreen.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.utils.SPUtil
import com.sw.plate.App
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,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,50 @@
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.UserFaceModel
import com.sw.face.collect.network.api.ApiService
/**
* 远程数据处理
*/
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
)
)
}
}
}
@@ -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,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,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,108 @@
package com.sw.face.collect.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.dualscreen.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.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
class MainViewModel() : ViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
protected val repository = RemoteRepository(ApiClient.apiService)
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 {
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 (list.isEmpty()) {
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
ToastUtils.showToast("查询人脸数据为空")
return@withContext
}
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr))
}
faceApi.updateFaceData(currentPageNo, faceEntity)
if (list.size >= pageSize) {
currentPageNo++
getUserFaceCache(currentPageNo)
} else {
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
}
}
}
}
}
/**
* 获取人脸数据
*/
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 (list.size >= pageSize) {
getFaceIncrementList(
pageNo = pageNo + 1,
pageSize = pageSize,
timestamp = timestamp,
onAllQueryFinished = onAllQueryFinished,
onPageQueryFinished = onPageQueryFinished
)
} else {
onAllQueryFinished()
}
}
}
}
}
}
+8
View File
@@ -14,6 +14,9 @@ cameraCore = "1.3.0"
kotlinxCoroutinesAndroid = "1.6.4" kotlinxCoroutinesAndroid = "1.6.4"
lifecycleViewmodelKtx = "2.8.3" lifecycleViewmodelKtx = "2.8.3"
lifecycleRuntimeKtx = "2.8.3" lifecycleRuntimeKtx = "2.8.3"
retrofit = "3.0.0"
okhttp = "4.12.0"
gson = "2.13.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -36,6 +39,11 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
+1 -1
View File
@@ -56,7 +56,7 @@ dependencies {
// implementation("com.licheedev:android-serialport:2.1.5") // implementation("com.licheedev:android-serialport:2.1.5")
val roomVersion = "2.2.5" val roomVersion = "2.2.5"
implementation("androidx.room:room-runtime:$roomVersion") api("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion") annotationProcessor("androidx.room:room-compiler:$roomVersion")
implementation("io.reactivex.rxjava2:rxandroid:2.0.1") implementation("io.reactivex.rxjava2:rxandroid:2.0.1")
@@ -2,9 +2,11 @@ package com.sw.plate.utils.arcface.facedb.entity;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.TextUtils;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import java.util.Arrays; import java.util.Arrays;
@@ -42,7 +44,14 @@ public class FaceEntity implements Parcelable {
*/ */
@ColumnInfo(name = "register_time") @ColumnInfo(name = "register_time")
private long registerTime; private long registerTime;
/**
* 用户类型:1-普通会员、2-临时用户、3-内部员工、或者其它待定类型
*/
@ColumnInfo(name = "user_type")
private String userType;
@Ignore
private int trackId;//人脸追踪ID
public FaceEntity(String userName, String imagePath, byte[] featureData) { public FaceEntity(String userName, String imagePath, byte[] featureData) {
this.userName = userName; this.userName = userName;
@@ -120,6 +129,22 @@ public class FaceEntity implements Parcelable {
this.registerTime = registerTime; this.registerTime = registerTime;
} }
public int getTrackId() {
return trackId;
}
public void setTrackId(int trackId) {
this.trackId = trackId;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
@@ -132,8 +157,11 @@ public class FaceEntity implements Parcelable {
dest.writeString(userName); dest.writeString(userName);
dest.writeString(imagePath); dest.writeString(imagePath);
dest.writeByteArray(featureData); dest.writeByteArray(featureData);
dest.writeString(userType);
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {
@@ -143,16 +171,17 @@ public class FaceEntity implements Parcelable {
return false; return false;
} }
FaceEntity that = (FaceEntity) o; FaceEntity that = (FaceEntity) o;
return faceId == that.faceId && return this.faceId == that.faceId &&
registerTime == that.registerTime && this.registerTime == that.registerTime &&
userName.equals(that.userName) && TextUtils.equals(this.userName, that.userName) &&
imagePath.equals(that.imagePath) && TextUtils.equals(this.imagePath, that.imagePath) &&
Arrays.equals(featureData, that.featureData); Arrays.equals(featureData, that.featureData) &&
TextUtils.equals(this.userType, that.userType);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath); int result = Objects.hash(faceId, registerTime, userName, imagePath, userType);
result = 31 * result + Arrays.hashCode(featureData); result = 31 * result + Arrays.hashCode(featureData);
return result; return result;
} }