人脸采集代码提交

This commit is contained in:
2025-12-26 13:48:50 +08:00
parent 38950829b3
commit 894962e387
154 changed files with 14368 additions and 34 deletions
+14 -34
View File
@@ -1,35 +1,15 @@
# ---> Android
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+1
View File
@@ -0,0 +1 @@
/build
+72
View File
@@ -0,0 +1,72 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.sw.face.collect"
compileSdk = 36
defaultConfig {
applicationId = "com.sw.face.collect"
minSdk = 30
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
viewBinding {
enable = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
// 协程
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// camerax
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)
// 权限申请
implementation(libs.accompanist.permissions)
implementation(project(":lib_face"))
implementation("com.github.Dimezis:BlurView:version-3.2.0")
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.FaceCollect">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="landscape"
tools:ignore="DiscouragedApi">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,738 @@
package com.sw.face.collect
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Point
import android.hardware.Camera
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.sw.face.collect.base.BaseActivity
import com.sw.face.collect.base.GlobalData
import com.sw.face.collect.databinding.ActivityMainBinding
import com.sw.face.collect.ext.dp
import com.sw.face.collect.ext.gone
import com.sw.face.collect.ext.toast
import com.sw.face.collect.ext.visible
import com.sw.face.collect.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.countDownByFlow
import com.sw.face.collect.view.LanServerListenerImpl
import com.sw.face.collect.view.TcpClientListenerImpl
import com.sw.plate.utils.L
import com.sw.plate.utils.NV21ToBitmap
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.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.system.exitProcess
class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlobalLayoutListener {
// private val debouncer = Debouncer(2000)
companion object {
private const val TAG = "MainActivity"
}
// 虹软人脸配置 ⬇
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private var livenessType = LivenessType.IR
// private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
private val nv21ToBitmap by lazy { NV21ToBitmap(this) }
//
private var irCameraHelper: DualCameraHelper? = null
private var irFaceRectTransformer: FaceRectTransformer? = null
private val recognizeViewModel: RecognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
override fun onGlobalLayout() {
Log.d(TAG, "onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
openCamera()
}
override fun initialize() {
super.initialize()
FaceEngineUtils.activeEngine()
setupArcCamera()
binding.btnCollectFace.setOnClickListener {
requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
if (granted.not()) {
toast("没有相机权限")
return@requestSinglePermissionResult
}
collectFace()
}
}
binding.tvTitle.setOnClickListener { finish() }
addSocketListener()
binding.flFace.visible()
binding.flRecognizeIr.visible()
binding.flCameraView.visible()
binding.tvFaceTip.visible()
binding.layoutState.gone()
resumeCamera()
}
private fun openCamera() {
Log.d(TAG, "openCamera")
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED
) {
toast("无摄像头权限")
return
}
try {
// val cameraCount = Camera.getNumberOfCameras()
// if (cameraCount < 3) {
// toast("摄像头数量异常"+cameraCount)
// return
// }
recognizeViewModel.init(
PreviewConfig(
PreviewConfig.DEFAULT_IR_CAMERA_ID, PreviewConfig.DEFAULT_RGB_CAMERA_ID,
0, 0
)
)
initRgbCamera()
if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
initIrCamera()
}
} catch (e: Exception) {
Log.d(TAG, "打开摄像头失败")
toast("打开摄像头失败+${e.message}")
}
}
/**
* 启用虹软人脸识别
*/
fun setupArcCamera() {
initArcViewModel()
initArcView()
openRectInfoDraw = true
}
private fun sendFaceData(faceData: String) {
// 给某个客户端发送
val jsonObject = JSONObject().also {
it.put("type", "faceFeature")
it.put("content", faceData)
}
tcpClient?.send(jsonObject)
}
private var tcpClient: TcpClient? = null
private fun addSocketListener() {
//val ipAddress = NetworkUtils.getIPAddress(true)
//Log.d(TAG, "addSocketListener,ipAddress: $ipAddress");
tcpClient = TcpClient(
"192.168.1.95",
5000,
GlobalData.deviceId, // clientId
5000, // connectTimeoutMs
10 * 1000, // heartbeatIntervalMs
30 * 1000 // heartbeatTimeoutMs
)
tcpClient?.setListener(object : TcpClientListenerImpl() {
override fun onSendSuccess(json: JSONObject?) {
super.onSendSuccess(json)
// toast("发送成功")
}
override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
super.onSendFailed(json, e)
toast("发送失败${e?.message}")
}
})
tcpClient?.start();
try {
LanServer.getInstance().let {
it.setListener(object : LanServerListenerImpl() {
override fun onMessageReceived(clientId: String?, message: JSONObject?) {
super.onMessageReceived(clientId, message)
toast("收到消息clientId=$clientId")
}
})
it.start()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// private var imageBitmap: Bitmap? = null
private fun collectFace() {
userFaceInfo = null
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
//showWaitingDialog("人脸信息中……")
binding.tvFaceTip.gone()
binding.btnCollectFace.gone()
binding.layoutState.visible()
binding.pbCollectLoading.visible()
binding.tvCollectState.text = "采集中......"
getFaceData { faceData ->
Log.d(TAG, "faceData: $faceData")
runOnUiThread {
//hideWaitingDialog()
//toast("人脸信息已采集")
binding.pbCollectLoading.gone()
//binding.tvCollectState.text = "采集完成"
userFaceInfo = null
countDown()
}
sendFaceData(faceData)
}
}
private var job: Job?=null
private fun getFaceData(block: (String) -> Unit) {
runBlocking {
job = executor.startIntervalTask(5) {
if (userFaceInfo == null) {
return@startIntervalTask
}
val faceData = Base64.encode(userFaceInfo!!.faceFeature!!.featureData)
block(faceData)
job?.cancel()
}
}
}
val executor = IntervalExecutor()
public override fun onStop() {
rgbCameraHelper?.release()
rgbCameraHelper = null
irCameraHelper?.release()
irCameraHelper = null
recognizeViewModel.destroy()
super.onStop()
}
fun resumeCamera() {
Log.d(TAG, "resumeCamera isRecognition = $isRecognition")
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
}
}
fun pauseCamera() {
Log.d(TAG, "pauseCamera isRecognition = $isRecognition")
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
private var userFaceInfo: UserFaceInfo? = null
private var isCheckCamera = false
private fun initArcViewModel() {
if (DualCameraHelper.hasDualCamera()) {
livenessType = LivenessType.IR
isCheckCamera = true
} else {
livenessType = LivenessType.RGB
isCheckCamera = false
}
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.prepareRegister()
recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
this.userFaceInfo = userFaceInfo
Log.d(TAG, "initArcViewModel: userFaceInfo == null : ${userFaceInfo == null}")
}
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
Log.d(TAG, "ftInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Log.d(TAG, "frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Log.d(TAG, "flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Log.d(TAG, "recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
Log.d(TAG, "recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(
this,
Observer { compareResult: CompareResult ->
Log.d(
TAG,
"recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
)
// val lastFaceTrackId = compareResult.trackId
// val faceEntity = compareResult.faceEntity
// val currentUserId = faceEntity.userName
// if (!currentUserId.isNullOrBlank()) {
// toast("当前用户已采集过人脸信息")
// return@Observer
// }
// //未识别到,拍摄照片
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
Log.d(TAG, "drawRectInfoText observe info = $info")
})
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
/**
* 调整View的宽高,使预览显示正常且采集框固定为
*
* @param rgbPreview RGB预览View
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @param scale 缩放比例
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
// val w = 640.dp//300.dp
val w = 640.dp//300.dp
val h = 480.dp//510.dp
val layoutParams = FrameLayout.LayoutParams(w, h);
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Log.d(
TAG,
"initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation"
)
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb,
binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation,
0.5F
//0.6F
)
Log.d(
TAG,
"initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}"
)
Log.d(
TAG,
"initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}"
)
Log.d(
TAG,
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror(
context
)
}, isDrawRgbRectVerticalMirror = ${
ConfigUtil.isDrawRgbRectVerticalMirror(
context
)
}"
)
// 调整识别窗口位置
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
displayOrientation,
// 90,
cameraId,
isMirror,
true,
false
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
// faceByteArray = nv21
if (!isRecognition) {
return
}
if (isCheckCamera) {
L.e("检查摄像头数据")
isCheckCamera = false
val isBw: Boolean = BitmapUtils.isBlackWhite(
nv21ToBitmap.nv21ToBitmap(
nv21,
rgbFaceRectTransformer!!.previewWidth,
rgbFaceRectTransformer!!.previewHeight,
0
)
)
if (isBw) {
L.e("黑白切换摄像头")
switchCamera()
}
L.e("摄像头正常")
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null
&& rgbFaceRectTransformer != null
) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Log.d(TAG, "initRgbCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Log.d(TAG, "initRgbCamera onCameraError: ${e.message}")
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Log.d(
TAG,
"initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}"
)
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Log.d(
TAG,
"initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation"
)
}
}
val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth
val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight
Log.d(TAG, "initRgbCamera measuredWidth=$measuredWidthmeasuredHeight=$measuredHeight")
val previewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(Point(measuredWidth, measuredHeight))
// .previewViewSize(Point(608.dp, 456.dp))
// .previewViewSize(Point(1024, 768))
.rotation(windowManager.defaultDisplay.rotation)
.additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
// .previewSize(Point(1080, 720))
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper?.run {
init()
// setSurfaceFrameCallback { bitmap ->
// imageBitmap = BitmapFlipUtil.flipHorizontally(bitmap)
//
//// binding.ivRecImage.visible()
//// binding.ivRecImage.setImageBitmap(imageBitmap)
// }
start()
}
}
/**
* 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
*/
private fun initIrCamera() {
Log.d(TAG, "initIrCamera: livenessType = $livenessType")
if (livenessType === LivenessType.RGB) {
return
}
val irCameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Log.d(
TAG,
"initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation"
)
val previewSizeIr = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr,
previewSizeIr, displayOrientation, 0.25f
)
irFaceRectTransformer = FaceRectTransformer(
previewSizeIr.width, previewSizeIr.height,
// layoutParams.width, layoutParams.height,
layoutParams.width, layoutParams.height,
displayOrientation, cameraId, isMirror,
ConfigUtil.isDrawIrRectHorizontalMirror(context),
ConfigUtil.isDrawIrRectVerticalMirror(context)
)
recognizeViewModel.onIrCameraOpened(camera)
recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer)
}
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
recognizeViewModel.refreshIrPreviewData(nv21)
}
override fun onCameraClosed() {
Log.d(TAG, "initIrCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Log.d(TAG, "initIrCamera onCameraError: ${e.message}")
e.printStackTrace()
}
override fun onCameraConfigurationChanged(
cameraID: Int,
displayOrientation: Int
) {
if (irFaceRectTransformer != null) {
irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Log.d(
TAG,
"initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation"
)
}
}
val previewConfig = recognizeViewModel.previewConfig
irCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewIr.measuredWidth,
binding.dualCameraTexturePreviewIr.measuredHeight
)
)
// .previewViewSize(Point(4,3))
.rotation(windowManager.defaultDisplay.rotation)
.specificCameraId(previewConfig.irCameraId)
.previewOn(binding.dualCameraTexturePreviewIr)
.cameraListener(irCameraListener)
.isMirror(true)
.previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小
.additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度
.build()
irCameraHelper?.init()
try {
irCameraHelper?.start()
} catch (e: RuntimeException) {
ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice))
}
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo>) {
// Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
// val listIsEmpty = facePreviewInfoList.isEmpty()
// val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null
// Log.d(TAG,"listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId")
// if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) {
// if (lastFaceTrackId != -1 && currentUserId != null) {
// mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// Timber.tag(com.sw.dualscreen.presentation.MainScreenPresentation.Companion.TAG).i("$lastFaceTrackId 用户离开")
// lastFaceTrackId = -1
// Timber.tag(com.sw.dualscreen.presentation.MainScreenPresentation.Companion.TAG)
// .d("postUserData userNutritionData是否null ${userNutrition == null}, currentFood是否null: ${currentFood == null}currentUserId是否null${currentUserId == null}")
// if (userNutrition == null || currentFood == null || currentUserId == null) {
// return
// }
// postUserData()
// if (mealPickupMode == 1 || isKeepFaceState) {
// step2FaceRecognizing(currentFood!!)
// isKeepFaceState = false
// } else {
// activity.isAnalyzing = false
// step1FoodRecognizing()
// }
// }
// }
}
fun switchCamera() {
try {
L.e("开始切换摄像头")
if (rgbCameraHelper != null && irCameraHelper != null) {
rgbCameraHelper!!.stop()
irCameraHelper!!.stop()
rgbCameraHelper!!.switchCameraId()
irCameraHelper!!.switchCameraId()
rgbCameraHelper!!.start()
irCameraHelper!!.start()
rgbFaceRectTransformer!!.cameraId =
if (rgbFaceRectTransformer!!.getCameraId() === 1) 0 else 1
// showLongToast(getString(R.string.notice_change_detect_degree));
L.e("切换摄像头成功")
} else {
L.e("切换摄像头失败")
// showToast(getString(R.string.switch_camera_failed));
exit()
val newIntent = Intent(this, MainActivity::class.java) // 要启动的Activity
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(newIntent)
}
} catch (e: java.lang.RuntimeException) {
L.e("切换摄像头失败")
toast(e.message + getString(R.string.camera_error_notice))
exit()
val newIntent = Intent(this, MainActivity::class.java) // 要启动的Activity
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(newIntent)
}
}
private fun exit() {
finish()
exitProcess(0)
}
override fun onDestroy() {
countDownJob?.cancel()
super.onDestroy()
}
private var countDownJob: Job? = null
private fun countDown() {
countDownJob = countDownByFlow(
total = 3,
scope = lifecycleScope,
onStart = {
binding.tvCollectState.text = "采集完成(3s"
},
onTick = { seconds ->
binding.tvCollectState.text = "采集完成(${seconds}s"
},
onFinish = {
//binding.tvCollectState.text = "采集完成"
binding.layoutState.gone()
binding.tvFaceTip.visible()
binding.btnCollectFace.visible()
}
)
}
}
@@ -0,0 +1,163 @@
//package com.sw.face.collect
//
//import android.Manifest
//import android.net.Uri
//import android.util.Log
//import android.view.View
//import androidx.camera.view.PreviewView
//import androidx.core.view.updateLayoutParams
//import com.sw.face.collect.base.BaseActivity
//import com.sw.face.collect.databinding.ActivityMainBakBinding
//import com.sw.face.collect.databinding.LayoutCameraPreviewBinding
//import com.sw.face.collect.ext.dp
//import com.sw.face.collect.ext.gone
//import com.sw.face.collect.ext.toast
//import com.sw.face.collect.ext.visible
//import com.sw.face.collect.socket.LanCommunicationManager
//import com.sw.face.collect.socket.LanServer
//import com.sw.face.collect.utils.Base64
//import com.sw.face.collect.utils.CameraUtils
//import com.sw.face.collect.utils.ToastUtils
//import org.json.JSONException
//import org.json.JSONObject
//
//class MainActivityBak : BaseActivity<ActivityMainBakBinding>() {
// // private val debouncer = Debouncer(2000)
//
// companion object {
// private const val TAG = "MainActivity"
// }
//
// private lateinit var previewView: PreviewView
// private val cameraUtils: CameraUtils by lazy {
// CameraUtils(this)
// }
//
// override fun inflateViewBinding() = ActivityMainBakBinding.inflate(layoutInflater)
//
// override fun initialize() {
// super.initialize()
// cameraUtils.initCamera()
// val previewBinding =
// LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
// previewView = previewBinding.previewView.also {
// it.updateLayoutParams {
// width = 680.dp
// height = 500.dp
// }
// }
// cameraUtils.setPreviewController(previewView)
//
// binding.btnClear.setOnClickListener {
// binding.ivFaceImage.gone()
// binding.flCameraPreview.visible()
// }
// binding.btnTakePhoto.setOnClickListener {
// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
// if (granted.not()) {
// toast("没有相机权限")
// return@requestSinglePermissionResult
// }
// takePhoto()
// }
// }
// binding.btnUpload.setOnClickListener {
// getFaceData { faceData ->
// Log.d(TAG, "faceData: $faceData")
// sendFaceData(faceData)
// }
// }
// binding.tvTitle.setOnClickListener { finish() }
// addSocketListener()
// }
//
// private fun sendFaceData(faceData: String) {
// // 给某个客户端发送
// val jsonObject = JSONObject().also {
// it.put("type", "cmd")
// it.put("content", faceData)
// }
// val clientId = "device12231530"
// lanManager?.sendToClient(clientId, jsonObject)
// lanManager?.broadcast(jsonObject)
// }
//
// private var lanManager: LanCommunicationManager?=null
// private fun addSocketListener() {
// lanManager = LanServer.getInstance()
// lanManager?.setListener(object : LanCommunicationManager.Listener{
// override fun onClientConnected(clientId: String?) {
// Log.d(TAG, "addSocketListener,onClientConnected: clientId=$clientId")
// }
//
// override fun onClientDisconnected(clientId: String?) {
// Log.d(TAG, "addSocketListener,onClientDisconnected: clientId=$clientId")
// }
//
// override fun onMessageReceived(
// clientId: String?,
// message: JSONObject?
// ) {
// Log.d(TAG, "addSocketListener,onMessageReceived: clientId=$clientIdmessage=$message")
// }
// })
// try {
// lanManager?.start()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
//
// private fun takePhoto() {
// showWaitingDialog("拍照中……")
// cameraUtils.takePhoto(cameraCallback) { errMsg ->
// hideWaitingDialog()
// ToastUtils.showToast("拍照异常,请重新操作")
// }
// }
//
// private var imageUri: Uri? = null
// private fun getFaceData(block: (String) -> Unit) {
// if (imageUri == null) {
// toast("未采集到照片,请拍照")
// return
// }
// context.contentResolver.openInputStream(imageUri!!)?.use { stream ->
// val faceData = Base64.encode(stream.readBytes())
// block(faceData)
// }
// }
//
// private val cameraCallback: (Uri) -> Unit = { uri ->
// try {
// this.imageUri = uri
// binding.flCameraPreview.gone()
// binding.ivFaceImage.run {
// visible()
// setImageURI(uri)
// }
// hideWaitingDialog()
// } catch (e: Exception) {
// e.printStackTrace()
// hideWaitingDialog()
// ToastUtils.showToast("程序异常${e.message}")
// }
// }
//
// override fun onResume() {
// super.onResume()
// cameraUtils.bind()
// binding.llCameraFlag.run {
// visibility = View.VISIBLE
// postDelayed({
// visibility = View.GONE
// }, 3000)
// }
// }
//
// override fun onPause() {
// super.onPause()
// cameraUtils.unbind()
// binding.llCameraFlag.visibility = View.VISIBLE
// }
//}
@@ -0,0 +1,25 @@
package com.sw.face.collect
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
class MyApp: App() {
companion object {
private const val TAG = "MyApp"
@SuppressLint("StaticFieldLeak")
var instance: Context? = null
}
override fun onCreate() {
super.onCreate()
instance = this
GlobalData.deviceId = AppUtil.getUDID(this)
Log.d(TAG, "onCreate: deviceId = ${GlobalData.deviceId}")
}
}
@@ -0,0 +1,112 @@
package com.sw.face.collect.base
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.sw.face.collect.databinding.DialogWaitingBinding
import com.sw.face.collect.view.CustomDialog
import com.sw.face.collect.R
import com.sw.face.collect.model.IActivityResult
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: VB
protected lateinit var context: Context
private var mDialogWaiting: CustomDialog? = null
private lateinit var permissionLauncher: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// EventBus.getDefault().register(this)
enableEdgeToEdge()
context = this
//保持亮屏
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = inflateViewBinding()
setContentView(binding.root)
initActivityLauncher()
initialize()
}
override fun onDestroy() {
// EventBus.getDefault().unregister(this)
super.onDestroy()
}
protected abstract fun inflateViewBinding(): VB
open fun initialize() {
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?) {
runOnUiThread {
hideWaitingDialog()
val dialogBinding = DialogWaitingBinding.inflate(LayoutInflater.from(this))
dialogBinding.tvTip.text = tip
mDialogWaiting = CustomDialog(this, dialogBinding.root)
mDialogWaiting?.show()
}
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
runOnUiThread {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
}
fun showWaitingDialog2(tip: String?) {
if (mDialogWaiting == null) {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting?.show()
}
val contentView = mDialogWaiting?.findViewById<ViewGroup>(android.R.id.content)
val tvTip = contentView?.findViewById<TextView>(R.id.tvTip)
tvTip?.text = tip
}
private var launchPermissionCallback: ((Boolean) -> Unit)? = null
fun requestSinglePermissionResult(
permission: String,
callback: (Boolean) -> Unit
) {
this.launchPermissionCallback = callback
permissionLauncher.launch(permission)
}
private fun initActivityLauncher() {
permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
launchPermissionCallback?.invoke(isGranted)
}
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun onPostEvent(event: PostEvent) {
//
// }
}
@@ -0,0 +1,60 @@
package com.sw.face.collect.base
object GlobalData {
/**
* 设备id
*/
var deviceId: String = ""
/**
* app版本号
*/
var appVersion: String = "1"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q3WA-8QT4"
}
/**
* 全局常量
*/
object Constants {
/**
* 自动关闭时间
*/
const val AUTO_CLOSE_TIME: Long = 3
}
/**
* key
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_PICKUP_MODE = "pickupMode"
const val KEY_CHARGE_MODE = "chargeMode"
}
@@ -0,0 +1,252 @@
package com.sw.face.collect.ext
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat
import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment
import com.sw.face.collect.utils.ToastUtils
//import com.google.gson.Gson
//import com.google.gson.reflect.TypeToken
//import com.sw.inbound.utils.CustomToastUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
fun Context.toast(
message: String?,
duration: Int = Toast.LENGTH_SHORT,
) {
if (message.isNullOrBlank()) {
return
}
Handler(Looper.getMainLooper()).post {
//Toast.makeText(this, message, duration).show()
//CustomToastUtils.showToast(message, duration)
ToastUtils.showToast(message,duration)
}
}
fun Fragment.toast(
message: String?,
duration: Int = 2000
) {
activity?.toast(message, duration)
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
inline fun <reified T : Activity> Context.startActivity(
bundle: Bundle? = null,
options: ActivityOptionsCompat? = null
) {
Intent(this, T::class.java).apply {
bundle?.let { putExtras(it) }
if (options != null && this@startActivity is Activity) {
startActivity(this, options.toBundle())
} else {
startActivity(this)
}
}
}
inline fun <reified T> Context.startActivity(
block: Intent.() -> Unit = {}
) {
Intent(this, T::class.java).apply {
block()
startActivity(this)
}
}
//inline fun <reified T : Activity> Context.startActivity(action:(bundle: Bundle)-> Unit) {
// Intent(this, T::class.java).apply {
// action(Bundle())
// startActivity(this)
// }
//}
//inline fun <reified T> String.toType(gson: Gson? = null, typeToken: TypeToken<T>): T {
// return (gson ?: Gson()).fromJson(this, typeToken.type)
//}
//
//inline fun <reified T> String.toObject(gson: Gson? = null): T {
// return (gson ?: Gson()).fromJson(this, T::class.java)
//}
//
//fun Any?.toJsonString(gson: Gson? = null): String {
// return (gson ?: Gson()).toJson(this) ?: ""
//}
@SuppressLint("ApplySharedPref")
inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) editor.commit() else editor.apply()
}
fun SharedPreferences.put(vararg pairs: Pair<String, Any>) {
edit {
pairs.forEach { (key, value) ->
when (value) {
is Int -> putInt(key, value)
is String -> putString(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Long -> putLong(key, value)
}
}
}
}
val Float.dp: Float
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
val Int.dp: Int
get() = this.toFloat().dp.toInt()
fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// 处理搜索逻辑
searchCallback()
return@setOnEditorActionListener true // 阻止事件继续传递
}
return@setOnEditorActionListener false
}
}
fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) {
var job: Job? = null
setOnClickListener {
job?.cancel()
job = CoroutineScope(Dispatchers.Main).launch {
delay(delay)
action()
}
}
}
fun Double.roundedDecimalPlace(num: Int): Double {
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
}
fun Double.roundedOneDecimalPlace(): Double {
return this.roundedDecimalPlace(1)
}
fun View.setShapeDrawable(
solidColor: String = "#FFFFFF",
strokeWidth: Int = 0,
strokeColor: String = "#FFFFFF",
radius: Int = 0
) {
background = GradientDrawable().apply {
setColor(solidColor.toColorInt())
setStroke(strokeWidth, strokeColor.toColorInt())
cornerRadius = radius.toFloat()
}
}
fun View.setShapeDrawable2(
solidColor: String = "#FFFFFF",
strokeWidth: Int = 0,
strokeColor: String = "#FFFFFF",
topLeftRadius: Int = 0,
topRightRadius: Int = 0,
bottomRightRadius: Int = 0,
bottomLeftRadius: Int = 0
) {
background = GradientDrawable().apply {
setColor(solidColor.toColorInt())
setStroke(strokeWidth, strokeColor.toColorInt())
setCornerRadii(
floatArrayOf(
topLeftRadius.toFloat(), topLeftRadius.toFloat(),
topRightRadius.toFloat(), topRightRadius.toFloat(),
bottomRightRadius.toFloat(), bottomRightRadius.toFloat(),
bottomLeftRadius.toFloat(), bottomLeftRadius.toFloat()
)
)
}
}
fun String?.ifNullOrBlank(defaultValue: String): String {
return if (this.isNullOrBlank()) defaultValue else this
}
fun Int?.ifNullOrZero(defaultValue: String): String {
return if (this == null || this == 0) defaultValue else this.toString()
}
fun Double?.ifNullOrZero(defaultValue: String): String {
return if (this == null || this == 0.toDouble()) defaultValue else this.toString()
}
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
this.clearFocus() // 清除焦点避免键盘再次弹出
}
/**
* 复制文本到剪贴板
* @param context 上下文
* @param text 要复制的文本内容
* @param showToast 是否显示复制成功提示,默认为true
*/
fun String.copyText(context: Context, showToast: Boolean = true) {
// 获取剪贴板管理器
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
// 创建ClipData对象,包含要复制的文本
val clipData = ClipData.newPlainText("label", this)
// 将数据设置到剪贴板
clipboardManager.setPrimaryClip(clipData)
// 显示复制成功提示
if (showToast) {
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
}
@@ -0,0 +1,24 @@
package com.sw.face.collect.ext
import android.text.SpannableStringBuilder
import android.text.Spanned
inline fun buildSpannableString(builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder {
return SpannableStringBuilder().apply(builderAction)
}
fun SpannableStringBuilder.appendText(text: String, vararg spans: Any): SpannableStringBuilder {
val start = length
append(text)
spans.forEach { span ->
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
return this
}
fun SpannableStringBuilder.withSpan(span: Any, block: SpannableStringBuilder.() -> Unit) {
val start = length
block()
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
@@ -0,0 +1,24 @@
package com.sw.face.collect.model;
import androidx.activity.result.ActivityResult;
import java.util.Map;
public interface IActivityResult {
interface LaunchActivityCallback {
void callback(ActivityResult result);
}
interface LaunchTackPictureCallback {
void callback(boolean result);
}
interface RequestPermissionCallback {
void callback(boolean result);
}
interface RequestMultiplePermissionsCallback {
void callback(Map<String,Boolean> result);
}
}
@@ -0,0 +1,300 @@
package com.sw.face.collect.socket;
import android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送
* 可用于 Android 和 JVM 程序。
*/
public class LanCommunicationManager {
// ============ 监听配置 ============
private final int port;
private final long HEARTBEAT_TIMEOUT_MS;
private final int MAX_CLIENT_THREADS;
// ============ 状态 ============
private volatile boolean running = false;
private ServerSocket serverSocket;
// ============ 线程池 ============
private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService clientExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// ============ 客户端会话 ============
private final ConcurrentHashMap<String, ClientSession> clients = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Socket, ClientSession> unAuthSessions = new ConcurrentHashMap<>();
// ============ 回调接口 ============
public interface Listener {
// 新客户端完成 AUTH / 认证
void onClientConnected(String clientId);
// 客户端断开
void onClientDisconnected(String clientId);
// 收到业务消息(type != heartbeat/auth)
void onMessageReceived(String clientId, JSONObject message);
}
private Listener listener;
// ============ 构造 ============
public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) {
this.port = port;
this.MAX_CLIENT_THREADS = maxClientThreads;
this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs;
this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads));
}
public void setListener(Listener listener) {
this.listener = listener;
}
// ============ 启动服务端 ============
@SuppressLint("DiscouragedApi")
public void start() throws IOException {
if (running) return;
running = true;
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(2000);
acceptExecutor.execute(this::acceptLoop);
scheduler.scheduleAtFixedRate(this::heartbeatCheck,
HEARTBEAT_TIMEOUT_MS,
HEARTBEAT_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
System.out.println("LanCommunicationManager started on port " + port);
}
private void acceptLoop() {
while (running) {
try {
Socket socket = serverSocket.accept();
socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2);
ClientSession session = new ClientSession(socket);
unAuthSessions.put(socket, session);
clientExecutor.execute(() -> clientReadLoop(session));
} catch (SocketTimeoutException ignore) {
} catch (Exception e) {
if (running) e.printStackTrace();
}
}
}
// ============ 处理客户端数据读取 ============
private void clientReadLoop(ClientSession session) {
Socket socket = session.socket;
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
while (running && !socket.isClosed()) {
int len;
try {
len = in.readInt();
} catch (SocketTimeoutException ste) {
continue;
}
if (len <= 0 || len > 10 * 1024 * 1024) break;
byte[] buf = new byte[len];
in.readFully(buf);
session.updateLastSeen();
JSONObject msg = new JSONObject(new String(buf));
handleMessage(session, msg);
}
} catch (Exception ignored) {
} finally {
closeSession(session);
}
}
private void handleMessage(ClientSession session, JSONObject msg) {
String type = msg.optString("type", "");
switch (type) {
case "auth":
handleAuth(session, msg);
break;
case "heartbeat":
session.updateLastSeen();
break;
default:
if (listener != null && session.clientId != null) {
listener.onMessageReceived(session.clientId, msg);
}
break;
}
}
private void handleAuth(ClientSession session, JSONObject msg) {
String clientId = msg.optString("clientId", null);
if (clientId == null) return;
session.clientId = clientId;
// 移动到已认证 map
unAuthSessions.remove(session.socket);
clients.put(clientId, session);
if (listener != null) listener.onClientConnected(clientId);
sendToSession(session, ack("auth_ok"));
}
// ============ 心跳超时 ============
private void heartbeatCheck() {
long now = System.currentTimeMillis();
for (Map.Entry<String, ClientSession> e : clients.entrySet()) {
ClientSession s = e.getValue();
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) {
closeSession(s);
}
}
for (ClientSession s : unAuthSessions.values()) {
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) {
closeSession(s);
}
}
}
// ============ 发送 ============
public boolean sendToClient(String clientId, JSONObject json) {
ClientSession s = clients.get(clientId);
return s != null && sendToSession(s, json);
}
public void broadcast(JSONObject json) {
for (ClientSession s : clients.values()) {
sendToSession(s, json);
}
}
private boolean sendToSession(ClientSession s, JSONObject json) {
try {
DataOutputStream out = s.out;
synchronized (out) {
byte[] data = json.toString().getBytes();
out.writeInt(data.length);
out.write(data);
out.flush();
}
return true;
} catch (Exception e) {
closeSession(s);
return false;
}
}
// ============ ACK ============
private JSONObject ack(String type) {
JSONObject j = new JSONObject();
try {
j.put("type", "ack");
j.put("ack", type);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return j;
}
// ============ 停止 ============
public void stop() {
running = false;
try {
serverSocket.close();
} catch (Exception ignored) {
}
for (ClientSession s : clients.values()) closeSession(s);
for (ClientSession s : unAuthSessions.values()) closeSession(s);
acceptExecutor.shutdownNow();
clientExecutor.shutdownNow();
scheduler.shutdownNow();
System.out.println("LanCommunicationManager stopped");
}
// ============ 会话类 ============
public static class ClientSession {
public final Socket socket;
public final DataOutputStream out;
public volatile long lastSeen = System.currentTimeMillis();
public volatile String clientId;
public ClientSession(Socket socket) throws IOException {
this.socket = socket;
this.out = new DataOutputStream(socket.getOutputStream());
}
public void updateLastSeen() {
lastSeen = System.currentTimeMillis();
}
}
public void closeSession(ClientSession session) {
if (session == null) return;
try {
Socket socket = session.socket;
// 1. 从已认证表移除
if (session.clientId != null) {
ClientSession removed = clients.remove(session.clientId);
if (removed != null && listener != null) {
listener.onClientDisconnected(session.clientId);
}
}
// 2. 从未认证表移除
unAuthSessions.remove(socket);
// 3. 关闭输出流
try {
session.out.close();
} catch (Exception ignored) {
}
// 4. 关闭 socket
try {
if (!socket.isClosed()) socket.close();
} catch (Exception ignored) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,21 @@
package com.sw.face.collect.socket;
public class LanServer {
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
if (instance == null) {
synchronized (LanServer.class) {
if (instance == null) {
instance = new LanCommunicationManager(
5000, // 监听端口
20, // 最大客户端数
10_000 // 心跳超时时间 10 秒
);
}
}
}
return instance;
}
}
@@ -0,0 +1,330 @@
package com.sw.face.collect.socket;
import android.util.Log;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* TcpClient - Android ready
* <p>
* Features:
* - length-prefix protocol (int length + bytes)
* - separate read thread and write queue & writer thread
* - auto-reconnect with exponential backoff
* - heartbeat scheduler
* - send queue with optional callback for send result
* - auto send "auth" JSON after connection
*/
public class TcpClient {
private static final String TAG = "TcpClient";
// configuration
private final String serverIp;
private final int serverPort;
private final String clientId; // will be sent in auth message
private final int connectTimeoutMs;
private final long heartbeatIntervalMs;
private final long heartbeatTimeoutMs;
// socket + streams
private Socket socket;
private DataOutputStream out;
private DataInputStream in;
// threads & executors
private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer"));
private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader"));
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler"));
private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect"));
// send queue
private final BlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<>();
// state
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicBoolean connected = new AtomicBoolean(false);
private final AtomicBoolean authSent = new AtomicBoolean(false);
// reconnection/backoff
private final long baseReconnectDelayMs = 1000; // 1s
private final long maxReconnectDelayMs = 30_000; // 30s
private final AtomicInteger reconnectAttempt = new AtomicInteger(0);
// heartbeat task future
private ScheduledFuture<?> heartbeatFuture;
// listener
public interface Listener {
void onConnected();
void onDisconnected(Exception e);
void onMessage(JSONObject json);
void onSendSuccess(JSONObject json);
void onSendFailed(JSONObject json, Exception e);
}
private Listener listener;
public void setListener(Listener l) {
this.listener = l;
}
// ctor
public TcpClient(String serverIp, int serverPort, String clientId,
int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.clientId = clientId;
this.connectTimeoutMs = connectTimeoutMs;
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
}
// start client (will attempt connect)
public void start() {
if (running.getAndSet(true)) return;
scheduleConnect(0);
// writer thread drains sendQueue
writerExecutor.execute(this::writerLoop);
}
// stop client and cleanup
public void stop() {
running.set(false);
cancelHeartbeat();
closeSocketQuiet();
writerExecutor.shutdownNow();
readerExecutor.shutdownNow();
scheduler.shutdownNow();
connectExecutor.shutdownNow();
sendQueue.clear();
}
// send JSON (queued). Non-blocking.
public void send(JSONObject json) {
if (!running.get()) return;
sendQueue.offer(json);
}
// AUTH shortcut (immediately send auth JSON)
private void sendAuth() {
try {
JSONObject auth = new JSONObject();
auth.put("type", "auth");
auth.put("clientId", clientId);
sendQueue.offer(auth);
authSent.set(true);
} catch (Exception ignored) {
}
}
// writer thread loop (serializes sends)
private void writerLoop() {
while (running.get()) {
try {
JSONObject json = sendQueue.take(); // blocks
if (connected.get() && out != null) {
try {
byte[] data = json.toString().getBytes();
synchronized (out) {
out.writeInt(data.length);
out.write(data);
out.flush();
}
if (listener != null) listener.onSendSuccess(json);
} catch (Exception e) {
if (listener != null) listener.onSendFailed(json, e);
// on write failure, attempt reconnect
safeCloseAndScheduleReconnect(e);
}
} else {
// not connected: requeue it and wait for connection
sendQueue.offer(json);
Thread.sleep(500); // avoid busy loop
}
} catch (InterruptedException ignored) {
break;
}
}
}
// reader loop (runs in readerExecutor)
private void startReaderLoop() {
readerExecutor.execute(() -> {
try {
while (running.get() && connected.get() && in != null) {
int length;
try {
length = in.readInt(); // will throw SocketTimeoutException if set
} catch (SocketTimeoutException ste) {
// used to detect socket liveness; continue loop
continue;
}
if (length <= 0 || length > 10 * 1024 * 1024) {
// invalid length, break
throw new RuntimeException("Invalid message length: " + length);
}
byte[] buf = new byte[length];
in.readFully(buf);
String s = new String(buf);
try {
JSONObject json = new JSONObject(s);
// update last seen time via heartbeat ack if needed
if ("heartbeat".equals(json.optString("type"))) {
// optionally respond or update time
} else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) {
// ignore or process ack
} else {
if (listener != null) listener.onMessage(json);
}
} catch (Exception je) {
Log.w(TAG, "Invalid JSON from server: " + s, je);
}
}
} catch (Exception e) {
if (running.get()) {
safeCloseAndScheduleReconnect(e);
}
}
});
}
// schedule connect attempt with delay (ms)
private void scheduleConnect(long delayMs) {
connectExecutor.execute(() -> {
try {
if (delayMs > 0) Thread.sleep(delayMs);
} catch (InterruptedException ignored) {
}
if (!running.get()) return;
tryConnect();
});
}
// connect logic
private void tryConnect() {
if (!running.get()) return;
closeSocketQuiet(); // ensure closed
try {
Socket s = new Socket();
s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs);
s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000));
socket = s;
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
connected.set(true);
reconnectAttempt.set(0);
authSent.set(false);
// start reader
startReaderLoop();
// send auth immediately
sendAuth();
// start heartbeat
startHeartbeat();
if (listener != null) listener.onConnected();
Log.i(TAG, "Connected to " + serverIp + ":" + serverPort);
} catch (Exception e) {
Log.w(TAG, "Connect failed: " + e.getMessage());
scheduleReconnectWithBackoff();
}
}
// start heartbeat scheduler
private void startHeartbeat() {
cancelHeartbeat();
heartbeatFuture = scheduler.scheduleAtFixedRate(() -> {
if (!running.get() || !connected.get()) return;
try {
JSONObject hb = new JSONObject();
hb.put("type", "heartbeat");
hb.put("time", System.currentTimeMillis());
sendQueue.offer(hb);
} catch (Exception ignored) {
}
}, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS);
}
private void cancelHeartbeat() {
if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
heartbeatFuture.cancel(true);
heartbeatFuture = null;
}
}
// close socket quietly and notify listener
private void safeCloseAndScheduleReconnect(Exception cause) {
closeSocketQuiet();
if (listener != null) listener.onDisconnected(cause);
scheduleReconnectWithBackoff();
}
private void scheduleReconnectWithBackoff() {
int attempt = reconnectAttempt.incrementAndGet();
long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs);
Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms");
scheduleConnect(delay);
}
// compute exponential backoff
private long maxReconnectDelayMsFromAttempt(int attempt) {
long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30));
if (d < 0) d = maxReconnectDelayMs;
return Math.min(d, maxReconnectDelayMs);
}
// close socket and streams
private void closeSocketQuiet() {
connected.set(false);
cancelHeartbeat();
try {
if (out != null) {
out.close();
}
} catch (Exception ignored) {
}
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (Exception ignored) {
}
out = null;
in = null;
socket = null;
}
// helper: when manually call reconnect (immediately)
public void reconnectNow() {
scheduleConnect(0);
}
// helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered)
public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException {
return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS);
}
}
@@ -0,0 +1,47 @@
package com.sw.face.collect.socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* Simple UDP discovery client:
* Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip"
*/
public class UdpDiscoveryClient {
public interface Listener {
void onFound(String ip, String serverName);
void onError(Exception e);
}
public void discover(int discoveryPort, int timeoutMs, Listener listener) {
new Thread(() -> {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
byte[] data = "DISCOVER_SERVER".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort);
socket.send(packet);
socket.setSoTimeout(timeoutMs);
byte[] buf = new byte[512];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
socket.receive(resp);
String msg = new String(resp.getData(), 0, resp.getLength());
if (msg.startsWith("SERVER_FOUND")) {
// format: SERVER_FOUND:serverName:ip
String[] parts = msg.split(":", 3);
if (parts.length >= 3) {
listener.onFound(parts[2], parts[1]);
return;
}
}
listener.onError(new Exception("Invalid response"));
} catch (Exception e) {
listener.onError(e);
}
}).start();
}
}
@@ -0,0 +1,113 @@
package com.sw.face.collect.socket;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class test {
LanCommunicationManager manager = new LanCommunicationManager(
9999, // 端口
20, // 最大客户端线程
30_000 // 心跳超时 30 秒
);
public void test() {
manager.setListener(new LanCommunicationManager.Listener() {
@Override
public void onClientConnected(String clientId) {
System.out.println("新的客户端上线:" + clientId);
}
@Override
public void onClientDisconnected(String clientId) {
System.out.println("客户端离线:" + clientId);
}
@Override
public void onMessageReceived(String clientId, JSONObject msg) {
System.out.println("收到 " + clientId + " 的消息:" + msg);
}
});
// 启动
try {
manager.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 给某个客户端发送
JSONObject j = new JSONObject();
try {
j.put("type", "cmd");
j.put("content", "hello");
} catch (JSONException e) {
throw new RuntimeException(e);
}
manager.sendToClient("device123", j);
// 广播
manager.broadcast(j);
}
public void clientTets() {
// 1) discover server (optional)
UdpDiscoveryClient disc = new UdpDiscoveryClient();
disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() {
@Override
public void onFound(String ip, String serverName) {
startClient(ip);
}
@Override
public void onError(Exception e) { /* fallback to manual IP */ }
});
}
// 2) start client
private TcpClient client;
private void startClient(String serverIp) {
client = new TcpClient(
serverIp,
9999,
"device123", // clientId
5000, // connectTimeoutMs
10_000, // heartbeatIntervalMs
30_000 // heartbeatTimeoutMs
);
client.setListener(new TcpClient.Listener() {
@Override
public void onConnected() {
Log.i("APP", "connected");
}
@Override
public void onDisconnected(Exception e) {
Log.i("APP", "disconnected", e);
}
@Override
public void onMessage(JSONObject json) {
Log.i("APP", "msg:" + json);
}
@Override
public void onSendSuccess(JSONObject json) {
}
@Override
public void onSendFailed(JSONObject json, Exception e) {
}
});
client.start();
}
}
@@ -0,0 +1,265 @@
package com.sw.face.collect.utils;
public final class Base64 {
private static final int BASELENGTH = 128;
private static final int LOOKUPLENGTH = 64;
private static final int TWENTYFOURBITGROUP = 24;
private static final int EIGHTBIT = 8;
private static final int SIXTEENBIT = 16;
private static final int FOURBYTE = 4;
private static final int SIGN = -128;
private static char PAD = '=';
private static byte[] base64Alphabet = new byte[BASELENGTH];
private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
static {
for (int i = 0; i < BASELENGTH; ++i) {
base64Alphabet[i] = -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
base64Alphabet[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
base64Alphabet[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
base64Alphabet[i] = (byte) (i - '0' + 52);
}
base64Alphabet['+'] = 62;
base64Alphabet['/'] = 63;
for (int i = 0; i <= 25; i++) {
lookUpBase64Alphabet[i] = (char) ('A' + i);
}
for (int i = 26, j = 0; i <= 51; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('a' + j);
}
for (int i = 52, j = 0; i <= 61; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('0' + j);
}
lookUpBase64Alphabet[62] = (char) '+';
lookUpBase64Alphabet[63] = (char) '/';
}
private static boolean isWhiteSpace(char octect) {
return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
}
private static boolean isPad(char octect) {
return (octect == PAD);
}
private static boolean isData(char octect) {
return (octect < BASELENGTH && base64Alphabet[octect] != -1);
}
/**
* Encodes hex octects into Base64
*
* @param binaryData Array containing binaryData
* @return Encoded Base64 array
*/
public static String encode(byte[] binaryData) {
if (binaryData == null) {
return null;
}
int lengthDataBits = binaryData.length * EIGHTBIT;
if (lengthDataBits == 0) {
return "";
}
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1
: numberTriplets;
char encodedData[] = null;
encodedData = new char[numberQuartet * 4];
byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
int encodedIndex = 0;
int dataIndex = 0;
for (int i = 0; i < numberTriplets; i++) {
b1 = binaryData[dataIndex++];
b2 = binaryData[dataIndex++];
b3 = binaryData[dataIndex++];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6)
: (byte) ((b3) >> 6 ^ 0xfc);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
}
// form integral number of 6-bit groups
if (fewerThan24bits == EIGHTBIT) {
b1 = binaryData[dataIndex];
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
encodedData[encodedIndex++] = PAD;
encodedData[encodedIndex++] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
encodedData[encodedIndex++] = PAD;
}
return new String(encodedData);
}
/**
* Decodes Base64 data into octects
*
* @param encoded string containing Base64 data
* @return Array containind decoded data.
*/
public static byte[] decode(String encoded) {
if (encoded == null) {
return null;
}
char[] base64Data = encoded.toCharArray();
// remove white spaces
int len = removeWhiteSpace(base64Data);
if (len % FOURBYTE != 0) {
return null;// should be divisible by four
}
int numberQuadruple = (len / FOURBYTE);
if (numberQuadruple == 0) {
return new byte[0];
}
byte decodedData[] = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
int i = 0;
int encodedIndex = 0;
int dataIndex = 0;
decodedData = new byte[(numberQuadruple) * 3];
for (; i < numberQuadruple - 1; i++) {
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))
|| !isData((d3 = base64Data[dataIndex++]))
|| !isData((d4 = base64Data[dataIndex++]))) {
return null;
}// if found "no data" just return null
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))) {
return null;// if found "no data" just return null
}
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
d3 = base64Data[dataIndex++];
d4 = base64Data[dataIndex++];
if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
if (isPad(d3) && isPad(d4)) {
if ((b2 & 0xf) != 0)// last 4 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 1];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
return tmp;
} else if (!isPad(d3) && isPad(d4)) {
b3 = base64Alphabet[d3];
if ((b3 & 0x3) != 0)// last 2 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 2];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
return tmp;
} else {
return null;
}
} else { // No PAD e.g 3cQl
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
return decodedData;
}
/**
* remove WhiteSpace from MIME containing encoded Base64 data.
*
* @param data the byte array of base64 data (with WS)
* @return the new length
*/
private static int removeWhiteSpace(char[] data) {
if (data == null) {
return 0;
}
// count characters that's not whitespace
int newSize = 0;
int len = data.length;
for (int i = 0; i < len; i++) {
if (!isWhiteSpace(data[i])) {
data[newSize++] = data[i];
}
}
return newSize;
}
}
@@ -0,0 +1,39 @@
package com.sw.face.collect.utils;
import android.graphics.Bitmap;
import android.graphics.Matrix;
public class BitmapFlipUtil {
/**
* 水平翻转Bitmap
* @param src 原始Bitmap
* @return 水平翻转后的Bitmap
*/
public static Bitmap flipHorizontally(Bitmap src) {
Matrix matrix = new Matrix();
matrix.preScale(-1, 1); // 水平翻转
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
}
/**
* 垂直翻转Bitmap
* @param src 原始Bitmap
* @return 垂直翻转后的Bitmap
*/
public static Bitmap flipVertically(Bitmap src) {
Matrix matrix = new Matrix();
matrix.preScale(1, -1); // 垂直翻转
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
}
/**
* 水平和垂直同时翻转(180°旋转)
* @param src 原始Bitmap
* @return 180°旋转后的Bitmap
*/
public static Bitmap flipBoth(Bitmap src) {
Matrix matrix = new Matrix();
matrix.preScale(-1, -1); // 同时水平和垂直翻转
return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false);
}
}
@@ -0,0 +1,38 @@
package com.sw.face.collect.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.ByteArrayOutputStream
object BitmapToByteArrayUtil {
/**
* Bitmap转PNG格式byte[]
* @param bitmap 原始Bitmap
* @return PNG格式的byte[]
*/
fun bitmapToPngByteArray(bitmap: Bitmap): ByteArray {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
return stream.toByteArray()
}
/**
* Bitmap转JPEG格式byte[]
* @param bitmap 原始Bitmap
* @param quality 压缩质量(0-100
* @return JPEG格式的byte[]
*/
fun bitmapToJpegByteArray(bitmap: Bitmap, quality: Int): ByteArray {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
return stream.toByteArray()
}
/**
* byte[]转Bitmap(通用格式)
* @param data byte数组
* @return 解析后的Bitmap
*/
fun byteArrayToBitmap(data: ByteArray): Bitmap {
return BitmapFactory.decodeByteArray(data, 0, data.size)
}
}
@@ -0,0 +1,36 @@
package com.sw.face.collect.utils;
import android.graphics.Bitmap;
import android.graphics.Color;
public class BitmapUtils {
public static boolean isBlackWhite(Bitmap bmp) {
Color c = null;
//遍历图片的像素点
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
c = bmp.getColor(x, y);
// L.e(c.red() + "===" + c.green() + "===" + c.blue());
//判断像素点的色偏差值Diff
if (getRGBDiff(c.red(), c.green(), c.blue()) != 0) {
return false;
}
}
}
return true;
}
private static float getRGBDiff(float r, float g, float b) {
float test1 = Math.abs(r - g);
float test2 = Math.abs(r - b);
float test3 = Math.abs(g - b);
float max = Math.max(test1, test2);
max = Math.max(max, test3);
return max;
}
}
@@ -0,0 +1,86 @@
package com.sw.face.collect.utils
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.camera.core.CameraSelector
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
class CameraUtils(private var activity: ComponentActivity) {
private var cameraController: LifecycleCameraController? = null
private var photoCaptureHelper: PhotoCaptureHelper? = null
private var failCallback: ((msg: String) -> Unit)? = null
// private var isCameraReady = false
fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) {
this.failCallback = failCallback
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(succCallback)
it.bindCameraCallback {
bind()
}
it.takePhoto()
}
}
private fun initCaptureHelper() {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
failCallback?.invoke(msg)
}
)
}
fun setPreviewController(previewView: PreviewView?) {
if (previewView?.controller == null) {
previewView?.controller = cameraController
}
}
fun initCamera() {
if (cameraController == null) {
cameraController = LifecycleCameraController(activity).apply {
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE
// or CameraController.VIDEO_CAPTURE
)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
// setWhiteBalanceMode(WhiteBalanceMode.AUTO)
}
bind()
}
// if (isCameraReady.not()) {
// try {
// cameraController!!.initializationFuture.addListener({
// isCameraReady = true
// Timber.d("Camera initialized successfully")
// }, ContextCompat.getMainExecutor(this))
// } catch (e: Exception) {
// Timber.d("Camera initialized error = ${e.message}")
// }
// }
}
fun bind() {
cameraController?.bindToLifecycle(activity)
}
fun unbind() {
cameraController?.unbind()
}
}
@@ -0,0 +1,28 @@
package com.sw.face.collect.utils
/**
* 防抖工具
*/
class Debouncer(private val delayMillis: Long = 2000) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
action()
return true
}
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}
@@ -0,0 +1,47 @@
package com.sw.face.collect.utils
import android.util.Log
import com.arcsoft.face.ErrorInfo
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
object FaceEngineUtils {
private val faceApi: FaceApi by lazy { FaceApi() }
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Log.d("FaceEngineUtils", "activeEngine activeCode = $activeCode")
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
})
}
}
@@ -0,0 +1,33 @@
package com.sw.face.collect.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
// 倒计时Flow扩展函数
fun countDownByFlow(
total: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onStart: (() -> Unit)? = null,
onFinish: (() -> Unit)? = null
): Job {
return flow {
for (i in total downTo 0) {
emit(i)
if (i != 0) delay(1000)
}
}.flowOn(Dispatchers.Main)
.onStart { onStart?.invoke() }
.onCompletion { onFinish?.invoke() }
.onEach { onTick.invoke(it) }
.launchIn(scope)
}
@@ -0,0 +1,66 @@
package com.sw.face.collect.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class IntervalExecutor {
/**
* 启动定时任务
* @param delayMillis 延迟时间(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
action()
delay(delayMillis)
}
}
}
/**
* 启动定时任务(带初始延迟)
* @param initialDelay 初始延迟时间(毫秒)
* @param delayMillis 后续执行间隔(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTaskWithInitialDelay(
initialDelay: Long,
delayMillis: Long,
action: suspend () -> Unit
): Job {
return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay)
while (isActive) {
action()
delay(delayMillis)
}
}
}
}
// 使用示例
fun main() = runBlocking {
val executor = IntervalExecutor()
// 示例1:每隔10秒执行一次
val job1 = executor.startIntervalTask(10000) {
println("定时任务执行: ${System.currentTimeMillis()}")
// 这里可以执行你的业务逻辑
}
// 示例2:先延迟5秒,然后每隔3秒执行一次
val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
}
// 运行30秒后取消任务
delay(30000)
job1.cancel()
job2.cancel()
println("所有定时任务已取消")
}
@@ -0,0 +1,50 @@
package com.sw.face.collect.utils;
import androidx.annotation.RequiresPermission;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.LinkedList;
public class NetworkUtils {
@RequiresPermission("android.permission.INTERNET")
public static String getIPAddress(boolean useIPv4) {
try {
Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
LinkedList<InetAddress> adds = new LinkedList();
while(nis.hasMoreElements()) {
NetworkInterface ni = (NetworkInterface)nis.nextElement();
if (ni.isUp() && !ni.isLoopback()) {
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while(addresses.hasMoreElements()) {
adds.addFirst(addresses.nextElement());
}
}
}
for(InetAddress add : adds) {
if (!add.isLoopbackAddress()) {
String hostAddress = add.getHostAddress();
boolean isIPv4 = hostAddress.indexOf(58) < 0;
if (useIPv4) {
if (isIPv4) {
return hostAddress;
}
} else if (!isIPv4) {
int index = hostAddress.indexOf(37);
return index < 0 ? hostAddress.toUpperCase() : hostAddress.substring(0, index).toUpperCase();
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return "";
}
}
@@ -0,0 +1,97 @@
package com.sw.face.collect.utils
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.core.content.ContextCompat
import java.io.File
/**
* 拍照工具类
* @param context Context 上下文
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
*/
class PhotoCaptureHelper(
private val context: Context,
private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {}
) {
companion object {
const val TAG = "PhotoCaptureHelper"
}
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback: (Uri) -> Unit) {
if (callbackList.contains(callback).not()) {
callbackList.add(callback)
}
}
private var bindCamera: (() -> Unit)? = null
fun bindCameraCallback(callback: () -> Unit) {
this.bindCamera = callback
}
/**
* 拍照方法
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
* @param fileExtension 文件扩展名,默认为".jpg"
*/
fun takePhoto(
fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg"
) {
Log.d(TAG, "开始拍照采集")
try {
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"${fileNamePrefix}${System.currentTimeMillis()}",
fileExtension,
cacheDir
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Log.d(TAG, "照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach {
it(photoUri)
}
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
if (errorMsg.contains("Not bound to a valid Camera")) {
if (bindCamera != null) {
bindCamera?.invoke()
//takePhoto()
}
}
Log.e(TAG, errorMsg)
onError(errorMsg)
}
}
)
} catch (e: Exception) {
val errorMsg = "创建临时文件失败: ${e.message}"
Log.e(TAG, errorMsg)
onError(errorMsg)
}
}
}
@@ -0,0 +1,117 @@
package com.sw.face.collect.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.sw.face.collect.MyApp;
import com.sw.face.collect.R;
/**
* The type Toast utils.
*/
public class ToastUtils {
private static Toast toast;
@SuppressLint("StaticFieldLeak")
private static TextView textCenterView;
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text, int duration) {
if (TextUtils.isEmpty(text)) {
return;
}
runOnMainThread(()->{
Context context = MyApp.Companion.getInstance();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 20);
toast.setDuration(duration);
toast.setView(view);
}
textCenterView.setText(text);
toast.show();
});
}
public static void runOnMainThread(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text) {
showToast(text, Toast.LENGTH_SHORT);
// if (snackbar == null) {
// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
// textCenterView = view.findViewById(R.id.toast_tv);
// snackbar = Snackbar.make(view, "", Snackbar.LENGTH_SHORT);
//
// }
// textCenterView.setText(text);
// snackbar.setDuration(Snackbar.LENGTH_SHORT);
// snackbar.show();
}
// private static Snackbar snackbar = null;
//
// public static void showLongToast(String text) {
// runOnMainThread(()-> {
// Context context = App.Companion.getInstance();
// if (toast == null) {
// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
// textCenterView = view.findViewById(R.id.toast_tv);
// toast = new Toast(context);
// toast.setGravity(Gravity.TOP, 0, 20);
// toast.setDuration(Toast.LENGTH_LONG);
// toast.setView(view);
// }
// textCenterView.setText(text);
// showMyToast(toast, 1000000 * 30);
// });
// }
//
//
// //自定义Toast控件
// private static void showMyToast(final Toast toast, final int cnt) {
// final Timer timer = new Timer();
// timer.schedule(new TimerTask() {
// @Override
// public void run() {
// toast.show();
// }
// }, 0, Toast.LENGTH_LONG);
// new Timer().schedule(new TimerTask() {
// @Override
// public void run() {
// toast.cancel();
// timer.cancel();
// }
// }, cnt);
// }
//
// public static void showSystemToast(String text) {
// Context context = App.Companion.getInstance();
// Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
// }
}
@@ -0,0 +1,101 @@
package com.sw.face.collect.view;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import com.sw.face.collect.R;
/**
* 加载进度条弹窗
*/
public class CustomDialog extends Dialog {
/**
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
*/
public CustomDialog(Context context, View layout) {
super(context, R.style.LoadingDialog);
setContentView(layout);
initConfig();
}
public CustomDialog(Context context, View layout, int style) {
super(context, style);
setContentView(layout);
initConfig();
}
private void initConfig() {
setCancelable(true);
setCanceledOnTouchOutside(true);
Window window = getWindow();
if (window != null) {
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
window.setAttributes(params);
}
}
//
// /**
// * 宽高由该方法的参数设置
// */
// public CustomDialog(Context context, int width, int height, View layout,
// int style) {
// super(context, style);
// // 设置内容
// setContentView(layout);
// // 设置窗口属性
// Window window = getWindow();
// WindowManager.LayoutParams params = window.getAttributes();
// // 设置宽度、高度、密度、对齐方式
// float density = getDensity(context);
// params.width = (int) (width * density);
// params.height = (int) (height * density);
// params.gravity = Gravity.CENTER;
// window.setAttributes(params);
//
// }
//
// /**
// * 获取显示密度
// *
// * @param context
// * @return
// */
// public float getDensity(Context context) {
// Resources res = context.getResources();
// DisplayMetrics dm = res.getDisplayMetrics();
// return dm.density;
// }
//
// private void fullScreenImmersive(View view) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// | View.SYSTEM_UI_FLAG_FULLSCREEN;
// view.setSystemUiVisibility(uiOptions);
// }
//
// }
// @Override
// public void show() {
// this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
// super.show();
//// fullScreenImmersive(getWindow().getDecorView());
//// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
// }
}
@@ -0,0 +1,22 @@
package com.sw.face.collect.view
import android.util.Log
import com.sw.face.collect.socket.LanCommunicationManager
import org.json.JSONObject
open class LanServerListenerImpl: LanCommunicationManager.Listener {
companion object {
private const val TAG = "LanServerListenerImpl"
}
override fun onClientConnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId")
}
override fun onClientDisconnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId")
}
override fun onMessageReceived(clientId: String?, message: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientIdmessage = $message")
}
}
@@ -0,0 +1,34 @@
package com.sw.face.collect.view
import android.util.Log
import com.sw.face.collect.socket.TcpClient
import org.json.JSONObject
import java.lang.Exception
import kotlin.math.log
open class TcpClientListenerImpl : TcpClient.Listener {
companion object {
private const val TAG = "TcpClientListenerImpl"
}
override fun onConnected() {
Log.d(TAG, "addSocketListener,onConnected: ")
}
override fun onDisconnected(e: Exception?) {
Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}")
}
override fun onMessage(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessage: $json")
}
override fun onSendSuccess(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onSendSuccess: $json")
}
override fun onSendFailed(json: JSONObject?, e: Exception?) {
Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}")
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+7
View File
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.FaceCollect" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="teal_200">#FF03DAC5</color>
</resources>
+7
View File
@@ -0,0 +1,7 @@
<resources>
<string name="app_name">人脸信息采集</string>
<string name="specific_engine_init_failed">%s 初始化失败,错误码:%d\n错误码常量名:%s</string>
<string name="permission_denied">权限被拒绝!</string>
<string name="camera_error_notice">\n可能的原因:该设备不支持同时打开两个摄像头</string>
</resources>
+35
View File
@@ -0,0 +1,35 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.FaceCollect" parent="Base.Theme.FaceCollect" >
<item name="android:windowBackground">@drawable/bg_splash</item>
</style>
<!-- Base application theme. -->
<style name="Base.Theme.FaceCollect" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="round10dpCornerStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">10dp</item>
</style>
<style name="LoadingDialog" parent="Theme.AppCompat.Dialog">
<item name="android:windowIsFloating">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="MyDialog" parent="@android:style/Theme.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.6</item>
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
</style>
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-cache-path
name="images"
path="pictures/" />
<external-cache-path
name="cache"
path="." />
</paths>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- <domain-config cleartextTrafficPermitted="true">-->
<!-- <domain includeSubdomains="true">vip.shuziweidao.com</domain>-->
<!-- <domain includeSubdomains="true">192.168.1.207</domain>-->
<!-- <domain includeSubdomains="true">device.shuziweidao.com</domain>-->
<!-- </domain-config>-->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
+5
View File
@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}
+23
View File
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
Vendored
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
Vendored
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+1
View File
@@ -0,0 +1 @@
/build
+72
View File
@@ -0,0 +1,72 @@
plugins {
// alias(libs.plugins.android.library)
id("com.android.library")
}
android {
namespace = "com.sw.plate"
compileSdk = 35
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
sourceSets {
named("main") {
jniLibs.srcDirs("libs")
}
}
}
dependencies {
api(
fileTree(
mapOf(
"dir" to "libs",
"include" to listOf("*.aar", "*.jar")
)
)
)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
// implementation("com.licheedev:android-serialport:2.1.5")
val roomVersion = "2.2.5"
implementation("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion")
implementation("io.reactivex.rxjava2:rxandroid:2.0.1")
implementation("com.google.code.gson:gson:2.8.6")
val glideVersion = "4.12.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion")
val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
}
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package com.sw.plate;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.sw.plate.test", appContext.getPackageName());
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,19 @@
package com.sw.plate;
import android.app.Application;
import android.content.Context;
public class App extends Application {
private static Context mContext;
@Override
public void onCreate() {
super.onCreate();
mContext = this;
}
public static Context getContext() {
return mContext;
}
}
@@ -0,0 +1,30 @@
package com.sw.plate;
import android.os.Environment;
public class AppConst {
public static final String BASE_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/sw";
// public static final String ARCSOFT_APP_ID = "J6jt8Lgou3cTW9Y1k9T8Zx4nP51ZgcHRv668znCcUu5g";
// public static final String ARCSOFT_SDK_KEY = "8necG4J6MQeTnz4gvcZuaRUywJynZindJCt2geuBnYv9";
// 85Q1-11DY-B13F-83WC
// APP_ID:H7kCBZ6zf8xMiqVXRmiXeaCaFhHGB5ubUiDkocQRydfQ
// SDK_KEY:7sLu3pXYUiBurhTJjWB5yWac8qYxjDTeR8iSqAG7dAnM
public static final String ARCSOFT_APP_ID = "H7kCBZ6zf8xMiqVXRmiXeaCaFhHGB5ubUiDkocQRydfQ";
public static final String ARCSOFT_SDK_KEY = "7sLu3pXYUiBurhTJjWB5yWac8qYxjDTeR8iSqAG7dAnM";
public static final String ARCSOFT_ACTIVE_KEY = "85Q1-11DY-B13F-83WC";
/**
* 方式二: 在激活界面读取本地配置文件进行激活
* <p>
* 配置文件名称,格式如下:
* APP_ID:XXXXXXXXXXXXX
* SDK_KEY:XXXXXXXXXXXXXXX
* ACTIVE_KEY:XXXX-XXXX-XXXX-XXXX
*/
public static final String ACTIVE_CONFIG_FILE_NAME = "activeConfig.txt";
}
@@ -0,0 +1,751 @@
package com.sw.plate.utils;
import static android.content.Context.TELEPHONY_SERVICE;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.NetworkInterface;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class AppUtil {
public static String getAppPackageName(Context context) {
String packageName = "";
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
packageName = pi.packageName;
if (AppUtil.isEmpty(packageName)) {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return packageName;
}
public static String getAppVersionName(Context context) {
String versionName = "";
// int versioncode=1;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versionName = pi.versionName;
// versioncode = pi.versionCode;表示更新了多少次
if (versionName == null || versionName.length() <= 0) {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return versionName;
}
public static int getAppVersionCode(Context context) {
int versioncode = 1;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versioncode = pi.versionCode;
} catch (Exception e) {
e.printStackTrace();
}
return versioncode;
}
//判断微信是否安装
public static boolean isWeixinInstalled(Context context) {
final PackageManager packageManager = context.getPackageManager();// 获取packagemanager
List<PackageInfo> pinfo = packageManager.getInstalledPackages(0);// 获取所有已安装程序的包信息
if (pinfo != null) {
for (int i = 0; i < pinfo.size(); i++) {
String pn = pinfo.get(i).packageName;
if (pn.equals("com.tencent.mm")) {
return true;
}
}
}
return false;
}
/**
* 打电话
* <p>
* Intent.ACTION_DIAL Intent.ACTION_CALL
*
* @param context
* @param mobile
*/
public static void callUp(Context context, String mobile) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:"
+ mobile));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* 获取设备ID
*
* @param context
* @return
*/
public static String getDevId(Context context) {
TelephonyManager TelephonyMgr = (TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
return TelephonyMgr.getDeviceId();
}
/**
* 姓名脱敏
*
* @param fullName
* @return
*/
public static String desensitizedName(String fullName) {
if (fullName == null || fullName.length() <= 1) {
return fullName;
}
char[] nameArr = fullName.toCharArray();
if (nameArr.length > 2) {
for (int i = 1; i < nameArr.length - 1; i++) {
nameArr[i] = '*';
}
} else {
nameArr[1] = '*';
}
return new String(nameArr);
}
public static String formatDateGetFull(String date) {
if (isEmpty(date)) {
return "";
}
Date d = new Date(Long.parseLong(date));
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return dateFormat1.format(d);
}
public static String formatDateGetCurrentTime() {
Date d = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//.SSS
return dateFormat1.format(d);
}
public static String formatDateGetFull(long date) {
if (date == 0) {
return "";
}
Date d = new Date(date);
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return dateFormat1.format(d);
}
public static String formatDateGetDay(long date) {
if (date == 0) {
return "";
}
Date d = new Date(date);
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
return dateFormat1.format(d);
}
public static boolean isEmpty(String s) {
if (TextUtils.isEmpty(s) || s.trim().equals("null")) {
return true;
} else {
return false;
}
}
/**
* 格式化浮点型
*
* @param data
* @return
*/
public static String formatDouble(double data) {
return new DecimalFormat("0.00").format(data);
}
/**
* 格式化分钟
*
* @param minutes
* @return
*/
public static String formatMinutes(int minutes) {
int hour = minutes / 60;
int minute = minutes % 60;
if (hour > 0 && minute > 0) {
return hour + "小时" + minute + "分钟";
} else if (hour > 0) {
return hour + "小时";
} else {
return minute + "分钟";
}
}
//com.fawan.news
public static void goToMarket(Context context, String packageName) {
Uri uri = Uri.parse("market://details?id=" + packageName);
Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri);
try {
context.startActivity(goToMarket);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
/**
* true为存在,false为不存在
*
* @param context
* @param packageName
* @return
*/
public static boolean isInstallApp(Context context, String packageName) {
try {
context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* 格式化float 保留两位小数
*
* @param data
* @return
*/
public static float formatFloat2(float data) {
// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足.
// return decimalFormat.format(data);//返回字符串
int scale = 1;//设置位数
int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等.
BigDecimal bd = new BigDecimal((double) data);
bd = bd.setScale(scale, roundingMode);
data = bd.floatValue();
return data;
}
/**
* Android 6.0 之前(不包括6.0)获取mac地址
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
*
* @param context * @return
*/
public static String getMacDefault(Context context) {
String mac = "";
if (context == null) {
return mac;
}
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = null;
try {
info = wifi.getConnectionInfo();
} catch (Exception e) {
e.printStackTrace();
}
if (info == null) {
return null;
}
mac = info.getMacAddress();
if (!TextUtils.isEmpty(mac)) {
mac = mac.toUpperCase(Locale.ENGLISH);
}
return mac;
}
/**
* Android 6.0-Android 7.0 获取mac地址
*/
public static String getMacAddress() {
String macSerial = null;
String str = "";
try {
Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address");
InputStreamReader ir = new InputStreamReader(pp.getInputStream());
LineNumberReader input = new LineNumberReader(ir);
while (null != str) {
str = input.readLine();
if (str != null) {
macSerial = str.trim();//去空格
break;
}
}
} catch (IOException ex) {
// 赋予默认值
ex.printStackTrace();
}
return macSerial;
}
/**
* Android 7.0之后获取Mac地址
* 遍历循环所有的网络接口,找到接口是 wlan0
* 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
*
* @return
*/
public static String getMacFromHardware() {
try {
ArrayList<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface nif : all) {
if (!nif.getName().equals("wlan0"))
continue;
byte[] macBytes = nif.getHardwareAddress();
if (macBytes == null) return "";
StringBuilder res1 = new StringBuilder();
for (Byte b : macBytes) {
res1.append(String.format("%02X:", b));
}
if (!TextUtils.isEmpty(res1)) {
res1.deleteCharAt(res1.length() - 1);
}
return res1.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 获取mac地址(适配所有Android版本)
*
* @return
*/
public static String getMac(Context context) {
String mac = "";
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
mac = getMacDefault(context);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
mac = getMacAddress();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mac = getMacFromHardware();
}
return mac;
}
//把String转化为float
public static double convertToFloat(String number, double defaultValue) {
if (TextUtils.isEmpty(number)) {
return defaultValue;
}
try {
return Double.parseDouble(number);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 获取AndroidId
*
* @param context
* @return
*/
public static String getAndroidId(Context context) {
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
return androidId;
}
/**
* 获取设备唯一 UDID
*
* @param context
* @return
*/
@SuppressLint("MissingPermission")
public static String getUDID(Context context) {
// String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
// L.e("androidID===" + androidID);
// return androidID;
String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (!androidID.equals("")) {
try {
if (!"9774d56d682e549c".equals(androidID)) {
androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString();
} else {
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString();
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return androidID;
}
//需要权限 android.permission.READ_PHONE_STATE
TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE);
String szImei = TelephonyMgr.getDeviceId();
if (!szImei.equals("")) {
return szImei;
}
//需要权限 android.permission.ACCESS_WIFI_STATE
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String m_szWLANMAC = wm.getConnectionInfo().getMacAddress();
if (!m_szWLANMAC.equals("")) {
return m_szWLANMAC;
}
//需要权限 android.permission.BLUETOOTH
BluetoothAdapter m_BluetoothAdapter = null;
m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
String m_szBTMAC = m_BluetoothAdapter.getAddress();
if (!m_szBTMAC.equals("")) {
return m_szBTMAC;
}
return getUniquePsuedoID();
}
//获得 Psuedo ID
public static String getUniquePsuedoID() {
String serial = null;
String m_szDevIDShort = "35" +
Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
Build.USER.length() % 10; //13 位
try {
serial = Build.class.getField("SERIAL").get(null).toString();
//API>=9 使用serial号
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
} catch (Exception exception) {
//serial需要一个初始化,随意值
serial = "serial";
}
//使用硬件信息拼凑出来的15位号码
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}
public static String getCPUSerial() {
String line = "";
String TAG = "aaa";
Log.e(TAG, " get_quck_Sn() ");
Class<?> c = null;
try {
c = Class.forName("android.os.SystemProperties");
Method get = c.getMethod("get", String.class);
line = (String) get.invoke(c, "ro.serialno");
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
Log.e(TAG, " get_quck_Sn() " + line);
System.out.println("设备串号" + line);
return line;
}
/**
* 判断网络连接状态
*
* @param context
* @return
*/
public static boolean isNetworkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 判断WiFi连接状态
*
* @param context
* @return
*/
public static boolean isWifiConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (mWiFiNetworkInfo != null) {
return mWiFiNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 判断移动网络状态
*
* @param context
* @return
*/
public static boolean isMobileConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (mMobileNetworkInfo != null) {
return mMobileNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 获取网络连接类型
*
* @param context
* @return
*/
public static int getConnectedType(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null && mNetworkInfo.isAvailable()) {
return mNetworkInfo.getType();
}
}
return -1;
}
/**
* 根据字符的起始和结束索引提取子串
*
* @param input 原始字符串
* @param startIndex 起始索引(包含,从0开始)
* @param endIndex 结束索引(不包含)
* @return 子串,若输入无效或索引越界则返回空字符串
*/
public static String getSubstringByIndices(String input, int startIndex, int endIndex) {
if (input == null) {
return "";
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(endIndex, input.length());
if (safeStart > safeEnd) {
return "";
}
return input.substring(safeStart, safeEnd);
}
public static String getSubstringByIndex(String input, int startIndex, int length) {
if (input == null) {
return "";
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(startIndex + length, input.length());
if (safeStart > safeEnd) {
return "";
}
return input.substring(safeStart, safeEnd);
}
/**
* 十进制转十六进制
*
* @param decimal
* @return
*/
public static String decimalToHexWithPadding(int decimal, int padding) {
// 将十进制转换为十六进制,并转换为字符串
String hex = Integer.toHexString(decimal);
// 确保字符串长度为至少4位,不足部分前面补0
while (hex.length() < padding) {
hex = "0" + hex;
}
return hex.toUpperCase(); // 返回大写形式的十六进制字符串
}
/**
* 十进制转二进制,且返回的二进制为至少7位数
*
* @param decimal
* @return
*/
public static String decimalToBinary(int decimal) {
// 如果输入为0,直接返回"0"
if (decimal == 0) {
return "0";
}
StringBuilder binary = new StringBuilder();
// 除2取余法,将余数加入二进制字符串
while (decimal > 0) {
int remainder = decimal % 2;
binary.insert(0, remainder);
decimal = decimal / 2;
}
int length = binary.length();
if (length < 7) {
int padding = 7 - length;
for (int i = 0; i < padding; i++) {
binary.insert(0, '0');
}
}
return binary.toString();
}
/**
* 将二进制字符串转换为十六进制字符串,每8位转换为两位十六进制,不足两位前面补零
*
* @param binaryStr 输入的二进制字符串(仅包含0和1)
* @return 转换后的十六进制字符串
* @throws IllegalArgumentException 如果输入不是有效的二进制字符串
*/
public static String binaryToHex(String binaryStr) {
// 校验输入合法性
if (binaryStr == null || !binaryStr.matches("[01]+")) {
throw new IllegalArgumentException("Invalid binary string");
}
// 补前导零使长度成为8的倍数
int length = binaryStr.length();
int padding = (8 - (length % 8)) % 8; // 计算需要补零的数量
StringBuilder paddedBinary = new StringBuilder();
for (int i = 0; i < padding; i++) {
paddedBinary.append('0');
}
paddedBinary.append(binaryStr);
// 每8位转换为两位十六进制
StringBuilder hexStr = new StringBuilder();
for (int i = 0; i < paddedBinary.length(); i += 8) {
String byteStr = paddedBinary.substring(i, i + 8);
int decimalValue = Integer.parseInt(byteStr, 2);
hexStr.append(String.format("%02X", decimalValue & 0xFF));
}
return hexStr.toString();
}
/**
* 数据校验 异或处理
*/
public static String getXor(String content) {
int a = 0;
for (int i = 0; i < content.length() / 2; i++) {
a = a ^ Integer.parseInt(content.substring(i * 2, (i * 2) + 2), 16);
}
String result = Integer.toHexString(a).toUpperCase();
if (result.length() == 1) {
return "0" + result;
} else {
return result;
}
}
public static double formatPersonInfo(String input, int startIndex, int length) {
if (input == null) {
return 0;
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(startIndex + length, input.length());
if (safeStart > safeEnd) {
return 0;
}
String result = input.substring(safeStart, safeEnd);
double num = Integer.parseInt(result, 16);
return num;
}
/**
* 安装apk
*
* @param activity
* @param apkFile
*/
public static void installApk(Context activity, File apkFile) {
//文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装
//因此,可能会存在权限问题,需要做下面的设置
//如果文件是sdcard上的,就不需要这个操作了
try {
apkFile.setExecutable(true, false);
apkFile.setReadable(true, false);
apkFile.setWritable(true, false);
} catch (Exception e) {
e.printStackTrace();
}
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
Uri uri;
//TODO N FileProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
uri = Uri.fromFile(apkFile);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
activity.startActivity(intent);
//TODO 0 INSTALL PERMISSION
//在AndroidManifest中加入权限即可
}
}
@@ -0,0 +1,265 @@
package com.sw.plate.utils;
public final class Base64 {
private static final int BASELENGTH = 128;
private static final int LOOKUPLENGTH = 64;
private static final int TWENTYFOURBITGROUP = 24;
private static final int EIGHTBIT = 8;
private static final int SIXTEENBIT = 16;
private static final int FOURBYTE = 4;
private static final int SIGN = -128;
private static char PAD = '=';
private static byte[] base64Alphabet = new byte[BASELENGTH];
private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
static {
for (int i = 0; i < BASELENGTH; ++i) {
base64Alphabet[i] = -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
base64Alphabet[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
base64Alphabet[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
base64Alphabet[i] = (byte) (i - '0' + 52);
}
base64Alphabet['+'] = 62;
base64Alphabet['/'] = 63;
for (int i = 0; i <= 25; i++) {
lookUpBase64Alphabet[i] = (char) ('A' + i);
}
for (int i = 26, j = 0; i <= 51; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('a' + j);
}
for (int i = 52, j = 0; i <= 61; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('0' + j);
}
lookUpBase64Alphabet[62] = (char) '+';
lookUpBase64Alphabet[63] = (char) '/';
}
private static boolean isWhiteSpace(char octect) {
return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
}
private static boolean isPad(char octect) {
return (octect == PAD);
}
private static boolean isData(char octect) {
return (octect < BASELENGTH && base64Alphabet[octect] != -1);
}
/**
* Encodes hex octects into Base64
*
* @param binaryData Array containing binaryData
* @return Encoded Base64 array
*/
public static String encode(byte[] binaryData) {
if (binaryData == null) {
return null;
}
int lengthDataBits = binaryData.length * EIGHTBIT;
if (lengthDataBits == 0) {
return "";
}
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1
: numberTriplets;
char encodedData[] = null;
encodedData = new char[numberQuartet * 4];
byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
int encodedIndex = 0;
int dataIndex = 0;
for (int i = 0; i < numberTriplets; i++) {
b1 = binaryData[dataIndex++];
b2 = binaryData[dataIndex++];
b3 = binaryData[dataIndex++];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6)
: (byte) ((b3) >> 6 ^ 0xfc);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
}
// form integral number of 6-bit groups
if (fewerThan24bits == EIGHTBIT) {
b1 = binaryData[dataIndex];
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
encodedData[encodedIndex++] = PAD;
encodedData[encodedIndex++] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
encodedData[encodedIndex++] = PAD;
}
return new String(encodedData);
}
/**
* Decodes Base64 data into octects
*
* @param encoded string containing Base64 data
* @return Array containind decoded data.
*/
public static byte[] decode(String encoded) {
if (encoded == null) {
return null;
}
char[] base64Data = encoded.toCharArray();
// remove white spaces
int len = removeWhiteSpace(base64Data);
if (len % FOURBYTE != 0) {
return null;// should be divisible by four
}
int numberQuadruple = (len / FOURBYTE);
if (numberQuadruple == 0) {
return new byte[0];
}
byte decodedData[] = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
int i = 0;
int encodedIndex = 0;
int dataIndex = 0;
decodedData = new byte[(numberQuadruple) * 3];
for (; i < numberQuadruple - 1; i++) {
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))
|| !isData((d3 = base64Data[dataIndex++]))
|| !isData((d4 = base64Data[dataIndex++]))) {
return null;
}// if found "no data" just return null
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))) {
return null;// if found "no data" just return null
}
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
d3 = base64Data[dataIndex++];
d4 = base64Data[dataIndex++];
if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
if (isPad(d3) && isPad(d4)) {
if ((b2 & 0xf) != 0)// last 4 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 1];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
return tmp;
} else if (!isPad(d3) && isPad(d4)) {
b3 = base64Alphabet[d3];
if ((b3 & 0x3) != 0)// last 2 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 2];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
return tmp;
} else {
return null;
}
} else { // No PAD e.g 3cQl
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
return decodedData;
}
/**
* remove WhiteSpace from MIME containing encoded Base64 data.
*
* @param data the byte array of base64 data (with WS)
* @return the new length
*/
private static int removeWhiteSpace(char[] data) {
if (data == null) {
return 0;
}
// count characters that's not whitespace
int newSize = 0;
int len = data.length;
for (int i = 0; i < len; i++) {
if (!isWhiteSpace(data[i])) {
data[newSize++] = data[i];
}
}
return newSize;
}
}
@@ -0,0 +1,179 @@
package com.sw.plate.utils;
public class ByteUtil {
/**
* 字节数组转换成对应的16进制表示的字符串
*
* @param src
* @return
*/
public static String bytes2HexStr(byte[] src) {
StringBuilder builder = new StringBuilder();
if (src == null || src.length <= 0) {
return "";
}
char[] buffer = new char[2];
for (int i = 0; i < src.length; i++) {
buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16);
buffer[1] = Character.forDigit(src[i] & 0x0F, 16);
builder.append(buffer);
}
return builder.toString().toUpperCase();
}
/**
* 十六进制字节数组转字符串
*
* @param src 目标数组
* @param dec 起始位置
* @param length 长度
* @return
*/
public static String bytes2HexStr(byte[] src, int dec, int length) {
byte[] temp = new byte[length];
System.arraycopy(src, dec, temp, 0, length);
return bytes2HexStr(temp);
}
/**
* 16进制字符串转10进制数字
*
* @param hex
* @return
*/
public static long hexStr2decimal(String hex) {
return Long.parseLong(hex, 16);
}
/**
* 把十进制数字转换成足位的十六进制字符串,并补全空位
*
* @param num
* @return
*/
public static String decimal2fitHex(long num) {
String hex = Long.toHexString(num).toUpperCase();
if (hex.length() % 2 != 0) {
return "0" + hex;
}
return hex.toUpperCase();
}
/**
* 把十进制数字转换成足位的十六进制字符串,并补全空位
*
* @param num
* @param strLength 字符串的长度
* @return
*/
public static String decimal2fitHex(long num, int strLength) {
String hexStr = decimal2fitHex(num);
StringBuilder stringBuilder = new StringBuilder(hexStr);
while (stringBuilder.length() < strLength) {
stringBuilder.insert(0, '0');
}
return stringBuilder.toString();
}
public static String fitDecimalStr(int dicimal, int strLength) {
StringBuilder builder = new StringBuilder(String.valueOf(dicimal));
while (builder.length() < strLength) {
builder.insert(0, "0");
}
return builder.toString();
}
/**
* 字符串转十六进制字符串
*
* @param str
* @return
*/
public static String str2HexString(String str) {
char[] chars = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder();
byte[] bs = null;
try {
bs = str.getBytes("utf8");
} catch (Exception e) {
e.printStackTrace();
}
int bit;
for (int i = 0; i < bs.length; i++) {
bit = (bs[i] & 0x0f0) >> 4;
sb.append(chars[bit]);
bit = bs[i] & 0x0f;
sb.append(chars[bit]);
}
return sb.toString();
}
/**
* 把十六进制表示的字节数组字符串,转换成十六进制字节数组
*
* @param
* @return byte[]
*/
public static byte[] hexStr2bytes(String hex) {
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] achar = hex.toUpperCase().toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (hexChar2byte(achar[pos]) << 4 | hexChar2byte(achar[pos + 1]));
}
return result;
}
/**
* 把16进制字符[0123456789abcde](含大小写)转成字节
*
* @param c
* @return
*/
private static int hexChar2byte(char c) {
switch (c) {
case '0':
return 0;
case '1':
return 1;
case '2':
return 2;
case '3':
return 3;
case '4':
return 4;
case '5':
return 5;
case '6':
return 6;
case '7':
return 7;
case '8':
return 8;
case '9':
return 9;
case 'a':
case 'A':
return 10;
case 'b':
case 'B':
return 11;
case 'c':
case 'C':
return 12;
case 'd':
case 'D':
return 13;
case 'e':
case 'E':
return 14;
case 'f':
case 'F':
return 15;
default:
return -1;
}
}
}
@@ -0,0 +1,230 @@
package com.sw.plate.utils;
import java.util.HashMap;
import java.util.Map;
public class CabinetLockCommand {
/**
* 生成开柜指令(含校验位)
*
* @param boxNumber 柜门号(1-65535
* @return 十六进制格式指令字符串,如 "5A2100017A"
*/
public static String generateOpenCommand(int boxNumber) {
if (boxNumber < 1 || boxNumber > 0xFFFF) {
throw new IllegalArgumentException("柜门号范围应为1-65535");
}
// 固定头+功能码
byte head = 0x5A;
byte functionCode = 0x21;
// 大端序箱门号(2字节)
byte[] boxCh = {
(byte) ((boxNumber >> 8) & 0xFF),
(byte) (boxNumber & 0xFF)
};
// 计算异或校验(head + functionCode + boxCh
byte xorCheck = head;
xorCheck ^= functionCode;
xorCheck ^= boxCh[0];
xorCheck ^= boxCh[1];
// 拼接完整指令
return String.format("%02X%02X%02X%02X%02X",
head, functionCode, boxCh[0], boxCh[1], xorCheck);
}
/**
* 生成查询开关门指令(含校验位)
*
* @param boxNumber 柜门号(1-65535
* @return 十六进制格式指令字符串,如 "5A2100017A"
*/
public static String generateBoxStatusCommand(int boxNumber) {
if (boxNumber < 1 || boxNumber > 0xFFFF) {
throw new IllegalArgumentException("柜门号范围应为1-65535");
}
// 固定头+功能码
byte head = 0x5A;
byte functionCode = 0x22;
// 大端序箱门号(2字节)
byte[] boxCh = {
(byte) ((boxNumber >> 8) & 0xFF),
(byte) (boxNumber & 0xFF)
};
// 计算异或校验(head + functionCode + boxCh
byte xorCheck = head;
xorCheck ^= functionCode;
xorCheck ^= boxCh[0];
xorCheck ^= boxCh[1];
// 拼接完整指令
return String.format("%02X%02X%02X%02X%02X",
head, functionCode, boxCh[0], boxCh[1], xorCheck);
}
/**
* 生成查询是否存放指令(含校验位)
*
* @param boxNumber 柜门号(1-65535
* @return 十六进制格式指令字符串,如 "5A2100017A"
*/
public static String generateBoxHasCommand(int boxNumber) {
if (boxNumber < 1 || boxNumber > 0xFFFF) {
throw new IllegalArgumentException("柜门号范围应为1-65535");
}
// 固定头+功能码
byte head = 0x5A;
byte functionCode = 0x25;
// 大端序箱门号(2字节)
byte[] boxCh = {
(byte) ((boxNumber >> 8) & 0xFF),
(byte) (boxNumber & 0xFF)
};
// 计算异或校验(head + functionCode + boxCh
byte xorCheck = head;
xorCheck ^= functionCode;
xorCheck ^= boxCh[0];
xorCheck ^= boxCh[1];
// 拼接完整指令
return String.format("%02X%02X%02X%02X%02X",
head, functionCode, boxCh[0], boxCh[1], xorCheck);
}
private static final byte TURN_ON = (byte) 0xB1;
private static final byte TURN_OFF = (byte) 0xB2;
private static final byte TURN_UVC_ON = (byte) 0xB3;
private static final byte TURN_UVC_OFF = (byte) 0xB4;
/**
* 生成灯光控制指令
*
* @param deviceNumber 设备号(1-255)
* @param isTurnOn true=开灯, false=关灯
* @return 十六进制指令字符串
*/
public static String generateLightCommand(int deviceNumber, boolean isTurnOn) {
if (deviceNumber < 1 || deviceNumber > 255) {
throw new IllegalArgumentException("设备号范围应为1-255");
}
byte[] command = new byte[5];
command[0] = 0x55;
command[1] = (byte) deviceNumber;
command[2] = isTurnOn ? TURN_ON : TURN_OFF;
command[3] = 0x5F;
command[4] = 0x00;
// 计算校验位
byte checksum = command[0];
for (int i = 1; i < command.length - 1; i++) {
checksum ^= command[i];
}
command[command.length - 1] = checksum;
// 转换为十六进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : command) {
sb.append(String.format("%02X", b));
}
return sb.toString().trim();
}
/**
* 生成紫外线灯光控制指令
*
* @param deviceNumber 设备号(1-255)
* @param isTurnOn true=开灯, false=关灯
* @return 十六进制指令字符串
*/
public static String generateUVCLightCommand(int deviceNumber, boolean isTurnOn) {
if (deviceNumber < 1 || deviceNumber > 255) {
throw new IllegalArgumentException("设备号范围应为1-255");
}
byte[] command = new byte[5];
command[0] = 0x55;
command[1] = (byte) deviceNumber;
command[2] = isTurnOn ? TURN_UVC_ON : TURN_UVC_OFF;
command[3] = 0x5F;
command[4] = 0x00;
// 计算校验位
byte checksum = command[0];
for (int i = 1; i < command.length - 1; i++) {
checksum ^= command[i];
}
command[command.length - 1] = checksum;
// 转换为十六进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : command) {
sb.append(String.format("%02X", b));
}
return sb.toString().trim();
}
/**
* 解析箱门状态数据
*
* @param data 原始数据字符串,如"5AA2000100161008E017"
* @return 包含所有箱门状态的Map,key为箱门号,value为开关状态(true=开)
*/
public static Map<Integer, Boolean> parseBoxStatus(String data) {
Map<Integer, Boolean> statusMap = new HashMap<>();
// 验证数据长度至少要有10个字符(5字节)
if (data == null || data.length() < 10) {
return statusMap;
}
try {
// 解析起始箱号和结束箱号
int startBox = Integer.parseInt(data.substring(4, 8), 16);
int endBox = Integer.parseInt(data.substring(8, 12), 16);
// 计算箱门总数和需要的字节数
int boxCount = endBox - startBox + 1;
int byteCount = (boxCount + 7) / 8;
// 验证数据长度是否足够
if (data.length() < 12 + byteCount * 2) {
return statusMap;
}
// 解析状态字节
String stateStr = data.substring(12, 12 + byteCount * 2);
// 处理每个字节
for (int i = 0; i < byteCount; i++) {
// 获取当前字节(低字节在前)
String byteStr = stateStr.substring(i * 2, i * 2 + 2);
int byteValue = Integer.parseInt(byteStr, 16);
// 处理字节中的每一位
for (int bit = 0; bit < 8; bit++) {
int boxNum = startBox + i * 8 + bit;
if (boxNum > endBox) break;
boolean isOpen = ((byteValue >> bit) & 0x01) == 0x01;
statusMap.put(boxNum, isOpen);
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
return statusMap;
}
}
@@ -0,0 +1,175 @@
package com.sw.plate.utils;
import android.os.SystemClock;
import android.util.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* Created by Administrator on 2018/11/9.
*/
public class GpioUtils {
private static final String TAG = "GpioUtils";
/*
给export申请权限
*/
public static void upgradeRootPermissionForExport() {
upgradeRootPermission("/sys/class/gpio/export");
}
/*
配置一个gpio路径
*/
public static boolean exportGpio(int gpio) {
return writeNode("/sys/class/gpio/export", "" + gpio);
}
/*
给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用
*/
public static void upgradeRootPermissionForGpio(int gpio) {
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction");
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value");
}
/*
设置io口为输入或输出
*/
public static boolean setGpioDirection(int gpio, int arg) {
String gpioDirection = "";
if (arg == 0) gpioDirection = "out";
else if (arg == 1) gpioDirection = "in";
else return false;
return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection);
}
/*
获取io口的状态为输出还是输入
*/
public static String getGpioDirection(int gpio) {
String gpioDirection = "";
gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction");
return gpioDirection;
}
/*
给输出io口写值,高电平或低电平
*/
public static boolean writeGpioValue(int gpio, String arg) {
return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg);
}
//获取当前gpio是高还是低
public static String getGpioValue(int gpio) {
return readNode("/sys/class/gpio/gpio" + gpio + "/value");
}
private static boolean upgradeRootPermission(String path) {
Process process = null;
DataOutputStream os = null;
try {
String cmd = "chmod 777 " + path;
process = Runtime.getRuntime().exec("su"); //切换到root帐号
os = new DataOutputStream(process.getOutputStream());
os.writeBytes(cmd + "\n");
os.writeBytes("exit\n");
os.flush();
process.waitFor();
} catch (Exception e) {
} finally {
try {
if (os != null) {
os.close();
}
process.destroy();
} catch (Exception e) {
}
}
try {
return process.waitFor() == 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
private static boolean writeNode(String path, String arg) {
Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg);
if (path == null || arg == null) {
Log.e(TAG, "set node error");
return false;
}
FileWriter fileWriter = null;
BufferedWriter bufferedWriter = null;
try {
fileWriter = new FileWriter(path);
fileWriter.write(arg);
} catch (Exception e) {
Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg);
e.printStackTrace();
return false;
} finally {
try {
if (fileWriter != null) {
fileWriter.close();
}
if (bufferedWriter != null) {
bufferedWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private static long mTime = 0;
private static int mFailTimes = 0;
private static String readNode(String path) {
String result = "";
FileReader fread = null;
BufferedReader buffer = null;
try {
fread = new FileReader(path);
buffer = new BufferedReader(fread);
String str = null;
while ((str = buffer.readLine()) != null) {
result = str;
break;
}
mFailTimes = 0;
} catch (IOException e) {
Log.e(TAG, "IO Exception");
e.printStackTrace();
if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) {
mFailTimes++;
}
if (mFailTimes >= 3) {
Log.d(TAG, "read format node continuous failed three times, exist thread");
}
} finally {
try {
if (buffer != null) {
buffer.close();
}
if (fread != null) {
fread.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
@@ -0,0 +1,57 @@
package com.sw.plate.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -0,0 +1,25 @@
package com.sw.plate.utils;
public class LightManager {
private static final String TAG = "LightManager";
public static void openGreenLight() {
L.e(TAG, "openGreenLight");
GpioUtils.writeGpioValue(41, "1");
}
public static void closeGreenLight() {
L.e(TAG, "closeGreenLight");
GpioUtils.writeGpioValue(41, "0");
}
public static void openRedLight() {
L.e(TAG, "openRedLight");
GpioUtils.writeGpioValue(40, "1");
}
public static void closeRedLight() {
L.e(TAG, "closeRedLight");
GpioUtils.writeGpioValue(40, "0");
}
}

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