diff --git a/pos_android_app/build.gradle b/pos_android_app/build.gradle index b2a1cd8..b22b559 100644 --- a/pos_android_app/build.gradle +++ b/pos_android_app/build.gradle @@ -139,4 +139,12 @@ dependencies { implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' implementation "org.java-websocket:Java-WebSocket:1.5.2" + + // ML Kit Face Detection + implementation 'com.google.mlkit:face-detection:16.1.6' + + // CameraX (optional but recommended for better camera handling) +// implementation "androidx.camera:camera-camera2:1.3.2" +// implementation "androidx.camera:camera-lifecycle:1.3.2" +// implementation "androidx.camera:camera-view:1.3.2" } \ No newline at end of file diff --git a/pos_android_app/src/main/java/com/dspread/pos/utils/FaceIDHelper.java b/pos_android_app/src/main/java/com/dspread/pos/faceID/FaceIDHelper.java similarity index 79% rename from pos_android_app/src/main/java/com/dspread/pos/utils/FaceIDHelper.java rename to pos_android_app/src/main/java/com/dspread/pos/faceID/FaceIDHelper.java index 0f1a080..3c1afb0 100644 --- a/pos_android_app/src/main/java/com/dspread/pos/utils/FaceIDHelper.java +++ b/pos_android_app/src/main/java/com/dspread/pos/faceID/FaceIDHelper.java @@ -1,8 +1,9 @@ -package com.dspread.pos.utils; +package com.dspread.pos.faceID; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Bitmap; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; @@ -12,10 +13,10 @@ import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.os.Handler; import android.os.HandlerThread; -import android.util.Size; +import android.util.Log; import android.view.Surface; import android.view.TextureView; -import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import java.util.Arrays; @@ -23,6 +24,7 @@ import java.util.Arrays; public class FaceIDHelper implements TextureView.SurfaceTextureListener { private static final int REQUEST_CAMERA_PERMISSION = 200; + private static final int DETECTION_INTERVAL_MS = 1000; // Check every second private Context context; private TextureView textureView; @@ -35,8 +37,10 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { private Handler backgroundHandler; private HandlerThread backgroundThread; - private String frontCameraId; + private RealFaceDetector realFaceDetector; + private Handler detectionHandler; + private Runnable detectionRunnable; public interface FaceIDCallback { void onFaceDetected(); @@ -48,6 +52,26 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { this.textureView = textureView; this.callback = callback; this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + + // Initialize real face detector + realFaceDetector = new RealFaceDetector(context, new RealFaceDetector.FaceDetectionCallback() { + @Override + public void onFaceDetected() { + if (callback != null) { + callback.onFaceDetected(); + } + } + + @Override + public void onFaceDetectionError(String error) { + Log.d("FaceIDHelper", "Face detection error: " + error); + } + + @Override + public void onNoFaceDetected() { + // Continue detection + } + }); } public void startCamera() { @@ -91,6 +115,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { openCamera(); + startFaceDetection(); } @Override @@ -98,6 +123,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + stopFaceDetection(); return false; } @@ -124,8 +150,6 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { public void onOpened(@NonNull CameraDevice camera) { cameraDevice = camera; createCameraPreview(); - // Simulate face detection after camera opens - simulateFaceDetection(); } @Override @@ -181,13 +205,28 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { } } - private void simulateFaceDetection() { - // Simulate face detection after 3 seconds - new Handler().postDelayed(() -> { - if (callback != null) { - callback.onFaceDetected(); + private void startFaceDetection() { + detectionHandler = new Handler(); + detectionRunnable = new Runnable() { + @Override + public void run() { + if (textureView.isAvailable()) { + Bitmap frameBitmap = textureView.getBitmap(); + if (frameBitmap != null) { + realFaceDetector.detectFaces(frameBitmap); + } + } + detectionHandler.postDelayed(this, DETECTION_INTERVAL_MS); } - }, 18000); + }; + detectionHandler.postDelayed(detectionRunnable, DETECTION_INTERVAL_MS); + } + + private void stopFaceDetection() { + if (detectionHandler != null && detectionRunnable != null) { + detectionHandler.removeCallbacks(detectionRunnable); + } + realFaceDetector.stop(); } public void startBackgroundThread() { @@ -197,6 +236,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { } public void stopBackgroundThread() { + stopFaceDetection(); if (backgroundThread != null) { backgroundThread.quitSafely(); try { @@ -210,6 +250,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener { } public void closeCamera() { + stopFaceDetection(); if (cameraCaptureSession != null) { cameraCaptureSession.close(); cameraCaptureSession = null; diff --git a/pos_android_app/src/main/java/com/dspread/pos/faceID/RealFaceDetector.java b/pos_android_app/src/main/java/com/dspread/pos/faceID/RealFaceDetector.java new file mode 100644 index 0000000..27043be --- /dev/null +++ b/pos_android_app/src/main/java/com/dspread/pos/faceID/RealFaceDetector.java @@ -0,0 +1,132 @@ +package com.dspread.pos.faceID; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; + +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.FaceDetector; +import com.google.mlkit.vision.face.FaceDetectorOptions; +import java.util.List; + +public class RealFaceDetector { + + private FaceDetector faceDetector; + private FaceDetectionCallback callback; + private boolean isDetecting = false; + + public interface FaceDetectionCallback { + void onFaceDetected(); + void onFaceDetectionError(String error); + void onNoFaceDetected(); + } + + public RealFaceDetector(Context context, FaceDetectionCallback callback) { + this.callback = callback; + initializeFaceDetector(); + } + + private void initializeFaceDetector() { + // High-accuracy landmark detection and face classification + FaceDetectorOptions options = new FaceDetectorOptions.Builder() + .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE) + .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL) + .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) + .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) + .setMinFaceSize(0.15f) // Minimum face size (15% of image width) + .enableTracking() // Enable face tracking for better performance + .build(); +//// Use a different face detection approach (offline-only) +//// the bundled version: +// .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) +// .setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE) +// .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE) +// .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) +// .build(); + + faceDetector = FaceDetection.getClient(options); + } + + public void detectFaces(Bitmap bitmap) { + if (isDetecting) return; + + isDetecting = true; + InputImage image = InputImage.fromBitmap(bitmap, 0); // 0 rotation + + faceDetector.process(image) + .addOnSuccessListener(faces -> { + isDetecting = false; + handleDetectionResult(faces); + }) + .addOnFailureListener(e -> { + isDetecting = false; + if (callback != null) { + callback.onFaceDetectionError("Face detection failed: " + e.getMessage()); + } + }); + } + + private void handleDetectionResult(List faces) { + if (faces == null || faces.isEmpty()) { + if (callback != null) { + callback.onNoFaceDetected(); + } + return; + } + + // Check if we have at least one good quality face + for (Face face : faces) { + if (isGoodQualityFace(face)) { + if (callback != null) { + callback.onFaceDetected(); + } + return; + } + } + + // If we reach here, faces were detected but not good quality + if (callback != null) { + callback.onNoFaceDetected(); + } + } + + private boolean isGoodQualityFace(Face face) { + // Check if face has good confidence (not occluded, good lighting) + // You can adjust these thresholds based on your requirements + + // Check if eyes are open (if classification is available) + if (face.getLeftEyeOpenProbability() != null && + face.getLeftEyeOpenProbability() < 0.3f) { + return false; // Eye probably closed + } + + if (face.getRightEyeOpenProbability() != null && + face.getRightEyeOpenProbability() < 0.3f) { + return false; // Eye probably closed + } + + // Check if smiling (optional, depends on your use case) + if (face.getSmilingProbability() != null && + face.getSmilingProbability() < 0.1f) { + // Not smiling, but this might not be necessary for payment + } + + // Check face bounding box size (should be reasonably large) + Rect bounds = face.getBoundingBox(); + float sizeRatio = (float) bounds.width() * bounds.height() / (1000 * 1000); // Example ratio + if (sizeRatio < 0.1f) { + return false; // Face too small + } + + return true; // Good quality face + } + + public void stop() { + if (faceDetector != null) { + faceDetector.close(); + } + isDetecting = false; + } +} \ No newline at end of file diff --git a/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java b/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java index 62db899..f024081 100644 --- a/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java +++ b/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java @@ -4,7 +4,6 @@ import android.app.Dialog; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; -import android.os.Handler; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.util.Base64; @@ -13,17 +12,12 @@ import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageButton; import android.widget.ListView; -import androidx.lifecycle.Observer; - import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; -import com.dspread.pos.TerminalApplication; -import com.dspread.pos.common.enums.TransCardMode; import com.dspread.pos.common.manager.QPOSCallbackManager; import com.dspread.pos.posAPI.POS; import com.dspread.pos.posAPI.PaymentServiceCallback; @@ -33,7 +27,6 @@ import com.dspread.pos.ui.payment.pinkeyboard.MyKeyboardView; import com.dspread.pos.ui.payment.pinkeyboard.PinPadDialog; import com.dspread.pos.ui.payment.pinkeyboard.PinPadView; import com.dspread.pos.utils.BitmapReadyListener; -import com.dspread.pos.utils.DevUtils; import com.dspread.pos.utils.DeviceUtils; import com.dspread.pos.utils.HandleTxnsResultUtils; import com.dspread.pos.utils.LogFileConfig; @@ -58,12 +51,10 @@ import me.goldze.mvvmhabit.utils.SPUtils; import me.goldze.mvvmhabit.utils.ToastUtils; // Add these imports at the top -import androidx.databinding.DataBindingUtil; import android.widget.FrameLayout; import com.dspread.pos_android_app.databinding.WaitingForCardBinding; // Generated binding class -import android.widget.FrameLayout; -import com.dspread.pos.utils.FaceIDHelper; +import com.dspread.pos.faceID.FaceIDHelper; public class PaymentActivity extends BaseActivity implements PaymentServiceCallback { @@ -175,7 +166,7 @@ public class PaymentActivity extends BaseActivity { if (success != null && success) { - simulatePaymentAfterDelay(); +// simulatePaymentAfterDelay(); } }); @@ -221,15 +212,20 @@ public class PaymentActivity extends BaseActivity { // Face detected - proceed with payment closeFaceIDCamera(); - waitingViewModel.simulateFaceIDSuccess(); +// waitingViewModel.simulateFaceIDSuccess(); + + FaceIDMood = true; + POS.getInstance().cancelTrade(); }); } @Override public void onCameraError(String error) { + Log.e("FaceDetection", "❌ Face detection error: " + error); runOnUiThread(() -> { ToastUtils.showShort(error); closeFaceIDCamera(); @@ -287,7 +283,7 @@ public class PaymentActivity extends BaseActivity + android:src="@drawable/faceid" /> @@ -194,17 +194,46 @@ android:id="@+id/camera_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" + android:gravity="center" + android:visibility="visible" android:background="@android:color/black"> + + + + + + + + + + android:visibility="gone" />