Face ID first attemp, on waiting_for-card layout

This commit is contained in:
Ahmed Al-Omairi 2025-08-31 18:17:23 +03:00
parent 50796a6951
commit c6cb612c99
7 changed files with 557 additions and 173 deletions

View File

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.front" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

View File

@ -9,11 +9,13 @@ import android.text.Spanned;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
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.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.ListView; import android.widget.ListView;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
@ -60,6 +62,9 @@ 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.utils.FaceIDHelper;
public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, PaymentViewModel> implements PaymentServiceCallback { public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, PaymentViewModel> implements PaymentServiceCallback {
@ -91,6 +96,9 @@ public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, Paymen
private boolean QRMood = false; private boolean QRMood = false;
private boolean FaceIDMood = false; private boolean FaceIDMood = false;
private FaceIDHelper faceIDHelper;
private boolean isFaceIDActive = false;
@Override @Override
public void initData() { public void initData() {
// Debug current locale // Debug current locale
@ -154,11 +162,103 @@ public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, Paymen
waitingContainer.addView(waitingBinding.getRoot()); waitingContainer.addView(waitingBinding.getRoot());
// Generate QR code when needed // Generate QR code when needed
// TO-DO fix M50F long time QR generated !!!
// waitingViewModel.generateQRCode(generatePaymentData()); // waitingViewModel.generateQRCode(generatePaymentData());
// Observe Face ID events
waitingViewModel.startFaceID.observe(this, start -> {
if (start != null && start) {
startFaceIDCamera();
}
});
waitingViewModel.faceIDSuccess.observe(this, success -> {
if (success != null && success) {
simulatePaymentAfterDelay();
}
});
// Set click listener for Face ID container
waitingBinding.faceIDContainer.setOnClickListener(v -> {
waitingViewModel.onFaceIDClicked();
});
startTransaction(); startTransaction();
} }
private void startFaceIDCamera() {
if (isFaceIDActive) return;
isFaceIDActive = true;
// Show camera container - now it's inside the FrameLayout
FrameLayout cameraContainer = waitingBinding.getRoot().findViewById(R.id.camera_container);
if (cameraContainer != null) {
cameraContainer.setVisibility(View.VISIBLE);
}
// Create TextureView for camera preview
TextureView textureView = new TextureView(this);
textureView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
// Add TextureView to camera container
if (cameraContainer != null) {
cameraContainer.addView(textureView, 0); // Add at index 0 (behind buttons)
}
// Set up close button
ImageButton btnCloseCamera = waitingBinding.getRoot().findViewById(R.id.btn_close_camera);
if (btnCloseCamera != null) {
btnCloseCamera.setOnClickListener(v -> {
closeFaceIDCamera();
});
}
// Initialize Face ID helper
faceIDHelper = new FaceIDHelper(this, textureView, new FaceIDHelper.FaceIDCallback() {
@Override
public void onFaceDetected() {
runOnUiThread(() -> {
// Face detected - proceed with payment
closeFaceIDCamera();
waitingViewModel.simulateFaceIDSuccess();
});
}
@Override
public void onCameraError(String error) {
runOnUiThread(() -> {
ToastUtils.showShort(error);
closeFaceIDCamera();
});
}
});
faceIDHelper.startBackgroundThread();
faceIDHelper.startCamera();
}
private void closeFaceIDCamera() {
if (!isFaceIDActive) return;
isFaceIDActive = false;
// Hide camera container
FrameLayout cameraContainer = waitingBinding.getRoot().findViewById(R.id.camera_container);
if (cameraContainer != null) {
cameraContainer.setVisibility(View.GONE);
cameraContainer.removeAllViews();
}
// Clean up camera resources
if (faceIDHelper != null) {
faceIDHelper.closeCamera();
faceIDHelper.stopBackgroundThread();
}
}
private void simulatePaymentAfterDelay() { private void simulatePaymentAfterDelay() {
boolean isTestMode = true; // Set this to false when not testing boolean isTestMode = true; // Set this to false when not testing
if (isTestMode) { if (isTestMode) {
@ -671,6 +771,10 @@ public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, Paymen
LogFileConfig.getInstance(this).readLog(); LogFileConfig.getInstance(this).readLog();
QPOSCallbackManager.getInstance().unregisterPaymentCallback(); QPOSCallbackManager.getInstance().unregisterPaymentCallback();
PrinterHelper.getInstance().close(); PrinterHelper.getInstance().close();
if (faceIDHelper != null) {
faceIDHelper.closeCamera();
faceIDHelper.stopBackgroundThread();
}
} }
private void convertReceiptToBitmap(final BitmapReadyListener listener) { private void convertReceiptToBitmap(final BitmapReadyListener listener) {

View File

@ -7,6 +7,7 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.databinding.ObservableBoolean; import androidx.databinding.ObservableBoolean;
import androidx.databinding.ObservableField; import androidx.databinding.ObservableField;
import androidx.lifecycle.MutableLiveData;
import com.dspread.pos.common.manager.QPOSCallbackManager; import com.dspread.pos.common.manager.QPOSCallbackManager;
import com.dspread.pos.printerAPI.PrinterHelper; import com.dspread.pos.printerAPI.PrinterHelper;
@ -22,7 +23,9 @@ public class WaitingForCardViewModel extends BaseViewModel {
public ObservableField<String> amount = new ObservableField<>("0.00"); public ObservableField<String> amount = new ObservableField<>("0.00");
public ObservableField<String> currencySymbol = new ObservableField<>("$"); public ObservableField<String> currencySymbol = new ObservableField<>("$");
// Add Face ID events
public MutableLiveData<Boolean> startFaceID = new MutableLiveData<>();
public MutableLiveData<Boolean> faceIDSuccess = new MutableLiveData<>();
public WaitingForCardViewModel(@NonNull Application application) { public WaitingForCardViewModel(@NonNull Application application) {
super(application); super(application);
} }
@ -60,6 +63,16 @@ public class WaitingForCardViewModel extends BaseViewModel {
} }
} }
// Add Face ID methods
public void onFaceIDClicked() {
startFaceID.setValue(true);
}
public void simulateFaceIDSuccess() {
// This will be called when face is detected
faceIDSuccess.setValue(true);
}
public void onCancel() { public void onCancel() {
// Handle cancel action // Handle cancel action
Log.d("WaitingForCardViewModel", "onCancel: "); Log.d("WaitingForCardViewModel", "onCancel: ");

View File

@ -0,0 +1,222 @@
package com.dspread.pos.utils;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Size;
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;
public class FaceIDHelper implements TextureView.SurfaceTextureListener {
private static final int REQUEST_CAMERA_PERMISSION = 200;
private Context context;
private TextureView textureView;
private FaceIDCallback callback;
private CameraManager cameraManager;
private CameraDevice cameraDevice;
private CameraCaptureSession cameraCaptureSession;
private CaptureRequest.Builder captureRequestBuilder;
private Handler backgroundHandler;
private HandlerThread backgroundThread;
private String frontCameraId;
public interface FaceIDCallback {
void onFaceDetected();
void onCameraError(String error);
}
public FaceIDHelper(Context context, TextureView textureView, FaceIDCallback callback) {
this.context = context;
this.textureView = textureView;
this.callback = callback;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
}
public void startCamera() {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
callback.onCameraError("Camera permission required");
return;
}
setupCamera();
}
private void setupCamera() {
try {
frontCameraId = getFrontCameraId();
if (frontCameraId == null) {
callback.onCameraError("Front camera not found");
return;
}
textureView.setSurfaceTextureListener(this);
} catch (CameraAccessException e) {
e.printStackTrace();
callback.onCameraError("Camera access error");
}
}
private String getFrontCameraId() throws CameraAccessException {
String[] cameraIds = cameraManager.getCameraIdList();
for (String cameraId : cameraIds) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
return cameraId;
}
}
return null;
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
openCamera();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
private void openCamera() {
try {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
return;
}
cameraManager.openCamera(frontCameraId, stateCallback, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
callback.onCameraError("Failed to open camera");
}
}
private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
cameraDevice = camera;
createCameraPreview();
// Simulate face detection after camera opens
simulateFaceDetection();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
cameraDevice.close();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
cameraDevice.close();
cameraDevice = null;
callback.onCameraError("Camera error: " + error);
}
};
private void createCameraPreview() {
try {
SurfaceTexture texture = textureView.getSurfaceTexture();
Surface surface = new Surface(texture);
captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
captureRequestBuilder.addTarget(surface);
cameraDevice.createCaptureSession(Arrays.asList(surface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
if (cameraDevice == null) return;
cameraCaptureSession = session;
try {
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
session.setRepeatingRequest(captureRequestBuilder.build(),
null, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
callback.onCameraError("Camera configuration failed");
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void simulateFaceDetection() {
// Simulate face detection after 3 seconds
new Handler().postDelayed(() -> {
if (callback != null) {
callback.onFaceDetected();
}
}, 18000);
}
public void startBackgroundThread() {
backgroundThread = new HandlerThread("CameraBackground");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
public void stopBackgroundThread() {
if (backgroundThread != null) {
backgroundThread.quitSafely();
try {
backgroundThread.join();
backgroundThread = null;
backgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void closeCamera() {
if (cameraCaptureSession != null) {
cameraCaptureSession.close();
cameraCaptureSession = null;
}
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#80000000" />
<corners android:radius="24dp" />
</shape>

View File

@ -5,11 +5,15 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<variable <variable name="vm" type="com.dspread.pos.ui.payment.WaitingForCardViewModel" />
name="vm"
type="com.dspread.pos.ui.payment.WaitingForCardViewModel" />
</data> </data>
<!-- Use FrameLayout as root to contain both camera overlay and main content -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Main Content -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -24,29 +28,13 @@
android:layout_weight="1" android:layout_weight="1"
android:gravity="center"> android:gravity="center">
<ImageView <ImageView
android:paddingTop="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="300dp" android:layout_height="@dimen/dp_300"
android:src="@drawable/melberry_char_purple" android:paddingTop="10dp"
android:scaleType="fitCenter"> android:scaleType="fitCenter"
android:src="@drawable/melberry_char_purple" />
</ImageView>
<!-- <pl.droidsonroids.gif.GifImageView-->
<!-- android:layout_width="200dp"-->
<!-- android:layout_height="200dp"-->
<!-- android:src="@drawable/checkcard" />-->
</LinearLayout> </LinearLayout>
<!-- Instruction text -->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="16dp"-->
<!-- android:text="@string/scan_or_tap_instruction"-->
<!-- android:textAlignment="center"-->
<!-- android:textSize="16sp" />-->
<!-- Amount and currency section --> <!-- Amount and currency section -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -55,7 +43,6 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center" android:gravity="center"
android:layout_weight="1"> android:layout_weight="1">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -63,7 +50,6 @@
android:textSize="36sp" android:textSize="36sp"
android:textColor="#2C2929" android:textColor="#2C2929"
tools:text="100.00" /> tools:text="100.00" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -86,6 +72,7 @@
<!-- Left side - Face ID Card --> <!-- Left side - Face ID Card -->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/faceIDContainer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="260dp" android:layout_height="260dp"
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
@ -116,7 +103,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical">
<pl.droidsonroids.gif.GifImageView <pl.droidsonroids.gif.GifImageView
android:layout_width="98dp" android:layout_width="98dp"
android:layout_height="124dp" android:layout_height="124dp"
@ -159,6 +145,7 @@
<!-- Right side - QR Code Card --> <!-- Right side - QR Code Card -->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/qrCodeContainer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="260dp" android:layout_height="260dp"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
@ -201,4 +188,43 @@
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Camera Container (Full Screen Overlay) -->
<FrameLayout
android:id="@+id/camera_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@android:color/black">
<!-- Camera Preview will be added here programmatically -->
<!-- Close button for camera -->
<ImageButton
android:id="@+id/btn_close_camera"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="24dp"
android:src="@drawable/ic_close_white"
android:background="@drawable/rounded_bg_black_50"
android:scaleType="center"
android:contentDescription="Close_camera"
android:layout_gravity="top|end" />
<!-- Face detection guidance -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Смотрите в камеру\nЛицо должно быть в рамке"
android:textColor="@color/white"
android:textSize="18sp"
android:textAlignment="center"
android:gravity="center"
android:layout_gravity="center"
android:padding="16dp"
android:background="@drawable/rounded_bg_black_50"
android:visibility="visible" />
</FrameLayout>
</FrameLayout>
</layout> </layout>