拉取全量和增量人脸数据,增加已采集数据提示
This commit is contained in:
+11
-1
@@ -68,5 +68,15 @@ dependencies {
|
||||
implementation(libs.accompanist.permissions)
|
||||
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.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
|
||||
@@ -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.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 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 mainViewModel by viewModels<MainViewModel>()
|
||||
|
||||
private var currentUserId: String? = null
|
||||
|
||||
override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
@@ -87,8 +97,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
openCamera()
|
||||
}
|
||||
|
||||
private var pageNo = 1
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
mainViewModel.getUserFaceCache(pageNo = pageNo)
|
||||
FaceEngineUtils.activeEngine()
|
||||
setupArcCamera()
|
||||
binding.btnCollectFace.setOnClickListener {
|
||||
@@ -110,6 +122,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
binding.layoutState.gone()
|
||||
|
||||
resumeCamera()
|
||||
startFaceTask()
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +219,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
|
||||
// private var imageBitmap: Bitmap? = null
|
||||
private fun collectFace() {
|
||||
currentUserId = null
|
||||
userFaceInfo = null
|
||||
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
|
||||
//showWaitingDialog("人脸信息中……")
|
||||
@@ -229,11 +243,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
}
|
||||
}
|
||||
|
||||
private var job: Job?=null
|
||||
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
|
||||
}
|
||||
@@ -335,11 +353,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
TAG,
|
||||
"recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
|
||||
)
|
||||
// val lastFaceTrackId = compareResult.trackId
|
||||
// val faceEntity = compareResult.faceEntity
|
||||
// val currentUserId = faceEntity.userName
|
||||
val lastFaceTrackId = compareResult.trackId
|
||||
val faceEntity = compareResult.faceEntity
|
||||
currentUserId = faceEntity.userName
|
||||
// if (!currentUserId.isNullOrBlank()) {
|
||||
// toast("当前用户已采集过人脸信息")
|
||||
//// toast("您已采集过人脸信息")
|
||||
// return@Observer
|
||||
// }
|
||||
// //未识别到,拍摄照片
|
||||
@@ -711,6 +729,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
|
||||
override fun onDestroy() {
|
||||
countDownJob?.cancel()
|
||||
faceTaskJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -727,7 +746,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
},
|
||||
onFinish = {
|
||||
//binding.tvCollectState.text = "采集完成"
|
||||
|
||||
currentUserId = null
|
||||
binding.layoutState.gone()
|
||||
binding.tvFaceTip.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 {
|
||||
private const val TAG = "MyApp"
|
||||
var DEBUG = true
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,6 +14,9 @@ cameraCore = "1.3.0"
|
||||
kotlinxCoroutinesAndroid = "1.6.4"
|
||||
lifecycleViewmodelKtx = "2.8.3"
|
||||
lifecycleRuntimeKtx = "2.8.3"
|
||||
retrofit = "3.0.0"
|
||||
okhttp = "4.12.0"
|
||||
gson = "2.13.1"
|
||||
|
||||
[libraries]
|
||||
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-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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -56,7 +56,7 @@ dependencies {
|
||||
// implementation("com.licheedev:android-serialport:2.1.5")
|
||||
|
||||
val roomVersion = "2.2.5"
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
api("androidx.room:room-runtime:$roomVersion")
|
||||
annotationProcessor("androidx.room:room-compiler:$roomVersion")
|
||||
|
||||
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.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -42,7 +44,14 @@ public class FaceEntity implements Parcelable {
|
||||
*/
|
||||
@ColumnInfo(name = "register_time")
|
||||
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) {
|
||||
this.userName = userName;
|
||||
@@ -120,6 +129,22 @@ public class FaceEntity implements Parcelable {
|
||||
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
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
@@ -132,8 +157,11 @@ public class FaceEntity implements Parcelable {
|
||||
dest.writeString(userName);
|
||||
dest.writeString(imagePath);
|
||||
dest.writeByteArray(featureData);
|
||||
dest.writeString(userType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -143,16 +171,17 @@ public class FaceEntity implements Parcelable {
|
||||
return false;
|
||||
}
|
||||
FaceEntity that = (FaceEntity) o;
|
||||
return faceId == that.faceId &&
|
||||
registerTime == that.registerTime &&
|
||||
userName.equals(that.userName) &&
|
||||
imagePath.equals(that.imagePath) &&
|
||||
Arrays.equals(featureData, that.featureData);
|
||||
return this.faceId == that.faceId &&
|
||||
this.registerTime == that.registerTime &&
|
||||
TextUtils.equals(this.userName, that.userName) &&
|
||||
TextUtils.equals(this.imagePath, that.imagePath) &&
|
||||
Arrays.equals(featureData, that.featureData) &&
|
||||
TextUtils.equals(this.userType, that.userType);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user