From c6cb612c99e1283b0dc0a06acb36c26e222d6d15 Mon Sep 17 00:00:00 2001 From: Ahmed Al-Omairi Date: Sun, 31 Aug 2025 18:17:23 +0300 Subject: [PATCH] Face ID first attemp, on waiting_for-card layout --- pos_android_app/src/main/AndroidManifest.xml | 5 + .../pos/ui/payment/PaymentActivity.java | 104 +++++ .../ui/payment/WaitingForCardViewModel.java | 15 +- .../com/dspread/pos/utils/FaceIDHelper.java | 222 +++++++++++ .../src/main/res/drawable/ic_close_white.xml | 9 + .../main/res/drawable/rounded_bg_black_50.xml | 5 + .../src/main/res/layout/waiting_for_card.xml | 370 ++++++++++-------- 7 files changed, 557 insertions(+), 173 deletions(-) create mode 100644 pos_android_app/src/main/java/com/dspread/pos/utils/FaceIDHelper.java create mode 100644 pos_android_app/src/main/res/drawable/ic_close_white.xml create mode 100644 pos_android_app/src/main/res/drawable/rounded_bg_black_50.xml diff --git a/pos_android_app/src/main/AndroidManifest.xml b/pos_android_app/src/main/AndroidManifest.xml index 08ccc05..2a1f25c 100644 --- a/pos_android_app/src/main/AndroidManifest.xml +++ b/pos_android_app/src/main/AndroidManifest.xml @@ -1,6 +1,11 @@ + + + + + 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 3ec23d9..62db899 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 @@ -9,11 +9,13 @@ import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.util.Base64; import android.util.Log; +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; @@ -60,6 +62,9 @@ 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; + public class PaymentActivity extends BaseActivity implements PaymentServiceCallback { @@ -91,6 +96,9 @@ public class PaymentActivity extends BaseActivity { + 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(); } + 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() { boolean isTestMode = true; // Set this to false when not testing if (isTestMode) { @@ -671,6 +771,10 @@ public class PaymentActivity extends BaseActivity amount = new ObservableField<>("0.00"); public ObservableField currencySymbol = new ObservableField<>("$"); - + // Add Face ID events + public MutableLiveData startFaceID = new MutableLiveData<>(); + public MutableLiveData faceIDSuccess = new MutableLiveData<>(); public WaitingForCardViewModel(@NonNull Application 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() { // Handle cancel action Log.d("WaitingForCardViewModel", "onCancel: "); 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/utils/FaceIDHelper.java new file mode 100644 index 0000000..0f1a080 --- /dev/null +++ b/pos_android_app/src/main/java/com/dspread/pos/utils/FaceIDHelper.java @@ -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; + } + } +} \ No newline at end of file diff --git a/pos_android_app/src/main/res/drawable/ic_close_white.xml b/pos_android_app/src/main/res/drawable/ic_close_white.xml new file mode 100644 index 0000000..7b234aa --- /dev/null +++ b/pos_android_app/src/main/res/drawable/ic_close_white.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/pos_android_app/src/main/res/drawable/rounded_bg_black_50.xml b/pos_android_app/src/main/res/drawable/rounded_bg_black_50.xml new file mode 100644 index 0000000..36d957f --- /dev/null +++ b/pos_android_app/src/main/res/drawable/rounded_bg_black_50.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/pos_android_app/src/main/res/layout/waiting_for_card.xml b/pos_android_app/src/main/res/layout/waiting_for_card.xml index 59aff9b..58bb42b 100644 --- a/pos_android_app/src/main/res/layout/waiting_for_card.xml +++ b/pos_android_app/src/main/res/layout/waiting_for_card.xml @@ -5,200 +5,226 @@ xmlns:tools="http://schemas.android.com/tools"> - + - + + android:layout_height="match_parent"> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:padding="16dp"> - - + - - - + + android:layout_height="@dimen/dp_300" + android:paddingTop="10dp" + android:scaleType="fitCenter" + android:src="@drawable/melberry_char_purple" /> + - - + + + + + - - + + - - - + + - + + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#8c1084" + android:padding="5dp"> - - + + - - + + + + + + + + android:layout_marginTop="10dp" + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_gravity="center"> + + + + + + + - - + - - + + - - - - - + + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#8c1084" + android:padding="3dp"> - - - - + + + + + + + + - + + + + + + + + + + + + + + \ No newline at end of file