Face ID real detect face and pay, works

This commit is contained in:
Ahmed Al-Omairi 2025-09-01 00:05:49 +03:00
parent c6cb612c99
commit 8b892e68df
6 changed files with 236 additions and 30 deletions

View File

@ -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"
}

View File

@ -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;

View File

@ -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<Face> 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;
}
}

View File

@ -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<ActivityPaymentBinding, PaymentViewModel> implements PaymentServiceCallback {
@ -175,7 +166,7 @@ public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, Paymen
waitingViewModel.faceIDSuccess.observe(this, success -> {
if (success != null && success) {
simulatePaymentAfterDelay();
// simulatePaymentAfterDelay();
}
});
@ -221,15 +212,20 @@ public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, Paymen
faceIDHelper = new FaceIDHelper(this, textureView, new FaceIDHelper.FaceIDCallback() {
@Override
public void onFaceDetected() {
Log.d("FaceDetection", "✅ Face detected successfully!");
runOnUiThread(() -> {
// 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<ActivityPaymentBinding, Paymen
binding.btnSendReceipt.setVisibility(View.VISIBLE);
}
},
200 // 10 second delay
50 // 10 second delay
);
}
});

View File

@ -51,7 +51,7 @@ public class WaitingForCardViewModel extends BaseViewModel {
}
public void generateQRCode(String paymentData) {
try {
Bitmap qrCode = QRCodeGenerator.generateQRCode(paymentData, 500, 500);
Bitmap qrCode = QRCodeGenerator.generateQRCode(paymentData, 300, 300);
if (qrCode != null) {
qrCodeBitmap.set(qrCode);
Log.d("QRCode", "QR code generated successfully");

View File

@ -106,7 +106,7 @@
<pl.droidsonroids.gif.GifImageView
android:layout_width="98dp"
android:layout_height="124dp"
android:src="@drawable/faceid_180px" />
android:src="@drawable/faceid" />
</LinearLayout>
</androidx.cardview.widget.CardView>
@ -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">
<!-- Camera Preview will be added here programmatically -->
<FrameLayout
android:id="@+id/camera_preview_holder"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_gravity="center">
<!-- Camera Preview will be added here -->
</FrameLayout>
<TextView
android:id="@+id/amountOnCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:padding="10dp"
android:text="@{vm.amount + ` `+vm.currencySymbol}"
android:textSize="36sp"
android:textStyle="bold"
android:background="@drawable/rounded_bg_black_50"
android:textColor="@color/white"
tools:text="100.00" />
<!-- Close button for camera -->
<ImageButton
android:id="@+id/btn_close_camera"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="24dp"
android:layout_margin="37dp"
android:src="@drawable/ic_close_white"
android:background="@drawable/rounded_bg_black_50"
android:scaleType="center"
@ -223,7 +252,7 @@
android:layout_gravity="center"
android:padding="16dp"
android:background="@drawable/rounded_bg_black_50"
android:visibility="visible" />
android:visibility="gone" />
</FrameLayout>
</FrameLayout>