yaorongkeji 8 месяцев назад
Родитель
Сommit
fecb7b3c4f

+ 5 - 0
baselib/src/main/java/com/yingyangfly/baselib/router/RouterUrlCommon.kt

@@ -259,4 +259,9 @@ object RouterUrlCommon {
      * 其他测评报告
      */
     const val otherReviewResultDetail = "/otherReviewResultDetail/otherReviewResultDetail"
+
+    /**
+     * 人脸登录
+     */
+    const val humanFaceLogin = "/login/humanFaceLogin"
 }

+ 1 - 0
baselib/src/main/res/values/dimens.xml

@@ -29,6 +29,7 @@
     <dimen name="divider_432px" tools:ignore="ResourceName">432px</dimen>
     <dimen name="divider_430px" tools:ignore="ResourceName">430px</dimen>
     <dimen name="divider_427px" tools:ignore="ResourceName">427px</dimen>
+    <dimen name="divider_420px" tools:ignore="ResourceName">420px</dimen>
     <dimen name="divider_418px" tools:ignore="ResourceName">418px</dimen>
     <dimen name="divider_416px" tools:ignore="ResourceName">416px</dimen>
     <dimen name="divider_415px" tools:ignore="ResourceName">415px</dimen>

+ 1 - 1
config.gradle

@@ -13,7 +13,7 @@ ext {
             applicationId    : "com.yingyangfly",
             minSdkVersion    : 26,
             targetSdkVersion : 30,
-            versionCode      : 34,
+            versionCode      : 35,
             versionName      : "1.0.0",
     ]
     //androidx配置

+ 12 - 0
login/build.gradle

@@ -21,4 +21,16 @@ dependencies {
     implementation(rootProject.ext.androidx.appcompat)
     implementation(rootProject.ext.androidx.material)
     implementation(rootProject.ext.androidx.constraintlayout)
+
+    implementation "androidx.camera:camera-core:1.0.2"
+    implementation "androidx.camera:camera-camera2:1.0.2"
+    implementation "androidx.camera:camera-lifecycle:1.0.2"
+    implementation "androidx.camera:camera-view:1.0.0-alpha27"
+    // 注意:camera-extensions 在某些设备上可能不稳定,如无特效需求可先注释
+    implementation "androidx.camera:camera-extensions:1.0.0-alpha27"
+
+
+//    // 其他库
+    implementation "com.guolindev.permissionx:permissionx:1.6.4"
+    implementation "com.google.mlkit:face-detection:16.1.5"
 }

+ 10 - 0
login/src/main/AndroidManifest.xml

@@ -7,6 +7,16 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
 
+    <!-- 相机权限 -->
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <!-- 如果需要录制视频 -->
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <!-- 声明相机特性 -->
+    <uses-feature android:name="android.hardware.camera" android:required="true" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+
     <application>
         <activity
             android:name="com.yingyang.login.ui.login.LoginActivity"

+ 7 - 0
login/src/main/java/com/yingyang/login/net/LoginApiService.kt

@@ -6,6 +6,8 @@ import com.yingyangfly.baselib.bean.UserInfoBean
 import com.yingyangfly.baselib.net.BaseResp
 import okhttp3.RequestBody
 import retrofit2.http.Body
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
 import retrofit2.http.POST
 import retrofit2.http.Query
 
@@ -61,4 +63,9 @@ interface LoginApiService {
      */
     @POST("app/selectNewVersion")
     suspend fun selectNewVersion(@Query("versionCode") versionCode: Int): BaseResp<UpdateBean>
+
+    @FormUrlEncoded
+    @POST("app/humanFace/login")
+    suspend fun humanFaceLogin(@Field("imageBase") imageBase: String): BaseResp<String>
+
 }

+ 133 - 0
login/src/main/java/com/yingyang/login/service/FaceOverlayView.kt

@@ -0,0 +1,133 @@
+package com.yingyang.login.service
+
+import android.content.Context
+import android.graphics.*
+import android.util.AttributeSet
+import android.view.View
+import com.google.mlkit.vision.face.Face
+import com.google.mlkit.vision.face.FaceLandmark
+
+class FaceOverlayView : View {
+
+    private val facePaint = Paint().apply {
+        color = Color.GREEN
+        style = Paint.Style.STROKE
+        strokeWidth = 4f
+    }
+
+    private val landmarkPaint = Paint().apply {
+        color = Color.RED
+        style = Paint.Style.FILL
+        strokeWidth = 2f
+    }
+
+    private var faces: List<Face> = emptyList()
+    private var imageWidth = 0
+    private var imageHeight = 0
+
+    constructor(context: Context) : super(context)
+    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+    fun setFaces(faces: List<Face>, imageWidth: Int, imageHeight: Int) {
+        this.faces = faces
+        this.imageWidth = imageWidth
+        this.imageHeight = imageHeight
+        invalidate()
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        if (faces.isEmpty() || imageWidth == 0 || imageHeight == 0) {
+            return
+        }
+
+        val scaleX = width.toFloat() / imageWidth
+        val scaleY = height.toFloat() / imageHeight
+
+        for (face in faces) {
+            val boundingBox = face.boundingBox
+
+            val left = boundingBox.left * scaleX
+            val top = boundingBox.top * scaleY
+            val right = boundingBox.right * scaleX
+            val bottom = boundingBox.bottom * scaleY
+
+            // 绘制人脸框
+            canvas.drawRect(left, top, right, bottom, facePaint)
+
+            // 绘制人脸关键点
+            drawLandmarks(canvas, face, scaleX, scaleY)
+
+            // 绘制人脸质量信息
+            drawFaceInfo(canvas, face, left, top, scaleX, scaleY)
+        }
+    }
+
+    private fun drawLandmarks(canvas: Canvas, face: Face, scaleX: Float, scaleY: Float) {
+        // 左眼
+        face.getLandmark(FaceLandmark.LEFT_EYE)?.let { landmark ->
+            val x = landmark.position.x * scaleX
+            val y = landmark.position.y * scaleY
+            canvas.drawCircle(x, y, 8f, landmarkPaint)
+        }
+
+        // 右眼
+        face.getLandmark(FaceLandmark.RIGHT_EYE)?.let { landmark ->
+            val x = landmark.position.x * scaleX
+            val y = landmark.position.y * scaleY
+            canvas.drawCircle(x, y, 8f, landmarkPaint)
+        }
+
+        // 鼻子
+        face.getLandmark(FaceLandmark.NOSE_BASE)?.let { landmark ->
+            val x = landmark.position.x * scaleX
+            val y = landmark.position.y * scaleY
+            canvas.drawCircle(x, y, 6f, landmarkPaint)
+        }
+
+        // 嘴巴
+        // 嘴巴关键点 - 使用 MOUTH_LEFT 和 MOUTH_RIGHT
+        face.getLandmark(FaceLandmark.MOUTH_LEFT)?.let { landmark ->
+            val x = landmark.position.x * scaleX
+            val y = landmark.position.y * scaleY
+            canvas.drawCircle(x, y, 4f, landmarkPaint)
+        }
+
+        face.getLandmark(FaceLandmark.MOUTH_RIGHT)?.let { landmark ->
+            val x = landmark.position.x * scaleX
+            val y = landmark.position.y * scaleY
+            canvas.drawCircle(x, y, 4f, landmarkPaint)
+        }
+    }
+
+    private fun drawFaceInfo(canvas: Canvas, face: Face, left: Float, top: Float, scaleX: Float, scaleY: Float) {
+        val infoPaint = Paint().apply {
+            color = Color.YELLOW
+            textSize = 24f
+            style = Paint.Style.FILL
+        }
+
+        var infoText = "Face"
+        val y = top - 10
+
+        // 添加微笑概率
+        face.smilingProbability?.let { probability ->
+            if (probability > 0.5f) {
+                infoText += " 😊"
+            }
+        }
+
+        // 添加左右眼状态
+        face.leftEyeOpenProbability?.let { leftProb ->
+            face.rightEyeOpenProbability?.let { rightProb ->
+                if (leftProb > 0.5f && rightProb > 0.5f) {
+                    infoText += " 👀"
+                }
+            }
+        }
+
+        canvas.drawText(infoText, left, y, infoPaint)
+    }
+}

+ 70 - 0
login/src/main/java/com/yingyang/login/service/FaceOverlayView2.kt

@@ -0,0 +1,70 @@
+package com.yingyang.login.service
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import com.google.mlkit.vision.face.Face
+import kotlin.math.min
+
+class FaceOverlayView2 @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+    private val faceRectPaint = Paint().apply {
+        color = Color.GREEN
+        style = Paint.Style.STROKE
+        strokeWidth = 3f
+        isAntiAlias = true
+    }
+
+    private var faces: List<Face> = emptyList()
+    private var scaleFactor = 1.0f
+    private var imageWidth = 1
+    private var imageHeight = 1
+
+    fun setFaces(
+        faces: List<Face>,
+        imageWidth: Int,
+        imageHeight: Int
+    ) {
+        this.faces = faces
+        this.imageWidth = imageWidth
+        this.imageHeight = imageHeight
+
+        // 计算缩放因子
+        scaleFactor = if (imageWidth > 0 && imageHeight > 0) {
+            min(width * 1f / imageWidth, height * 1f / imageHeight)
+        } else {
+            1.0f
+        }
+
+        invalidate()
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        for (face in faces) {
+            // 绘制人脸框
+            val rect = face.boundingBox
+            val scaledRect = Rect(
+                (rect.left * scaleFactor).toInt(),
+                (rect.top * scaleFactor).toInt(),
+                (rect.right * scaleFactor).toInt(),
+                (rect.bottom * scaleFactor).toInt()
+            )
+            canvas.drawRect(scaledRect, faceRectPaint)
+        }
+    }
+
+    fun clearFaces() {
+        faces = emptyList()
+        invalidate()
+    }
+}

+ 648 - 0
login/src/main/java/com/yingyang/login/ui/login/FaceCaptureActivity.kt

@@ -0,0 +1,648 @@
+package com.yingyang.login.ui.login
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.TextUtils
+import android.util.Base64
+import android.util.Log
+import android.util.Size
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.*
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.alibaba.android.arouter.launcher.ARouter
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.face.Face
+import com.google.mlkit.vision.face.FaceDetection
+import com.google.mlkit.vision.face.FaceDetectorOptions
+import com.yingyang.login.service.FaceOverlayView
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import com.yingyang.login.R
+import com.yingyang.login.service.InitService
+import com.yingyangfly.baselib.ext.toast
+import com.yingyangfly.baselib.router.RouterUrlCommon
+import com.yingyangfly.baselib.utils.JumpUtil
+import com.yingyangfly.baselib.utils.User
+
+@Route(path = RouterUrlCommon.humanFaceLogin)
+class FaceCaptureActivity : AppCompatActivity() {
+
+    // UI组件
+    private lateinit var previewView: PreviewView
+    private lateinit var startButton: Button
+    private lateinit var statusTextView: TextView
+    private lateinit var attemptsTextView: TextView
+    private lateinit var progressBar: ProgressBar
+    private lateinit var faceOverlayView: FaceOverlayView
+    private lateinit var iconBack: ImageView
+
+    // CameraX
+    private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
+    private var imageCapture: ImageCapture? = null
+    private var camera: Camera? = null
+    private lateinit var cameraExecutor: ExecutorService
+
+    // 人脸检测
+    private lateinit var faceDetector: com.google.mlkit.vision.face.FaceDetector
+
+    // 自动捕获状态
+    private var isAutoCaptureEnabled = false
+    private var isProcessingPhoto = false
+    private var captureAttempts = AtomicInteger(0)
+    private val maxCaptureAttempts = 3
+    private var currentAttempt = 1
+
+    // 人脸质量检测
+    private var stableFaceFrames = 0
+    private val requiredStableFrames = 5
+    private var lastFaceSize = 0f
+    private var lastFacePositionX = 0f
+    private var lastFacePositionY = 0f
+    private var lastDetectionTime = 0L
+    private val detectionCooldown = 1000L  // 1秒冷却时间
+
+    // Handler for delayed tasks
+    private val mainHandler = Handler(Looper.getMainLooper())
+
+    // 声明 ViewModel
+    private val viewModel: LoginViewModel by viewModels()
+
+    private var token: String = ""
+
+    // 新增:防止重复请求的变量
+    private var isRequestInProgress = false
+    private var lastCaptureTime = 0L
+    private val minCaptureInterval = 2000L // 最小2秒间隔
+
+    companion object {
+        private const val TAG = "FaceCapture"
+        private const val REQUEST_CODE_PERMISSIONS = 100
+        private const val FACE_SIZE_THRESHOLD = 0.2f
+        private const val FACE_MOVEMENT_THRESHOLD = 0.05f
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.login_activity_face_capture)
+
+        initViews()
+        initFaceDetector()
+
+        if (allPermissionsGranted()) {
+            startCamera()
+        } else {
+            requestPermissions()
+        }
+    }
+
+    private fun initViews() {
+        previewView = findViewById(R.id.preview_view)
+        startButton = findViewById(R.id.capture_button)
+        statusTextView = findViewById(R.id.status_text)
+        attemptsTextView = findViewById(R.id.attempts_text)
+        progressBar = findViewById(R.id.progress_bar)
+        faceOverlayView = findViewById(R.id.face_overlay)
+        iconBack = findViewById(R.id.icon_back)
+
+        cameraExecutor = Executors.newSingleThreadExecutor()
+
+        startButton.setOnClickListener {
+            if (!isAutoCaptureEnabled) {
+                startAutoCapture()
+            } else {
+                stopAutoCapture()
+            }
+        }
+
+        iconBack.setOnClickListener {
+            JumpUtil.jumpActivity(RouterUrlCommon.login, this)
+        }
+
+        updateUIState()
+    }
+
+    private fun updateUIState() {
+        attemptsTextView.text = "尝试次数: $currentAttempt/$maxCaptureAttempts"
+
+        if (isAutoCaptureEnabled) {
+            startButton.text = "停止"
+            statusTextView.text = "正在检测人脸..."
+        } else {
+            startButton.text = "开始"
+            if (currentAttempt > 1) {
+                statusTextView.text = "等待重新开始..."
+            } else {
+                statusTextView.text = "准备就绪"
+            }
+        }
+    }
+
+    private fun initFaceDetector() {
+        val options = FaceDetectorOptions.Builder()
+            .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
+            .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
+            .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
+            .setMinFaceSize(0.15f)
+            .build()
+
+        faceDetector = FaceDetection.getClient(options)
+    }
+
+    private fun allPermissionsGranted(): Boolean {
+        val requiredPermissions = mutableListOf<String>().apply {
+            add(Manifest.permission.CAMERA)
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+                add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            }
+        }
+        return requiredPermissions.all {
+            ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
+        }
+    }
+
+    private fun requestPermissions() {
+        val permissions = mutableListOf<String>().apply {
+            add(Manifest.permission.CAMERA)
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+                add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            }
+        }
+        ActivityCompat.requestPermissions(this, permissions.toTypedArray(), REQUEST_CODE_PERMISSIONS)
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        if (requestCode == REQUEST_CODE_PERMISSIONS) {
+            if (allPermissionsGranted()) {
+                startCamera()
+            } else {
+                Toast.makeText(this, "需要相机权限才能使用", Toast.LENGTH_SHORT).show()
+                finish()
+            }
+        }
+    }
+
+    private fun startCamera() {
+        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+        cameraProviderFuture.addListener(Runnable {
+            val cameraProvider = cameraProviderFuture.get()
+            val rotation = previewView.display.rotation
+
+            // 预览配置
+            val preview = Preview.Builder()
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setTargetRotation(rotation)
+                .build()
+                .also {
+                    it.setSurfaceProvider(previewView.surfaceProvider)
+                }
+
+            // 图像捕获配置
+            imageCapture = ImageCapture.Builder()
+                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setTargetRotation(rotation)
+                .setFlashMode(ImageCapture.FLASH_MODE_OFF)
+                .build()
+
+            // 图像分析配置
+            val imageAnalyzer = ImageAnalysis.Builder()
+                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+                .setTargetRotation(rotation)
+                .apply {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                        setTargetResolution(Size(640, 480))
+                    }
+                }
+                .build()
+                .also { analyzer ->
+                    analyzer.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy ->
+                        analyzeImage(imageProxy)
+                    })
+                }
+
+            // 选择摄像头
+            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+
+            try {
+                cameraProvider.unbindAll()
+                camera = cameraProvider.bindToLifecycle(
+                    this, cameraSelector, preview, imageCapture, imageAnalyzer
+                )
+                updateStatus("相机就绪")
+            } catch (e: Exception) {
+                Log.e(TAG, "相机启动失败: ${e.message}")
+                updateStatus("相机启动失败")
+                Toast.makeText(this, "相机启动失败", Toast.LENGTH_SHORT).show()
+            }
+        }, ContextCompat.getMainExecutor(this))
+    }
+
+    private fun analyzeImage(imageProxy: ImageProxy) {
+        // 新增:添加时间间隔检查
+        val currentTime = System.currentTimeMillis()
+        if (currentTime - lastCaptureTime < minCaptureInterval) {
+            imageProxy.close()
+            return
+        }
+
+        if (!isAutoCaptureEnabled || isProcessingPhoto) {
+            imageProxy.close()
+            return
+        }
+
+        val mediaImage = imageProxy.image
+        if (mediaImage == null) {
+            imageProxy.close()
+            return
+        }
+
+        // 检查冷却时间
+        if (currentTime - lastDetectionTime < detectionCooldown) {
+            imageProxy.close()
+            return
+        }
+
+        val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
+
+        faceDetector.process(image)
+            .addOnSuccessListener { faces ->
+                handleFaceDetectionResult(faces, imageProxy)
+            }
+            .addOnFailureListener { e ->
+                Log.e(TAG, "人脸检测失败: ${e.message}")
+                imageProxy.close()
+            }
+            .addOnCompleteListener {
+                // 确保 imageProxy 被关闭 - 直接调用 close() 是安全的
+                imageProxy.close()
+            }
+    }
+
+    private fun handleFaceDetectionResult(faces: List<Face>, imageProxy: ImageProxy) {
+        runOnUiThread {
+            faceOverlayView.setFaces(faces, imageProxy.width, imageProxy.height)
+
+            if (faces.isEmpty()) {
+                stableFaceFrames = 0
+                updateStatus("未检测到人脸")
+                imageProxy.close()
+                return@runOnUiThread
+            }
+
+            val face = faces.first()
+            val boundingBox = face.boundingBox
+
+            // 计算人脸位置和大小
+            val faceCenterX = (boundingBox.left + boundingBox.right) / 2f / imageProxy.width
+            val faceCenterY = (boundingBox.top + boundingBox.bottom) / 2f / imageProxy.height
+            val faceSize = boundingBox.width().toFloat() / imageProxy.width
+
+            // 检查人脸质量
+            val isFaceCentered = Math.abs(faceCenterX - 0.5f) < 0.1f && Math.abs(faceCenterY - 0.5f) < 0.1f
+            val isFaceGoodSize = faceSize > FACE_SIZE_THRESHOLD
+
+            if (isFaceCentered && isFaceGoodSize) {
+                // 检查人脸稳定性
+                if (stableFaceFrames == 0 ||
+                    (Math.abs(faceSize - lastFaceSize) < FACE_MOVEMENT_THRESHOLD &&
+                            Math.abs(faceCenterX - lastFacePositionX) < FACE_MOVEMENT_THRESHOLD &&
+                            Math.abs(faceCenterY - lastFacePositionY) < FACE_MOVEMENT_THRESHOLD)) {
+
+                    stableFaceFrames++
+                    updateStatus("检测到人脸,保持姿势 ($stableFaceFrames/$requiredStableFrames)")
+
+                    if (stableFaceFrames >= requiredStableFrames) {
+                        // 人脸稳定,触发自动拍照
+                        lastDetectionTime = System.currentTimeMillis()
+                        autoCaptureFace(imageProxy)
+                        return@runOnUiThread
+                    }
+                } else {
+                    stableFaceFrames = 1
+                    updateStatus("检测到人脸,请保持稳定")
+                }
+
+                lastFaceSize = faceSize
+                lastFacePositionX = faceCenterX
+                lastFacePositionY = faceCenterY
+            } else {
+                stableFaceFrames = 0
+                if (!isFaceCentered) {
+                    updateStatus("请将人脸对准中心")
+                } else if (!isFaceGoodSize) {
+                    updateStatus("请靠近摄像头")
+                }
+            }
+
+            imageProxy.close()
+        }
+    }
+
+    private fun startAutoCapture() {
+        isAutoCaptureEnabled = true
+        stableFaceFrames = 0
+        captureAttempts.set(0)
+        currentAttempt = 1
+        isRequestInProgress = false // 重置请求状态
+        updateUIState()
+        updateStatus("开始检测人脸...")
+    }
+
+    private fun stopAutoCapture() {
+        isAutoCaptureEnabled = false
+        isRequestInProgress = false // 重置请求状态
+        updateUIState()
+        updateStatus("已停止")
+    }
+
+    private fun autoCaptureFace(imageProxy: ImageProxy) {
+        // 新增:添加双重检查,确保不会同时处理多个请求
+        if (isProcessingPhoto || isRequestInProgress) {
+            Log.d(TAG, "已有请求在处理中,跳过本次拍照")
+            imageProxy.close()
+            return
+        }
+
+        isProcessingPhoto = true
+        isRequestInProgress = true
+        updateStatus("正在拍照...")
+        showProgressBar(true)
+
+        // 重置人脸稳定计数器,防止连续拍照
+        stableFaceFrames = 0
+        lastDetectionTime = System.currentTimeMillis()
+        lastCaptureTime = System.currentTimeMillis()
+
+        // 从 ImageProxy 获取图片并转换为 Base64
+        val base64Image = convertImageProxyToBase64(imageProxy)
+        imageProxy.close()
+
+        if (base64Image.isNotEmpty()) {
+            // 增加冷却时间,防止短时间内多次发送
+            mainHandler.postDelayed({
+                sendToServer(base64Image)
+            }, 500) // 添加500ms延迟
+        } else {
+            handleCaptureError("图片转换失败")
+        }
+    }
+
+    private fun convertImageProxyToBase64(imageProxy: ImageProxy): String {
+        val mediaImage = imageProxy.image ?: return ""
+
+        try {
+            // 将 YUV_420_888 转换为 JPEG
+            val yBuffer = mediaImage.planes[0].buffer
+            val uBuffer = mediaImage.planes[1].buffer
+            val vBuffer = mediaImage.planes[2].buffer
+
+            val ySize = yBuffer.remaining()
+            val uSize = uBuffer.remaining()
+            val vSize = vBuffer.remaining()
+
+            val nv21 = ByteArray(ySize + uSize + vSize)
+
+            // Y plane
+            yBuffer.get(nv21, 0, ySize)
+            // V plane
+            vBuffer.get(nv21, ySize, vSize)
+            // U plane
+            uBuffer.get(nv21, ySize + vSize, uSize)
+
+            val yuvImage = YuvImage(nv21, ImageFormat.NV21, mediaImage.width, mediaImage.height, null)
+            val outputStream = ByteArrayOutputStream()
+            yuvImage.compressToJpeg(Rect(0, 0, mediaImage.width, mediaImage.height), 80, outputStream)
+
+            val jpegBytes = outputStream.toByteArray()
+            return Base64.encodeToString(jpegBytes, Base64.NO_WRAP)
+
+        } catch (e: Exception) {
+            Log.e(TAG, "图片转换失败: ${e.message}")
+            return ""
+        }
+    }
+
+    private fun sendToServer(base64Image: String) {
+        updateStatus("正在验证...")
+
+        // 再次检查请求状态,避免重复请求
+        if (isRequestInProgress.not() || isProcessingPhoto.not()) {
+            Log.w(TAG, "请求状态异常,跳过本次请求")
+            isProcessingPhoto = false
+            isRequestInProgress = false
+            showProgressBar(false)
+            updateStatus("状态异常,请重新开始")
+            return
+        }
+
+        viewModel.humanFaceLogin(
+              base64Image,
+            fail = { errorMessage ->
+                runOnUiThread {
+                    handleServerResponse(
+                        success = false,
+                        errorMessage = errorMessage,
+                        base64Image = base64Image
+                    )
+                }
+            },
+            success = { token ->
+                runOnUiThread {
+                    if (TextUtils.isEmpty(token).not()) {
+                        this.token = token!!
+                        User.saveToken(token)
+                        handleServerResponse(
+                            success = true,
+                            errorMessage = null,
+                            base64Image = base64Image
+                        )
+                    } else {
+                        handleServerResponse(
+                            success = false,
+                            errorMessage = "登录失败",
+                            base64Image = base64Image
+                        )
+                    }
+                }
+            }
+        )
+    }
+
+    private fun handleServerResponse(success: Boolean, errorMessage: String?, base64Image: String) {
+        isProcessingPhoto = false
+        isRequestInProgress = false
+        showProgressBar(false)
+
+        if (success && TextUtils.isEmpty(token).not()) {
+            // 成功
+            updateStatus("人脸验证成功!")
+            Toast.makeText(this, "人脸验证成功", Toast.LENGTH_SHORT).show()
+
+            // 立即停止自动捕获
+            isAutoCaptureEnabled = false
+            updateUIState()
+
+            // 获取用户信息
+            getUserInfo()
+        } else {
+            // 失败
+            val attempts = captureAttempts.incrementAndGet()
+            val errorMsg = errorMessage ?: "验证失败"
+
+            if (attempts >= maxCaptureAttempts) {
+                // 达到最大尝试次数
+                updateStatus("验证失败,已达最大尝试次数")
+                Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
+
+                // 3秒后返回登录页面
+                mainHandler.postDelayed({
+                    JumpUtil.jumpActivity(RouterUrlCommon.login, this)
+                    finish()
+                }, 3000)
+            } else {
+                // 还有机会,准备重新尝试
+                currentAttempt++
+                updateUIState()
+                updateStatus("验证失败,准备第${currentAttempt}次尝试 ($errorMsg)")
+
+                // 添加冷却时间后重新开始
+                mainHandler.postDelayed({
+                    if (isAutoCaptureEnabled && !isProcessingPhoto && !isRequestInProgress) {
+                        stableFaceFrames = 0
+                        updateStatus("开始第${currentAttempt}次尝试...")
+                        // 延长冷却时间,避免连续失败
+                        lastDetectionTime = System.currentTimeMillis() + 2000
+                    }
+                }, 3000)
+            }
+        }
+    }
+
+    private fun handleCaptureError(errorMessage: String) {
+        isProcessingPhoto = false
+        isRequestInProgress = false
+        showProgressBar(false)
+
+        updateStatus("拍照失败: $errorMessage")
+        Toast.makeText(this, "拍照失败: $errorMessage", Toast.LENGTH_SHORT).show()
+
+        // 3秒后重试
+        mainHandler.postDelayed({
+            if (isAutoCaptureEnabled && !isProcessingPhoto && !isRequestInProgress) {
+                stableFaceFrames = 0
+                updateStatus("准备重新检测...")
+            }
+        }, 3000)
+    }
+
+    private fun updateStatus(message: String) {
+        runOnUiThread {
+            statusTextView.text = message
+        }
+    }
+
+    private fun showProgressBar(show: Boolean) {
+        runOnUiThread {
+            progressBar.visibility = if (show) View.VISIBLE else View.GONE
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        isAutoCaptureEnabled = false
+        isProcessingPhoto = false
+        isRequestInProgress = false
+        cameraExecutor.shutdown()
+        try {
+            if (!cameraExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
+                cameraExecutor.shutdownNow()
+            }
+        } catch (e: InterruptedException) {
+            cameraExecutor.shutdownNow()
+        }
+        faceDetector.close()
+        mainHandler.removeCallbacksAndMessages(null)
+    }
+
+
+    private fun getUserInfo() {
+        viewModel.getUserInfo(fail = {
+            it.toast()
+        }, success = {
+            runOnUiThread {
+                if (it != null) {
+                    if (TextUtils.isEmpty(it.mobile).not()) {
+                        User.saveMobile(it.mobile)
+                    }
+                    if (TextUtils.isEmpty(it.idCard).not()) {
+                        User.saveIdCard(it.idCard)
+                    }
+                    //保存用户头像
+                    if (TextUtils.isEmpty(it.avatar).not()) {
+                        User.saveAvatar(it.avatar)
+                    }
+                    //保存用户名
+                    if (TextUtils.isEmpty(it.name).not()) {
+                        User.saveName(it.name)
+                    }
+                    User.saveUserSex(it.getSex())
+                    User.saveUserAge(it.getAgeInfo())
+                    if (TextUtils.isEmpty(it.orgCode).not()) {
+                        User.saveOrgCode(it.orgCode)
+                    }
+                    if (TextUtils.isEmpty(it.id).not()) {
+                        User.saveUserId(it.id)
+                    }
+                    if (TextUtils.isEmpty(it.padNo).not()) {
+                        User.savePadNo(it.padNo)
+                    }
+                    //保存是否第一次登陆(0第一次登陆)
+                    if (TextUtils.isEmpty(it.firstLogin).not()) {
+                        User.saveFirstLogin(it.firstLogin)
+                    }
+                    startInitService()
+                    ARouter.getInstance().build(RouterUrlCommon.home)
+                        .withTransition(R.anim.leftin, R.anim.leftout).navigation(this)
+                    finish()
+                }
+            }
+        })
+    }
+
+    private fun startInitService() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            startForegroundService(Intent(this, InitService::class.java))
+        } else {
+            startService(Intent(this, InitService::class.java))
+        }
+    }
+}

+ 350 - 0
login/src/main/java/com/yingyang/login/ui/login/FaceCaptureActivity2.kt

@@ -0,0 +1,350 @@
+package com.yingyang.login.ui.login
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.provider.MediaStore
+import android.util.Log
+import android.util.Size
+import android.widget.Button
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.face.FaceDetection
+import com.google.mlkit.vision.face.FaceDetectorOptions
+import com.yingyang.login.R
+import com.yingyang.login.service.FaceOverlayView2
+import com.yingyangfly.baselib.router.RouterUrlCommon
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+
+class FaceCaptureActivity2 : AppCompatActivity() {
+
+    private lateinit var previewView: PreviewView
+    private lateinit var captureButton: Button
+    private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
+    private var imageCapture: ImageCapture? = null
+    private var camera: Camera? = null
+    private lateinit var cameraExecutor: ExecutorService
+
+    // 人脸检测相关
+    private lateinit var faceDetector: com.google.mlkit.vision.face.FaceDetector
+    private lateinit var faceOverlayView: FaceOverlayView2
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_face_capture2)
+
+        previewView = findViewById(R.id.preview_view)
+        captureButton = findViewById(R.id.capture_button)
+        faceOverlayView = findViewById(R.id.face_overlay)
+
+        // 初始化线程池
+        cameraExecutor = Executors.newSingleThreadExecutor()
+
+        // 初始化人脸检测器
+        initFaceDetector()
+
+        // 检查权限
+        if (allPermissionsGranted()) {
+            startCamera()
+        } else {
+            requestPermissions()
+        }
+
+        // 设置拍照按钮点击事件
+        captureButton.setOnClickListener {
+            takePhoto()
+        }
+
+        // 按钮初始状态
+        captureButton.isEnabled = true
+        captureButton.text = "点击拍照"
+    }
+
+    private fun initFaceDetector() {
+        val options = FaceDetectorOptions.Builder()
+            .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
+            .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
+            .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
+            .setMinFaceSize(0.1f)
+            .build()
+
+        faceDetector = FaceDetection.getClient(options)
+    }
+
+    private fun allPermissionsGranted(): Boolean {
+        val requiredPermissions = mutableListOf<String>().apply {
+            add(Manifest.permission.CAMERA)
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+                add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            }
+        }
+
+        return requiredPermissions.all {
+            ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
+        }
+    }
+
+    private fun requestPermissions() {
+        val permissions = mutableListOf<String>().apply {
+            add(Manifest.permission.CAMERA)
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+                add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            }
+        }
+
+        ActivityCompat.requestPermissions(
+            this,
+            permissions.toTypedArray(),
+            REQUEST_CODE_PERMISSIONS
+        )
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        if (requestCode == REQUEST_CODE_PERMISSIONS) {
+            if (allPermissionsGranted()) {
+                startCamera()
+            } else {
+                Toast.makeText(this, "需要相机权限才能使用", Toast.LENGTH_SHORT).show()
+                finish()
+            }
+        }
+    }
+
+    private fun startCamera() {
+        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+        cameraProviderFuture.addListener(Runnable {
+            val cameraProvider = cameraProviderFuture.get()
+
+            // 获取显示旋转方向
+            val rotation = previewView.display.rotation
+
+            // 预览配置
+            val preview = Preview.Builder()
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3)  // 预览可以设置宽高比
+                .setTargetRotation(rotation)
+                .build()
+                .also {
+                    it.setSurfaceProvider(previewView.surfaceProvider)
+                }
+
+            // 图像捕获配置
+            imageCapture = ImageCapture.Builder()
+                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3)  // 拍照可以设置宽高比
+                .setTargetRotation(rotation)
+                .setFlashMode(ImageCapture.FLASH_MODE_OFF)
+                .build()
+
+            // 图像分析配置 - 只使用分辨率
+            val imageAnalyzer = ImageAnalysis.Builder()
+                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+                .setTargetRotation(rotation)
+                .apply {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                        setTargetResolution(Size(720, 1280))  // 设置固定分辨率
+                    }
+                }
+                .build()
+                .also { analyzer ->
+                    analyzer.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy ->
+                        analyzeImage(imageProxy)
+                    })
+                }
+
+            // 选择前置摄像头
+            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+
+            try {
+                // 解绑所有用例
+                cameraProvider.unbindAll()
+
+                // 绑定用例到相机
+                camera = cameraProvider.bindToLifecycle(
+                    this, cameraSelector, preview, imageCapture, imageAnalyzer
+                )
+
+            } catch (e: Exception) {
+                Log.e(TAG, "绑定失败: ${e.message}")
+                Toast.makeText(this, "相机启动失败: ${e.message}", Toast.LENGTH_SHORT).show()
+            }
+
+        }, ContextCompat.getMainExecutor(this))
+    }
+
+    private fun analyzeImage(imageProxy: ImageProxy) {
+        val mediaImage = imageProxy.image
+        if (mediaImage != null) {
+            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
+
+            faceDetector.process(image)
+                .addOnSuccessListener { faces ->
+                    // 更新人脸框显示
+                    runOnUiThread {
+                        faceOverlayView.setFaces(faces, imageProxy.width, imageProxy.height)
+
+                        // 更新拍照按钮提示文本(不限制拍照)
+                        if (faces.isNotEmpty()) {
+                            captureButton.text = "检测到${faces.size}张人脸,点击拍照"
+                        } else {
+                            captureButton.text = "点击拍照"
+                        }
+                    }
+                }
+                .addOnFailureListener { e ->
+                    Log.e(TAG, "人脸检测失败: ${e.message}")
+                }
+                .addOnCompleteListener {
+                    imageProxy.close()
+                }
+        } else {
+            imageProxy.close()
+        }
+    }
+
+    private fun takePhoto() {
+        val imageCapture = imageCapture ?: return
+
+        // 创建时间戳文件名
+        val name = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
+            .format(System.currentTimeMillis())
+        val fileName = "FACE_$name.jpg"
+
+        // 创建内容值
+        val contentValues = ContentValues().apply {
+            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
+            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/FaceRecognition")
+            }
+        }
+
+        // 创建输出选项
+        val outputOptions = ImageCapture.OutputFileOptions
+            .Builder(
+                contentResolver,
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                contentValues
+            )
+            .build()
+
+        // 拍照前可以添加一些视觉反馈(可选)
+        captureButton.text = "拍照中..."
+        captureButton.isEnabled = false
+
+        // 拍照
+        imageCapture.takePicture(
+            outputOptions,
+            ContextCompat.getMainExecutor(this),
+            object : ImageCapture.OnImageSavedCallback {
+                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+                    val savedUri = outputFileResults.savedUri
+                    val msg = "照片已保存: ${savedUri ?: fileName}"
+                    Toast.makeText(this@FaceCaptureActivity2, msg, Toast.LENGTH_LONG).show()
+                    Log.d(TAG, msg)
+
+                    // 恢复按钮状态
+                    captureButton.text = "点击拍照"
+                    captureButton.isEnabled = true
+
+                    // 保存后处理照片(可选)
+                    savedUri?.let {
+                        processCapturedImage(it)
+                    }
+                }
+
+                override fun onError(exception: ImageCaptureException) {
+                    Log.e(TAG, "拍照失败: ${exception.message}", exception)
+                    Toast.makeText(this@FaceCaptureActivity2, "拍照失败: ${exception.message}", Toast.LENGTH_SHORT).show()
+
+                    // 恢复按钮状态
+                    captureButton.text = "点击拍照"
+                    captureButton.isEnabled = true
+                }
+            }
+        )
+    }
+
+    private fun processCapturedImage(imageUri: android.net.Uri) {
+        try {
+            // 从URI加载图片
+            val inputStream = contentResolver.openInputStream(imageUri)
+            val bitmap = BitmapFactory.decodeStream(inputStream)
+            inputStream?.close()
+
+            // 这里可以添加人脸识别或处理逻辑
+            // 例如:检测照片中的人脸质量、对齐等
+
+            // 保存处理后的图片到私有目录(可选)
+            bitmap?.let {
+                saveToPrivateDirectory(it)
+            }
+
+        } catch (e: Exception) {
+            Log.e(TAG, "处理图片失败: ${e.message}")
+        }
+    }
+
+    private fun saveToPrivateDirectory(bitmap: Bitmap) {
+        // 保存到应用私有目录
+        val outputDir = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "face_captures")
+        if (!outputDir.exists()) {
+            outputDir.mkdirs()
+        }
+
+        val outputFile = File(outputDir, "face_${System.currentTimeMillis()}.jpg")
+
+        try {
+            val outputStream = outputFile.outputStream()
+            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
+            outputStream.close()
+
+            Log.d(TAG, "图片已保存到私有目录: ${outputFile.absolutePath}")
+        } catch (e: Exception) {
+            Log.e(TAG, "保存到私有目录失败: ${e.message}")
+        } finally {
+            bitmap.recycle()
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        cameraExecutor.shutdown()
+        faceDetector.close()
+    }
+
+    companion object {
+        private const val TAG = "FaceCapture"
+        private const val REQUEST_CODE_PERMISSIONS = 10
+    }
+}

+ 10 - 0
login/src/main/java/com/yingyang/login/ui/login/LoginActivity.kt

@@ -81,6 +81,7 @@ class LoginActivity : BaseMVVMActivity<ActivityLoginBinding, LoginViewModel>(),
             tvAlterCode.setOnTouchListener(this@LoginActivity)
             scaleEvaluation.setOnTouchListener(this@LoginActivity)
             eaBtn.setOnTouchListener(this@LoginActivity)
+            humanFace.setOnTouchListener(this@LoginActivity)
         }
     }
 
@@ -260,6 +261,7 @@ class LoginActivity : BaseMVVMActivity<ActivityLoginBinding, LoginViewModel>(),
         binding.alterPassLayout.show(false)
         binding.codeButton.setTextColorResource(R.color.color_2EF3FA)
         binding.passwordButton.setTextColorResource(R.color.color_76b4fd)
+        binding.humanFace.setTextColorResource(R.color.color_76b4fd)
         binding.forgetPass.show(false)
         binding.layoutLogin.show(true)
         loginType = 1
@@ -388,6 +390,7 @@ class LoginActivity : BaseMVVMActivity<ActivityLoginBinding, LoginViewModel>(),
                     loginType = 0
                     binding.passwordButton.setTextColorResource(R.color.color_2EF3FA)
                     binding.codeButton.setTextColorResource(R.color.color_76b4fd)
+                    binding.humanFace.setTextColorResource(R.color.color_76b4fd)
                     binding.password.hint = "请输入密码"
                     binding.forgetPass.show(true)
                     binding.passwordLayout2.show(true)
@@ -395,11 +398,18 @@ class LoginActivity : BaseMVVMActivity<ActivityLoginBinding, LoginViewModel>(),
                 } else if (v.id == R.id.codeButton) {  // 验证码登录选择
                     binding.passwordButton.setTextColorResource(R.color.color_76b4fd)
                     binding.codeButton.setTextColorResource(R.color.color_2EF3FA)
+                    binding.humanFace.setTextColorResource(R.color.color_76b4fd)
                     binding.password.hint = "请输入验证码"
                     binding.forgetPass.show(false)
                     binding.passwordLayout2.show(false)
                     binding.passwordLayout.show(true)
                     loginType = 1
+                }else if (v.id == R.id.humanFace) {
+                    binding.passwordButton.setTextColorResource(R.color.color_76b4fd)
+                    binding.codeButton.setTextColorResource(R.color.color_76b4fd)
+                    binding.humanFace.setTextColorResource(R.color.color_2EF3FA)
+                    ARouter.getInstance().build(RouterUrlCommon.humanFaceLogin)
+                        .withTransition(R.anim.leftin, R.anim.leftout).navigation(mContext)
                 }else if (v.id ==R.id.forgetPass) {  // 忘记密码选择
                     binding.alterPassLayout.show(true)
                     binding.layoutLogin.show(false)

+ 11 - 0
login/src/main/java/com/yingyang/login/ui/login/LoginViewModel.kt

@@ -121,4 +121,15 @@ class LoginViewModel : BaseViewModel() {
     }.runUI(
         success, fail
     )
+
+
+    fun humanFaceLogin(
+        imageBase: String,
+        fail: ((msg: String) -> Unit)? = null,
+        success: ((success: String?) -> Unit)? = null
+    ) = launchFlow(true) {
+        LOGIN_API.humanFaceLogin(imageBase)
+    }.runUI(
+        success, fail
+    )
 }

+ 16 - 0
login/src/main/manifest/AndroidManifest.xml

@@ -4,7 +4,23 @@
 
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- 录音权限 -->
 
+    <!-- 相机权限 -->
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <!-- 如果需要录制视频 -->
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <!-- 声明相机特性 -->
+    <uses-feature android:name="android.hardware.camera" android:required="true" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+
     <application>
+
+        <activity android:name="com.yingyang.login.ui.login.FaceCaptureActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="landscape"
+            android:windowSoftInputMode="adjustPan" />
+
         <activity
             android:name="com.yingyang.login.ui.login.LoginActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"

+ 15 - 0
login/src/main/res/drawable/button_circle.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <solid android:color="#4CAF50" />
+
+    <size
+        android:width="80dp"
+        android:height="80dp" />
+
+    <stroke
+        android:width="3dp"
+        android:color="#FFFFFF" />
+
+</shape>

+ 11 - 0
login/src/main/res/drawable/corner_indicator.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:pathData="M0,0 L12,0 L0,12 Z"
+        android:fillColor="#FFFFFF"
+        android:strokeWidth="1" />
+</vector>

+ 80 - 0
login/src/main/res/drawable/face_guide_border.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- 外边框 -->
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/transparent" />
+            <stroke
+                android:width="2dp"
+                android:color="#FFFFFF" />
+            <corners android:radius="12dp" />
+        </shape>
+    </item>
+
+    <!-- 内边框 -->
+    <item android:left="2dp" android:top="2dp" android:right="2dp" android:bottom="2dp">
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/transparent" />
+            <stroke
+                android:width="1dp"
+                android:color="#66FFFFFF" />
+            <corners android:radius="10dp" />
+        </shape>
+    </item>
+
+    <!-- 水平中线(眼睛对齐线) -->
+    <item android:top="@dimen/divider_250px">
+        <shape android:shape="line">
+            <stroke
+                android:width="1dp"
+                android:color="#33FFFFFF"
+                android:dashWidth="5dp"
+                android:dashGap="5dp" />
+        </shape>
+    </item>
+
+    <!-- 垂直中线 -->
+    <item android:left="@dimen/divider_250px">
+        <shape android:shape="line">
+            <stroke
+                android:width="1dp"
+                android:color="#33FFFFFF"
+                android:dashWidth="5dp"
+                android:dashGap="5dp" />
+        </shape>
+    </item>
+
+    <!-- 四角指示器 -->
+    <!-- 左上角 -->
+    <item>
+        <bitmap
+            android:src="@drawable/corner_indicator"
+            android:gravity="top|left" />
+    </item>
+
+    <!-- 右上角 -->
+    <item>
+        <bitmap
+            android:src="@drawable/corner_indicator"
+            android:gravity="top|right"
+            android:rotation="90" />
+    </item>
+
+    <!-- 左下角 -->
+    <item>
+        <bitmap
+            android:src="@drawable/corner_indicator"
+            android:gravity="bottom|left"
+            android:rotation="270" />
+    </item>
+
+    <!-- 右下角 -->
+    <item>
+        <bitmap
+            android:src="@drawable/corner_indicator"
+            android:gravity="bottom|right"
+            android:rotation="180" />
+    </item>
+
+</layer-list>

+ 74 - 0
login/src/main/res/layout/activity_face_capture2.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout 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"
+    tools:ignore="ResourceName">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#000">
+
+        <!-- 相机预览视图 -->
+        <androidx.camera.view.PreviewView
+            android:id="@+id/preview_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <!-- 人脸检测框叠加层 -->
+        <com.yingyang.login.service.FaceOverlayView2
+            android:id="@+id/face_overlay"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="visible" />
+
+        <!-- 拍照按钮 -->
+        <Button
+            android:id="@+id/capture_button"
+            android:layout_width="80dp"
+            android:layout_height="80dp"
+            android:layout_marginBottom="40dp"
+            android:background="@drawable/button_circle"
+            android:text="点击拍照"
+            android:textColor="#FFF"
+            android:textSize="14sp"
+            android:enabled="true"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+        <!-- 提示信息 -->
+        <TextView
+            android:id="@+id/instruction_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="30dp"
+            android:text="请将面部对准框内,点击下方按钮拍照"
+            android:textColor="#FFF"
+            android:textSize="16sp"
+            android:background="#66000000"
+            android:padding="12dp"
+            android:gravity="center"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <!-- 人脸对齐参考框 -->
+        <View
+            android:layout_width="250dp"
+            android:layout_height="320dp"
+            android:layout_marginBottom="100dp"
+            android:background="@android:color/transparent"
+            android:elevation="4dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            style="@style/face_guide_border" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</layout>

+ 14 - 3
login/src/main/res/layout/activity_login.xml

@@ -25,10 +25,10 @@
 
         <androidx.constraintlayout.widget.ConstraintLayout
             android:id="@+id/layoutLogin"
-            android:layout_width="@dimen/divider_400px"
+            android:layout_width="@dimen/divider_440px"
             android:layout_height="@dimen/divider_640px"
             android:background="@mipmap/xitongdenglu_pop_bg"
-            android:layout_marginEnd="@dimen/divider_120px"
+            android:layout_marginEnd="@dimen/divider_80px"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="parent">
@@ -69,11 +69,22 @@
                 android:text="密码登录"
                 android:textStyle="bold"
                 android:textSize="@dimen/divider_28px"
-
                 android:textColor="@color/color_76b4fd"
                 app:layout_constraintStart_toEndOf="@+id/codeButton"
                 app:layout_constraintTop_toBottomOf="@+id/tvTitle"/>
 
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/humanFace"
+                android:layout_width="@dimen/divider_150px"
+                android:layout_height="@dimen/divider_40px"
+                android:layout_marginTop="@dimen/divider_30px"
+                android:text="人脸登录"
+                android:textStyle="bold"
+                android:textSize="@dimen/divider_28px"
+                android:textColor="@color/color_76b4fd"
+                app:layout_constraintStart_toEndOf="@+id/passwordButton"
+                app:layout_constraintTop_toBottomOf="@+id/tvTitle"/>
+
             <androidx.appcompat.widget.AppCompatEditText
                 android:id="@+id/username"
                 android:layout_width="match_parent"

+ 99 - 0
login/src/main/res/layout/login_activity_face_capture.xml

@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/black">
+
+    <!-- 相机预览 -->
+    <androidx.camera.view.PreviewView
+        android:id="@+id/preview_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <!-- 人脸框覆盖层 -->
+    <com.yingyang.login.service.FaceOverlayView
+        android:id="@+id/face_overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/transparent" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/icon_back"
+        android:layout_width="@dimen/divider_110px"
+        android:layout_height="@dimen/divider_48px"
+        android:layout_marginTop="@dimen/divider_40px"
+        android:layout_marginStart="@dimen/divider_40px"
+        android:background="@mipmap/icon_back_1"/>
+
+    <!-- 顶部状态栏 -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="50dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="16dp"
+        android:background="#99000000"
+        android:orientation="vertical"
+        android:padding="12dp">
+
+        <TextView
+            android:id="@+id/status_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="准备就绪"
+            android:textColor="@android:color/white"
+            android:textSize="16sp"
+            android:textStyle="bold" />
+
+        <TextView
+            android:id="@+id/attempts_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="4dp"
+            android:text="尝试次数: 1/3"
+            android:textColor="@android:color/white"
+            android:textSize="14sp" />
+
+    </LinearLayout>
+
+    <!-- 进度条 -->
+    <ProgressBar
+        android:id="@+id/progress_bar"
+        style="?android:attr/progressBarStyleLarge"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:visibility="gone" />
+
+    <!-- 底部控制栏 -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="30dp"
+        android:gravity="center"
+        android:orientation="vertical">
+
+        <Button
+            android:id="@+id/capture_button"
+            android:layout_width="120dp"
+            android:layout_height="50dp"
+            android:text="开始"
+            android:textSize="16sp"
+            android:backgroundTint="#4CAF50"
+            android:textColor="@android:color/white" />
+
+    </LinearLayout>
+
+    <!-- 人脸检测指引框 -->
+    <View
+        android:layout_width="300dp"
+        android:layout_height="300dp"
+        android:layout_centerInParent="true"
+        android:background="@android:color/transparent"
+        android:backgroundTint="#4CAF50"
+        android:backgroundTintMode="src_in" />
+
+</RelativeLayout>

BIN
login/src/main/res/mipmap-xxhdpi/icon_back_1.png


+ 4 - 0
login/src/main/res/values/styles.xml

@@ -20,4 +20,8 @@
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
     </style>
+
+    <style name="face_guide_border" tools:ignore="ResourceName">
+        <item name="android:background">@drawable/face_guide_border</item>
+    </style>
 </resources>