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.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
implementation "org.java-websocket:Java-WebSocket:1.5.2" 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.Manifest;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCaptureSession;
@ -12,10 +13,10 @@ import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureRequest;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.util.Size; import android.util.Log;
import android.view.Surface; import android.view.Surface;
import android.view.TextureView; import android.view.TextureView;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import java.util.Arrays; import java.util.Arrays;
@ -23,6 +24,7 @@ import java.util.Arrays;
public class FaceIDHelper implements TextureView.SurfaceTextureListener { public class FaceIDHelper implements TextureView.SurfaceTextureListener {
private static final int REQUEST_CAMERA_PERMISSION = 200; private static final int REQUEST_CAMERA_PERMISSION = 200;
private static final int DETECTION_INTERVAL_MS = 1000; // Check every second
private Context context; private Context context;
private TextureView textureView; private TextureView textureView;
@ -35,8 +37,10 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
private Handler backgroundHandler; private Handler backgroundHandler;
private HandlerThread backgroundThread; private HandlerThread backgroundThread;
private String frontCameraId; private String frontCameraId;
private RealFaceDetector realFaceDetector;
private Handler detectionHandler;
private Runnable detectionRunnable;
public interface FaceIDCallback { public interface FaceIDCallback {
void onFaceDetected(); void onFaceDetected();
@ -48,6 +52,26 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
this.textureView = textureView; this.textureView = textureView;
this.callback = callback; this.callback = callback;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); 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() { public void startCamera() {
@ -91,6 +115,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
@Override @Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
openCamera(); openCamera();
startFaceDetection();
} }
@Override @Override
@ -98,6 +123,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
@Override @Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
stopFaceDetection();
return false; return false;
} }
@ -124,8 +150,6 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
public void onOpened(@NonNull CameraDevice camera) { public void onOpened(@NonNull CameraDevice camera) {
cameraDevice = camera; cameraDevice = camera;
createCameraPreview(); createCameraPreview();
// Simulate face detection after camera opens
simulateFaceDetection();
} }
@Override @Override
@ -181,13 +205,28 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
} }
} }
private void simulateFaceDetection() { private void startFaceDetection() {
// Simulate face detection after 3 seconds detectionHandler = new Handler();
new Handler().postDelayed(() -> { detectionRunnable = new Runnable() {
if (callback != null) { @Override
callback.onFaceDetected(); 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() { public void startBackgroundThread() {
@ -197,6 +236,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
} }
public void stopBackgroundThread() { public void stopBackgroundThread() {
stopFaceDetection();
if (backgroundThread != null) { if (backgroundThread != null) {
backgroundThread.quitSafely(); backgroundThread.quitSafely();
try { try {
@ -210,6 +250,7 @@ public class FaceIDHelper implements TextureView.SurfaceTextureListener {
} }
public void closeCamera() { public void closeCamera() {
stopFaceDetection();
if (cameraCaptureSession != null) { if (cameraCaptureSession != null) {
cameraCaptureSession.close(); cameraCaptureSession.close();
cameraCaptureSession = null; 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.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.text.Spanned; import android.text.Spanned;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.Base64; import android.util.Base64;
@ -13,17 +12,12 @@ import android.view.TextureView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ListView; import android.widget.ListView;
import androidx.lifecycle.Observer;
import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject; 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.common.manager.QPOSCallbackManager;
import com.dspread.pos.posAPI.POS; import com.dspread.pos.posAPI.POS;
import com.dspread.pos.posAPI.PaymentServiceCallback; 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.PinPadDialog;
import com.dspread.pos.ui.payment.pinkeyboard.PinPadView; import com.dspread.pos.ui.payment.pinkeyboard.PinPadView;
import com.dspread.pos.utils.BitmapReadyListener; import com.dspread.pos.utils.BitmapReadyListener;
import com.dspread.pos.utils.DevUtils;
import com.dspread.pos.utils.DeviceUtils; import com.dspread.pos.utils.DeviceUtils;
import com.dspread.pos.utils.HandleTxnsResultUtils; import com.dspread.pos.utils.HandleTxnsResultUtils;
import com.dspread.pos.utils.LogFileConfig; import com.dspread.pos.utils.LogFileConfig;
@ -58,12 +51,10 @@ import me.goldze.mvvmhabit.utils.SPUtils;
import me.goldze.mvvmhabit.utils.ToastUtils; import me.goldze.mvvmhabit.utils.ToastUtils;
// Add these imports at the top // Add these imports at the top
import androidx.databinding.DataBindingUtil;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.dspread.pos_android_app.databinding.WaitingForCardBinding; // Generated binding class import com.dspread.pos_android_app.databinding.WaitingForCardBinding; // Generated binding class
import android.widget.FrameLayout; import com.dspread.pos.faceID.FaceIDHelper;
import com.dspread.pos.utils.FaceIDHelper;
public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, PaymentViewModel> implements PaymentServiceCallback { 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 -> { waitingViewModel.faceIDSuccess.observe(this, success -> {
if (success != null && 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() { faceIDHelper = new FaceIDHelper(this, textureView, new FaceIDHelper.FaceIDCallback() {
@Override @Override
public void onFaceDetected() { public void onFaceDetected() {
Log.d("FaceDetection", "✅ Face detected successfully!");
runOnUiThread(() -> { runOnUiThread(() -> {
// Face detected - proceed with payment // Face detected - proceed with payment
closeFaceIDCamera(); closeFaceIDCamera();
waitingViewModel.simulateFaceIDSuccess(); // waitingViewModel.simulateFaceIDSuccess();
FaceIDMood = true;
POS.getInstance().cancelTrade();
}); });
} }
@Override @Override
public void onCameraError(String error) { public void onCameraError(String error) {
Log.e("FaceDetection", "❌ Face detection error: " + error);
runOnUiThread(() -> { runOnUiThread(() -> {
ToastUtils.showShort(error); ToastUtils.showShort(error);
closeFaceIDCamera(); closeFaceIDCamera();
@ -287,7 +283,7 @@ public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, Paymen
binding.btnSendReceipt.setVisibility(View.VISIBLE); 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) { public void generateQRCode(String paymentData) {
try { try {
Bitmap qrCode = QRCodeGenerator.generateQRCode(paymentData, 500, 500); Bitmap qrCode = QRCodeGenerator.generateQRCode(paymentData, 300, 300);
if (qrCode != null) { if (qrCode != null) {
qrCodeBitmap.set(qrCode); qrCodeBitmap.set(qrCode);
Log.d("QRCode", "QR code generated successfully"); Log.d("QRCode", "QR code generated successfully");

View File

@ -106,7 +106,7 @@
<pl.droidsonroids.gif.GifImageView <pl.droidsonroids.gif.GifImageView
android:layout_width="98dp" android:layout_width="98dp"
android:layout_height="124dp" android:layout_height="124dp"
android:src="@drawable/faceid_180px" /> android:src="@drawable/faceid" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
@ -194,17 +194,46 @@
android:id="@+id/camera_container" android:id="@+id/camera_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" android:gravity="center"
android:visibility="visible"
android:background="@android:color/black"> android:background="@android:color/black">
<!-- Camera Preview will be added here programmatically --> <!-- 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 --> <!-- Close button for camera -->
<ImageButton <ImageButton
android:id="@+id/btn_close_camera" android:id="@+id/btn_close_camera"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_margin="24dp" android:layout_margin="37dp"
android:src="@drawable/ic_close_white" android:src="@drawable/ic_close_white"
android:background="@drawable/rounded_bg_black_50" android:background="@drawable/rounded_bg_black_50"
android:scaleType="center" android:scaleType="center"
@ -223,7 +252,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:padding="16dp" android:padding="16dp"
android:background="@drawable/rounded_bg_black_50" android:background="@drawable/rounded_bg_black_50"
android:visibility="visible" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
</FrameLayout> </FrameLayout>