diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ce135b..5093907 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/MainActivity.kt b/app/src/main/java/com/sw/face/collect/MainActivity.kt index 26549fa..a7c16e1 100644 --- a/app/src/main/java/com/sw/face/collect/MainActivity.kt +++ b/app/src/main/java/com/sw/face/collect/MainActivity.kt @@ -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(), ViewTreeObserver.OnGlobalLayoutListener { @@ -79,6 +85,10 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo private val recognizeViewModel: RecognizeViewModel by lazy { viewModels().value } + private val mainViewModel by viewModels() + + private var currentUserId: String? = null + override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater) override fun onGlobalLayout() { @@ -87,8 +97,10 @@ class MainActivity : BaseActivity(), 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(), ViewTreeObserver.OnGlo binding.layoutState.gone() resumeCamera() + startFaceTask() } @@ -206,6 +219,7 @@ class MainActivity : BaseActivity(), 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(), 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(), 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(), ViewTreeObserver.OnGlo override fun onDestroy() { countDownJob?.cancel() + faceTaskJob?.cancel() super.onDestroy() } @@ -727,7 +746,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo }, onFinish = { //binding.tvCollectState.text = "采集完成" - + currentUserId = null binding.layoutState.gone() binding.tvFaceTip.visible() binding.btnCollectFace.visible() @@ -735,4 +754,72 @@ class MainActivity : BaseActivity(), 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) { + Thread { + val faceList = mutableListOf() + 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() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/MyApp.kt b/app/src/main/java/com/sw/face/collect/MyApp.kt index 66a9d00..0ea5fca 100644 --- a/app/src/main/java/com/sw/face/collect/MyApp.kt +++ b/app/src/main/java/com/sw/face/collect/MyApp.kt @@ -11,6 +11,7 @@ class MyApp: App() { companion object { private const val TAG = "MyApp" + var DEBUG = true @SuppressLint("StaticFieldLeak") var instance: Context? = null } diff --git a/app/src/main/java/com/sw/face/collect/model/ApiResponse.kt b/app/src/main/java/com/sw/face/collect/model/ApiResponse.kt new file mode 100644 index 0000000..bb2c7b5 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/model/ApiResponse.kt @@ -0,0 +1,10 @@ +package com.sw.face.collect.model + +data class ApiResponse( + val code: String, + val msg: String? = "", + val data: T? = null, + val result: T? = null, +) + + diff --git a/app/src/main/java/com/sw/face/collect/model/DataBean.kt b/app/src/main/java/com/sw/face/collect/model/DataBean.kt new file mode 100644 index 0000000..56a5988 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/model/DataBean.kt @@ -0,0 +1,6 @@ +package com.sw.face.collect.model + +data class RespCodeMsg( + val code: String?, + val msg: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/model/UserFaceModel.kt b/app/src/main/java/com/sw/face/collect/model/UserFaceModel.kt new file mode 100644 index 0000000..20626d9 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/model/UserFaceModel.kt @@ -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? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/network/ApiClient.kt b/app/src/main/java/com/sw/face/collect/network/ApiClient.kt new file mode 100644 index 0000000..39704cf --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/network/ApiClient.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/network/api/ApiService.kt b/app/src/main/java/com/sw/face/collect/network/api/ApiService.kt new file mode 100644 index 0000000..5051533 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/network/api/ApiService.kt @@ -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 + ): ApiResponse?> + + /** + * 获取人脸增量数据 + */ + @POST + suspend fun getFaceIncrementList( + @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list", + @Body param: Map + ): ApiResponse?> +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/network/interceptor/RequestInterceptor.kt b/app/src/main/java/com/sw/face/collect/network/interceptor/RequestInterceptor.kt new file mode 100644 index 0000000..f915d87 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/network/interceptor/RequestInterceptor.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/repository/BaseRepository.kt b/app/src/main/java/com/sw/face/collect/repository/BaseRepository.kt new file mode 100644 index 0000000..2fed96f --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/repository/BaseRepository.kt @@ -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 safeApiCall(apiCall: suspend () -> ApiResponse): ApiResponse { + 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 ?: "无错误信息"}") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/repository/RemoteRepository.kt b/app/src/main/java/com/sw/face/collect/repository/RemoteRepository.kt new file mode 100644 index 0000000..3cbb40c --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/repository/RemoteRepository.kt @@ -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?> { + return safeApiCall { + apiService.getUserFaceCache( + param = mapOf( + "pageNum" to pageNum, + "pageSize" to pageSize + ) + ) + } + } + /** + * 获取人脸数据 + */ + suspend fun getFaceIncrementList( + pageNum: Long, + pageSize: Long = 100L, + timestamp: Long + ): ApiResponse?> { + return safeApiCall { + apiService.getFaceIncrementList( + param = mapOf( + "pageNum" to pageNum, + "pageSize" to pageSize, + "timestamp" to timestamp + ) + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/GsonUtils.kt b/app/src/main/java/com/sw/face/collect/utils/GsonUtils.kt new file mode 100644 index 0000000..7b245ab --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/GsonUtils.kt @@ -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 fromJson(json: String?, clazz: Class): 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 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 fromJsonList(json: String?, clazz: Class): List? { + if (json.isNullOrEmpty()) { + return null + } + return try { + val type = TypeToken.getParameterized(List::class.java, clazz).type + defaultGson.fromJson>(json, type) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + /** + * 将 JSON 字符串转换为 Map 对象 + * @param json JSON 字符串 + * @param keyClazz Map 的 key 类型 + * @param valueClazz Map 的 value 类型 + * @return 转换后的 Map 对象 + */ + fun fromJsonMap( + json: String?, + keyClazz: Class, + valueClazz: Class + ): Map? { + if (json.isNullOrEmpty()) { + return null + } + return try { + val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type + defaultGson.fromJson>(json, type) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + /** + * 将对象转换为另一种类型的对象 + * @param obj 源对象 + * @param clazz 目标类型 + * @return 转换后的对象 + */ + fun convert(obj: Any?, clazz: Class): T? { + if (obj == null) { + return null + } + return fromJson(toJson(obj), clazz) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/SPUtil.kt b/app/src/main/java/com/sw/face/collect/utils/SPUtil.kt new file mode 100644 index 0000000..8927a10 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/SPUtil.kt @@ -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) } + else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}") + } + notifyDataChanged(key) + } + + @Suppress("UNCHECKED_CAST") + fun 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) 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 { + 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 = emptySet()) = SpProperty(key, default) + + inner class SpProperty(private val key: String, private val defaultValue: T) : + ReadWriteProperty { + + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/SpTool.java b/app/src/main/java/com/sw/face/collect/utils/SpTool.java new file mode 100644 index 0000000..20826f7 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/SpTool.java @@ -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); + } + + + +} diff --git a/app/src/main/java/com/sw/face/collect/viewmodel/MainViewModel.kt b/app/src/main/java/com/sw/face/collect/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..9ba51a5 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/viewmodel/MainViewModel.kt @@ -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 = 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) -> Unit + ) { + viewModelScope.launch { + val response = repository.getFaceIncrementList( + pageNum = pageNo.toLong(), + pageSize = pageSize.toLong(), + timestamp = timestamp + ) + if (parseResponse(response)) { + withContext(Dispatchers.Default) { + val list: List = response.data ?: emptyList() + onPageQueryFinished(list) + if (list.size >= pageSize) { + getFaceIncrementList( + pageNo = pageNo + 1, + pageSize = pageSize, + timestamp = timestamp, + onAllQueryFinished = onAllQueryFinished, + onPageQueryFinished = onPageQueryFinished + ) + } else { + onAllQueryFinished() + } + } + } + } + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00aa199..3db2d23 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/lib_face/build.gradle.kts b/lib_face/build.gradle.kts index 449f75b..76adfec 100644 --- a/lib_face/build.gradle.kts +++ b/lib_face/build.gradle.kts @@ -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") diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java index aac56cb..27c8119 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java @@ -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; }