Compare commits

...

3 Commits

15 changed files with 1138 additions and 243 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,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

@ -0,0 +1,263 @@
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;
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.Log;
import android.view.Surface;
import android.view.TextureView;
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 static final int DETECTION_INTERVAL_MS = 1000; // Check every second
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;
private RealFaceDetector realFaceDetector;
private Handler detectionHandler;
private Runnable detectionRunnable;
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);
// 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() {
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();
startFaceDetection();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
stopFaceDetection();
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();
}
@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 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);
}
};
detectionHandler.postDelayed(detectionRunnable, DETECTION_INTERVAL_MS);
}
private void stopFaceDetection() {
if (detectionHandler != null && detectionRunnable != null) {
detectionHandler.removeCallbacks(detectionRunnable);
}
realFaceDetector.stop();
}
public void startBackgroundThread() {
backgroundThread = new HandlerThread("CameraBackground");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
public void stopBackgroundThread() {
stopFaceDetection();
if (backgroundThread != null) {
backgroundThread.quitSafely();
try {
backgroundThread.join();
backgroundThread = null;
backgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void closeCamera() {
stopFaceDetection();
if (cameraCaptureSession != null) {
cameraCaptureSession.close();
cameraCaptureSession = null;
}
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = 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

@ -6,6 +6,8 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.dspread.pos.common.base.BaseFragment; import com.dspread.pos.common.base.BaseFragment;
import com.dspread.pos.TitleProviderListener; import com.dspread.pos.TitleProviderListener;
import com.dspread.pos.ui.main.MainActivity; import com.dspread.pos.ui.main.MainActivity;
@ -19,7 +21,17 @@ public class MulberryFragment extends BaseFragment<FragmentMulberryBinding, Mulb
@Override @Override
public void initViewObservable() { public void initViewObservable() {
// Observe item click events // Call this after view is created
RecyclerView recyclerView = binding.getRoot().findViewById(R.id.mulberryCards_list);
viewModel.adjustMulberryView(binding.ivLogo,
binding.topLogoContainer,
binding.footerContainer,
binding.centerContainer,
binding.getRoot()
);
// Handle fragment navigation // Handle fragment navigation
viewModel.navigateToFragment.observe(this, fragmentId -> { viewModel.navigateToFragment.observe(this, fragmentId -> {
Activity activity = getActivity(); Activity activity = getActivity();

View File

@ -7,18 +7,24 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.databinding.Observable; import androidx.databinding.Observable;
import androidx.databinding.ObservableArrayList; import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableField; import androidx.databinding.ObservableField;
import androidx.databinding.ObservableList; import androidx.databinding.ObservableList;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.dspread.pos.common.base.BaseAppViewModel; import com.dspread.pos.common.base.BaseAppViewModel;
import com.dspread.pos.data.local.PreferencesManager; import com.dspread.pos.data.local.PreferencesManager;
import com.dspread.pos.utils.ImageUtils;
import com.dspread.pos.utils.PosParameters; import com.dspread.pos.utils.PosParameters;
import com.dspread.pos.utils.TRACE; import com.dspread.pos.utils.TRACE;
import com.dspread.pos_android_app.BR; import com.dspread.pos_android_app.BR;
@ -35,11 +41,23 @@ import java.util.Map;
import me.tatarka.bindingcollectionadapter2.ItemBinding; import me.tatarka.bindingcollectionadapter2.ItemBinding;
import me.goldze.mvvmhabit.bus.event.SingleLiveEvent; import me.goldze.mvvmhabit.bus.event.SingleLiveEvent;
import me.goldze.mvvmhabit.base.ItemViewModel;
public class MulberryViewModel extends BaseAppViewModel { public class MulberryViewModel extends BaseAppViewModel {
private static final String TAG = "MulberryVM"; private static final String TAG = "MulberryVM";
// Device type constants
private static final int DEVICE_TYPE_SMALL = 1; // D20, D30, D30M
private static final int DEVICE_TYPE_COMPACT = 2; // D70 (wide but small)
private static final int DEVICE_TYPE_LARGE = 3; // D80, D80K, others
private int currentDeviceType;
// Device-specific observables
public final ObservableField<Integer> logoHeight = new ObservableField<>();
public final ObservableField<Integer> gridSpanCount = new ObservableField<>();
public final ObservableField<Integer> maxNavItems = new ObservableField<>();
public final ObservableField<Integer> cardImageSize = new ObservableField<>();
// UI Observables // UI Observables
public final ObservableField<Integer> mainLogo = new ObservableField<>(); public final ObservableField<Integer> mainLogo = new ObservableField<>();
public final ObservableField<Integer> footerLogo = new ObservableField<>(); public final ObservableField<Integer> footerLogo = new ObservableField<>();
@ -63,23 +81,26 @@ public class MulberryViewModel extends BaseAppViewModel {
private final Map<String, Bitmap> imageCache = new HashMap<>(); private final Map<String, Bitmap> imageCache = new HashMap<>();
private final MutableLiveData<Boolean> imagesLoaded = new MutableLiveData<>(false); private final MutableLiveData<Boolean> imagesLoaded = new MutableLiveData<>(false);
// Provide height in pixels, not dp
public MulberryViewModel(@NonNull Application application) { public MulberryViewModel(@NonNull Application application) {
super(application); super(application);
TRACE.i("Mulberry fragment init"); TRACE.i("Mulberry fragment init");
Log.d(TAG, "MulberryViewModel constructor called"); Log.d(TAG, "MulberryViewModel constructor called");
// Add this to track initialization // Add this to track initialization
Log.d(TAG, "ViewModel hash: " + this.hashCode()); Log.d(TAG, "ViewModel hash: " + this.hashCode());
if (Build.MODEL.equalsIgnoreCase("D70") || // if (Build.MODEL.equalsIgnoreCase("D70") ||
Build.MODEL.equalsIgnoreCase("D30") || // Build.MODEL.equalsIgnoreCase("D30") ||
Build.MODEL.equalsIgnoreCase("D30M") || // Build.MODEL.equalsIgnoreCase("D30M") ||
Build.MODEL.equalsIgnoreCase("D80")|| // Build.MODEL.equalsIgnoreCase("D80")||
Build.MODEL.equalsIgnoreCase("D80K")) { // Build.MODEL.equalsIgnoreCase("D80K")) {
Log.d("Build.MODEL", "MainViewModel: " + Build.MODEL);; // Log.d("Build.MODEL", "MainViewModel: " + Build.MODEL);;
// To-Do set small size of logo ,and grid size 3 by 1 // // To-Do set small size of logo ,and grid size 3 by 1
} // }
// Observe WebSocket connection status // Observe WebSocket connection status
getWebSocketConnectionStatus().observeForever(isConnected -> { getWebSocketConnectionStatus().observeForever(isConnected -> {
@ -99,6 +120,82 @@ public class MulberryViewModel extends BaseAppViewModel {
initialize(); initialize();
} }
public void adjustMulberryView(ImageView logoImageView,
LinearLayout containerLayout,
LinearLayout footerContainer,
LinearLayout centerContainer,
View rootView
) {
// Find RecyclerView from root view
String model = Build.MODEL.toUpperCase();
int heightPx = 150; // default (~100dp)
int paddingPx = 10 ; // default 10dp
if (model.contains("D70")) {
heightPx = 20; // 20dp for D70
paddingPx = 2 ; // 2dp for D70
containerLayout.setVisibility(View.GONE);
footerContainer.setVisibility(View.GONE);
centerContainer.setVisibility(View.GONE);
// Adjust GridLayout columnCount
RecyclerView recyclerView = rootView.findViewById(R.id.mulberryCards_list);
if (recyclerView != null && recyclerView.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
layoutManager.setSpanCount(3);
// Set padding on the RecyclerView (not LayoutManager)
recyclerView.setPadding(0, 1, 0, 1);
recyclerView.setLayoutManager(layoutManager);
}
// Adjust CardView size
CardView cardView = rootView.findViewById(R.id.widget_card);
if (cardView != null) {
int sizeInPx = 80; // Convert 120dp to pixels
ViewGroup.LayoutParams params = cardView.getLayoutParams();
params.width = sizeInPx;
params.height = sizeInPx;
cardView.setLayoutParams(params);
}
for (int i = 0; i < recyclerView.getChildCount(); i++) {
View cardViews = recyclerView.getChildAt(i);
if (cardViews instanceof CardView) {
// Adjust card padding
cardViews.setPadding( 0,0,0,0
);
// Find and resize the ImageView
ImageView imageView = cardView.findViewById(R.id.navImage);
if (imageView != null) {
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.width = 80;
params.height = 80;
imageView.setLayoutParams(params);
}
}
}
} else if ( model.contains("D20")) {
heightPx = 100; // ~80dp for D30/D20
paddingPx = 8 ;
} else if (model.contains("D30")) {
heightPx = 80;
// containerLayout.setVisibility(View.GONE);
footerContainer.setVisibility(View.GONE);
centerContainer.setVisibility(View.GONE);
paddingPx = 0 ;
}
ViewGroup.LayoutParams params = logoImageView.getLayoutParams();
params.height = heightPx;
logoImageView.setLayoutParams(params);
containerLayout.setPadding(0, paddingPx, 0, paddingPx);
}
private void initialize() { private void initialize() {
Log.d(TAG, "Initializing ViewModel"); Log.d(TAG, "Initializing ViewModel");
@ -424,61 +521,74 @@ public class MulberryViewModel extends BaseAppViewModel {
JSONArray navItemsArray = new JSONArray(json); JSONArray navItemsArray = new JSONArray(json);
List<NavItemViewModel> enabledItems = new ArrayList<>(); List<NavItemViewModel> enabledItems = new ArrayList<>();
// First pass: collect all enabled items with their original JSON data
for (int i = 0; i < navItemsArray.length(); i++) { for (int i = 0; i < navItemsArray.length(); i++) {
JSONObject itemJson = navItemsArray.getJSONObject(i); JSONObject itemJson = navItemsArray.getJSONObject(i);
// Use your exact existing model structure
NavItemViewModel item = new NavItemViewModel( NavItemViewModel item = new NavItemViewModel(
itemJson.getString("navName"), itemJson.getString("navName"),
parseIconSource(itemJson.opt("icon")), parseIconSource(itemJson.opt("icon")),
itemJson.optInt("position", 0), itemJson.optInt("position", 0),
itemJson.optBoolean("enabled", true) // Keep enabled status itemJson.optBoolean("enabled", true),
itemJson // Store the original JSON to preserve page info
); );
// Only add enabled items (matching your working version's behavior)
if (item.enabled) { if (item.enabled) {
enabledItems.add(item); enabledItems.add(item);
Log.d(TAG, "Loaded item: " + item.name + Log.d(TAG, "Loaded item: " + item.name + " | Pos: " + item.position + " | Enabled: " + item.enabled);
" | Pos: " + item.position +
" | Enabled: " + item.enabled);
} }
} }
// 2. Sort with position logging (like your working version) // Sort by position (same as working version)
Collections.sort(enabledItems, (i1, i2) -> { Collections.sort(enabledItems, (i1, i2) -> {
Log.v(TAG, "Sorting: " + i1.position + " vs " + i2.position); Log.v(TAG, "Sorting: " + i1.position + " vs " + i2.position);
return Integer.compare(i1.position, i2.position); return Integer.compare(i1.position, i2.position);
}); });
// 3. Create ViewModels - optimized page ID handling // Create ViewModels using the original JSON data for page mapping
for (int i = 0; i < enabledItems.size(); i++) { for (NavItemViewModel item : enabledItems) {
JSONObject originalJson = navItemsArray.getJSONObject(i); try {
NavItemViewModel item = enabledItems.get(i); String pageName = item.originalJson.optString("navPage");
int pageId = PAGE_MAP.containsKey(pageName) ? PAGE_MAP.get(pageName) : NAV_HOME;
// Directly get page ID from the original JSON navItems.add(new MulberryItemViewModel(
String pageName = originalJson.optString("navPage"); this,
int pageId = PAGE_MAP.containsKey(pageName) item.name,
? PAGE_MAP.get(pageName) item.iconSource,
: NAV_HOME; pageId,
null
));
Log.d(TAG, "Added item: " + item.name + " | Page: " + pageName + " | PageID: " + pageId);
} catch (Exception e) {
navItems.add(new MulberryItemViewModel( Log.e(TAG, "Error creating ViewModel for item: " + item.name, e);
this, }
item.name,
item.iconSource,
pageId, // Maintain page structure
// null,
null
));
} }
Log.d(TAG, "Loaded " + navItems.size() + " items from prefs"); Log.d(TAG, "Loaded " + navItems.size() + " items from prefs");
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Error parsing nav items JSON", e); Log.e(TAG, "Error parsing nav items JSON", e);
} }
} }
// Updated model class to store original JSON
private static class NavItemViewModel {
String name;
Object iconSource;
int position;
boolean enabled;
JSONObject originalJson; // Store original JSON for page mapping
NavItemViewModel(String name, Object iconSource, int position, boolean enabled, JSONObject originalJson) {
this.name = name;
this.iconSource = iconSource;
this.position = position;
this.enabled = enabled;
this.originalJson = originalJson;
}
}
private Object parseIconSource(Object icon) { private Object parseIconSource(Object icon) {
if (icon instanceof String) { if (icon instanceof String) {
try { try {
@ -492,20 +602,6 @@ public class MulberryViewModel extends BaseAppViewModel {
return icon; // Return as-is if not string return icon; // Return as-is if not string
} }
// Simple model for navigation items
private static class NavItemViewModel {
String name;
Object iconSource;
int position;
boolean enabled;
NavItemViewModel(String name, Object iconSource, int position, boolean enabled) {
this.name = name;
this.iconSource = iconSource;
this.position = position;
this.enabled = enabled;
}
}
@Override @Override
protected void onCleared() { protected void onCleared() {

View File

@ -4,24 +4,20 @@ 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;
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.ArrayAdapter; import android.widget.ArrayAdapter;
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;
@ -31,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;
@ -56,10 +51,11 @@ 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 com.dspread.pos.faceID.FaceIDHelper;
public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, PaymentViewModel> implements PaymentServiceCallback { public class PaymentActivity extends BaseActivity<ActivityPaymentBinding, PaymentViewModel> implements PaymentServiceCallback {
@ -91,6 +87,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 +153,108 @@ 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() {
Log.d("FaceDetection", "✅ Face detected successfully!");
runOnUiThread(() -> {
// Face detected - proceed with payment
closeFaceIDCamera();
// 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();
});
}
});
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) {
@ -187,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
); );
} }
}); });
@ -671,6 +767,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);
} }
@ -48,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");
@ -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

@ -276,7 +276,7 @@ public class PosParameters {
map.put("scan", R.drawable.ax_scaner_grey); map.put("scan", R.drawable.ax_scaner_grey);
map.put("printer", R.drawable.ax_receipt_grey); map.put("printer", R.drawable.ax_receipt_grey);
map.put("settings", R.drawable.ax_settings_grey); map.put("settings", R.drawable.ax_settings_grey);
map.put("pinpad", R.drawable.ax_menu_grey); map.put("pinpad", R.drawable.ax_pinpad_grey);
map.put("cashier", R.drawable.ax_cashier_grey); map.put("cashier", R.drawable.ax_cashier_grey);
map.put("mulberry", R.drawable.am_mulberry_logo_wide_color); map.put("mulberry", R.drawable.am_mulberry_logo_wide_color);
map.put("overtec", R.drawable.am_overtec_logo_wide_color); map.put("overtec", R.drawable.am_overtec_logo_wide_color);

View File

@ -0,0 +1,179 @@
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="250dp" android:viewportHeight="1024" android:viewportWidth="1024" android:width="250dp">
<path android:pathData="M263.9,96.4c-9.8,2.6 -17.1,8.9 -21.7,18.6 -1.6,3.2 -1.7,28 -1.7,340.5l0,337 3.4,6.3c3.7,6.8 8.3,11 15.8,14.5 4.5,2.1 6.3,2.2 32.1,2.5l27.2,0.3 0,35.3c0,37.9 0.3,40.2 5.6,50.8 3.8,7.4 12.5,16.1 20.4,20.2 13,6.9 4.8,6.6 167,6.6 159.5,-0 153.5,0.2 165.7,-5.7 7.2,-3.4 18.1,-14 21.5,-20.8 5.6,-10.9 5.8,-13 5.8,-51.1l0,-35.3 27.3,-0.3c26.9,-0.3 27.3,-0.3 32.8,-2.9 6.8,-3.2 12,-8.2 15.7,-15.2l2.7,-5.2 0,-337 0,-337 -3.4,-6.3c-3.7,-6.8 -8.3,-11 -15.8,-14.5l-4.8,-2.2 -245.5,-0.2c-192.4,-0.1 -246.5,0.1 -250.1,1.1zM753,455.5l0,329.5 -241,-0 -241,-0 0,-329.5 0,-329.5 241,-0 241,-0 0,329.5zM674.8,849.7c-0.3,38.4 -0.4,38.9 -8.7,44.8l-4.3,3 -149.8,-0 -149.8,-0 -4.3,-3c-8.3,-5.9 -8.4,-6.4 -8.7,-44.8l-0.3,-33.7 163.1,-0 163.1,-0 -0.3,33.7z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="240.5" android:endY="929" android:startX="240.5" android:startY="95.3" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M327.3,175c-2.5,1.1 -5.1,3.3 -6.3,5.2 -2,3.2 -2,4.8 -2,89.3 0,83.6 0.1,86.1 2,89.2 1,1.8 3.7,4.1 5.9,5.3 4,2 4.8,2 185.1,2 180.3,-0 181.1,-0 185.1,-2 2.2,-1.2 4.9,-3.5 5.9,-5.3 1.9,-3.1 2,-5.6 2,-89.2 0,-84.5 0,-86.1 -2,-89.3 -1.2,-1.9 -3.8,-4.1 -6.3,-5.2 -4.1,-1.9 -8.8,-2 -184.7,-2 -175.9,-0 -180.6,0.1 -184.7,2zM675,269.5l0,65.5 -163,-0 -163,-0 0,-65.5 0,-65.5 163,-0 163,-0 0,65.5z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="319" android:endY="366" android:startX="319" android:startY="173" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M347.3,429.5c-29.6,8.3 -44.1,40 -30.7,67.3 6.9,14.2 20.4,24 36.3,26.5 18.3,2.8 38.7,-7.3 48,-23.7 14.3,-25.2 2.5,-58.3 -24.6,-68.6 -7.5,-2.9 -21.6,-3.6 -29,-1.5zM366.9,460.5c7,3.5 11,12.3 9.1,19.5 -3.7,13.7 -20.2,17.5 -29.7,6.7 -7,-8 -4.5,-21.4 4.9,-26 4.1,-2.1 12,-2.2 15.7,-0.2z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="311.6" android:endY="523.8" android:startX="311.6" android:startY="428.3" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M500,429.4c-18.1,5.1 -32.1,20.6 -35.3,38.9 -5.3,31.3 23.2,60 54.5,54.8 18.9,-3.1 34.2,-16.9 39,-35 4.5,-17.4 -0.5,-34.6 -13.7,-47 -6.7,-6.2 -12.4,-9.5 -20.8,-11.7 -6.3,-1.7 -17.6,-1.7 -23.7,-0zM521,461.2c5.4,3.7 7.3,7.1 7.7,13.5 0.3,4.7 -0.1,6.6 -1.9,9.8 -2.7,4.7 -9.4,8.5 -14.8,8.5 -5.4,-0 -12.1,-3.8 -14.8,-8.5 -1.8,-3.2 -2.2,-5.1 -1.9,-9.8 0.6,-9.8 6.9,-15.7 16.7,-15.7 4.3,-0 6.5,0.6 9,2.2z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="464.1" android:endY="523.7" android:startX="464.1" android:startY="428.1" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M654.3,429.5c-25.4,6.9 -40.5,33.3 -33.7,59 5.5,21.3 27.8,37.1 49.6,35.1 41.9,-3.8 59.5,-54.3 28.9,-82.9 -11.5,-10.8 -29.5,-15.3 -44.8,-11.2zM675,460.9c9.6,5.1 11.8,17.9 4.4,26.1 -6.2,6.8 -15.9,7.9 -22.9,2.6 -10,-7.7 -9.1,-22 1.9,-28.7 4.1,-2.5 12,-2.5 16.6,-0z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="619" android:endY="523.8" android:startX="619" android:startY="428" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M350.1,537.1c-7.9,1.5 -18.4,7.3 -24.4,13.2 -19.4,19.5 -18.7,51 1.6,69.3 21.9,19.7 56.1,15.4 71.9,-9 10.1,-15.7 10.5,-36.5 0.9,-51.5 -10.9,-16.9 -31,-25.8 -50,-22zM366.6,568.6c13,6.2 13.1,24.3 0.2,31 -3.6,1.9 -11.2,1.8 -15.4,-0.2 -9.5,-4.5 -12.2,-18 -5.1,-26.1 5.4,-6.2 13.3,-8 20.3,-4.7z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="311.6" android:endY="632" android:startX="311.6" android:startY="536.2" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M502.5,537.2c-13.4,2.5 -27.2,13.2 -33.4,26 -12.3,25.1 -0.4,55.4 26,65.9 6.8,2.7 19.7,3.6 27,1.9 21.8,-5 38.1,-25.3 37.8,-47 -0.3,-29.9 -27.9,-52.5 -57.4,-46.8zM520.1,569.3c6.3,3.3 9.1,8.5 8.6,16 -0.6,9.8 -6.9,15.7 -16.7,15.7 -9.8,-0 -16.1,-5.9 -16.7,-15.7 -0.3,-4.7 0.1,-6.6 1.9,-9.7 2.7,-4.7 9.5,-8.6 14.8,-8.6 2,-0 5.7,1 8.1,2.3z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="464.2" android:endY="631.9" android:startX="464.2" android:startY="536.3" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M656.3,537.5c-17.2,3.9 -32.4,18.8 -36.2,35.6 -1.7,7.3 -1.3,18.1 0.9,25.3 4.2,13.3 15.7,25.7 28.7,30.6 7.3,2.8 20.1,3.7 27.4,2 24.4,-5.8 40.7,-29.4 36.9,-53.7 -1.3,-8.1 -6.3,-19.2 -11.4,-24.9 -10.8,-12.5 -30,-18.7 -46.3,-14.9zM673.3,568.4c4.4,1.8 9.5,7.8 10.3,12.1 1.4,7.5 -2.2,15.2 -8.6,18.6 -4.2,2.3 -12,2.5 -15.9,0.4 -4,-2 -7.9,-6.9 -9,-11.3 -3.5,-13.2 10.5,-25.2 23.2,-19.8z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="619" android:endY="631.9" android:startX="619" android:startY="536.4" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M344.8,646.9c-16.7,5.4 -30.3,21.4 -32.7,38.7 -4.5,31.3 22.4,58.6 53.4,54.3 12.1,-1.6 21.1,-6.4 29.4,-15.5 9.1,-9.9 12.6,-20.4 11.9,-34.9 -0.4,-8.1 -1,-10.4 -4,-16.7 -6.2,-12.8 -15.7,-21.4 -28.6,-25.7 -8,-2.7 -21.3,-2.8 -29.4,-0.2zM368.7,678.1c7.9,4.9 10.2,15.9 4.9,23.9 -3,4.5 -9,7.9 -14.1,8 -4.4,-0 -11.6,-4 -14.2,-7.9 -10.3,-15.4 7.6,-33.8 23.4,-24z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="311.6" android:endY="740.3" android:startX="311.6" android:startY="645" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M496.8,647.3c-23.9,8.2 -37,32 -31.3,56.6 4.4,18.6 20.7,33.3 39.8,36 37.4,5.2 66.1,-33.4 50.1,-67.6 -10.2,-21.7 -35.9,-32.7 -58.6,-25zM516.2,676c7.7,2.2 12.8,8.9 12.8,17 0,13.9 -17.2,21.9 -27.6,12.7 -4.5,-3.9 -6.4,-7.6 -6.4,-12.7 0,-7.8 4.3,-13.9 11.7,-16.5 4.7,-1.7 4.9,-1.7 9.5,-0.5z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="464.2" android:endY="740.4" android:startX="464.2" android:startY="644.8" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M653.5,646.4c-20.7,6.6 -34.5,25 -34.5,46 0.1,29.5 24.9,51.1 54.5,47.5 25.4,-3.1 44.6,-28.8 40.5,-54.3 -2.9,-18.4 -16.4,-33.9 -33.9,-39 -6.5,-1.9 -20.9,-2 -26.6,-0.2zM671.2,676c15.3,4.3 16.9,25.5 2.4,32.1 -5.2,2.3 -9,2.4 -14.2,-0 -10.6,-4.8 -13.1,-19.3 -4.7,-27.7 4.8,-4.8 10,-6.2 16.5,-4.4z" android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient android:endX="619" android:endY="740.3" android:startX="619" android:startY="645.1" android:type="linear">
<item android:color="#FFACAFCA" android:offset="0"/>
<item android:color="#FF44454E" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
</vector>

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,6 +5,10 @@
xmlns:binding="http://schemas.android.com/apk/res-auto"> xmlns:binding="http://schemas.android.com/apk/res-auto">
<data> <data>
<variable
name="view"
type="com.dspread.pos.ui.mulberry.MulberryFragment" />
<variable <variable
name="viewModel" name="viewModel"
type="com.dspread.pos.ui.mulberry.MulberryViewModel" /> type="com.dspread.pos.ui.mulberry.MulberryViewModel" />
@ -33,11 +37,18 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toTopOf="@id/gridLayout"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent"
>
<ImageView <ImageView
android:id="@+id/ivLogo" android:id="@+id/ivLogo"
@ -55,12 +66,18 @@
android:id="@+id/gridLayout" android:id="@+id/gridLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alignmentMode="alignMargins" android:alignmentMode="alignMargins"
android:columnCount="2" android:columnCount="2"
android:columnOrderPreserved="false" android:columnOrderPreserved="false"
android:rowCount="2" android:rowCount="2"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
app:layout_constraintBottom_toTopOf="@id/centerContainer"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintTop_toBottomOf="@id/topLogoContainer" app:layout_constraintTop_toBottomOf="@id/topLogoContainer"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -102,7 +119,7 @@
android:id="@+id/card" android:id="@+id/card"
android:layout_width="140dp" android:layout_width="140dp"
android:layout_height="@dimen/main_grid_image_height_d30" android:layout_height="@dimen/main_grid_image_height_d30"
android:scaleType="centerCrop" android:scaleType="fitCenter"
android:src="@drawable/homo_card_black_box" android:src="@drawable/homo_card_black_box"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/cardText" app:layout_constraintBottom_toTopOf="@+id/cardText"

View File

@ -22,10 +22,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:padding="14dp">
<ImageView <ImageView
android:layout_width="100dp" android:id="@+id/navImage"
android:layout_width="wrap_content"
android:layout_height="100dp" android:layout_height="100dp"
android:src="@{viewModel.icon}" android:src="@{viewModel.icon}"
android:scaleType="fitCenter" android:scaleType="fitCenter"

View File

@ -5,200 +5,255 @@
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>
<LinearLayout <!-- Use FrameLayout as root to contain both camera overlay and main content -->
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<!-- Card animation GIF --> <!-- Main Content -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:paddingTop="10dp"
android:layout_width="match_parent"
android:layout_height="300dp"
android:src="@drawable/melberry_char_purple"
android:scaleType="fitCenter">
</ImageView>
<!-- <pl.droidsonroids.gif.GifImageView-->
<!-- android:layout_width="200dp"-->
<!-- android:layout_height="200dp"-->
<!-- android:src="@drawable/checkcard" />-->
</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 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:gravity="center"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.amount}"
android:textSize="36sp"
android:textColor="#2C2929"
tools:text="100.00" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.currencySymbol}"
android:textSize="36sp"
android:layout_marginLeft="8dp"
android:textColor="#2C2929"
tools:text="$" />
</LinearLayout>
<!-- Divided section for QR code and Face ID -->
<LinearLayout
android:id="@+id/twoCardsContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginTop="20dp" android:gravity="center"
android:orientation="horizontal" android:orientation="vertical"
android:weightSum="2" android:padding="16dp">
android:paddingHorizontal="1dp">
<!-- Left side - Face ID Card --> <!-- Card animation GIF -->
<androidx.cardview.widget.CardView <LinearLayout
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="260dp" android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
android:layout_weight="1" android:layout_weight="1"
app:cardCornerRadius="18dp" android:gravity="center">
app:cardElevation="2dp" <ImageView
app:cardUseCompatPadding="true">
<!-- Outer Card (White Background) -->
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="@dimen/dp_300"
android:orientation="vertical" android:paddingTop="10dp"
android:background="#8c1084" android:scaleType="fitCenter"
android:padding="5dp"> android:src="@drawable/melberry_char_purple" />
</LinearLayout>
<!-- Inner Card (Purple Background) --> <!-- Amount and currency section -->
<androidx.cardview.widget.CardView <LinearLayout
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="170dp" android:layout_height="wrap_content"
app:cardCornerRadius="12dp" android:layout_marginTop="16dp"
android:layout_gravity="center" android:orientation="horizontal"
android:background="@android:color/holo_purple"> android:gravity="center"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.amount}"
android:textSize="36sp"
android:textColor="#2C2929"
tools:text="100.00" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.currencySymbol}"
android:textSize="36sp"
android:layout_marginLeft="8dp"
android:textColor="#2C2929"
tools:text="$" />
</LinearLayout>
<!-- Center the GIF Image --> <!-- Divided section for QR code and Face ID -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/twoCardsContainer"
android:layout_height="match_parent" android:layout_width="match_parent"
android:gravity="center" android:layout_height="wrap_content"
android:orientation="vertical"> android:layout_marginTop="20dp"
android:orientation="horizontal"
android:weightSum="2"
android:paddingHorizontal="1dp">
<pl.droidsonroids.gif.GifImageView <!-- Left side - Face ID Card -->
android:layout_width="98dp" <androidx.cardview.widget.CardView
android:layout_height="124dp" android:id="@+id/faceIDContainer"
android:src="@drawable/faceid_180px" /> android:layout_width="0dp"
</LinearLayout> android:layout_height="260dp"
</androidx.cardview.widget.CardView> android:layout_marginEnd="2dp"
android:layout_weight="1"
app:cardCornerRadius="18dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="true">
<!-- Text in Outer Card --> <!-- Outer Card (White Background) -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:orientation="vertical"
android:orientation="horizontal" android:background="#8c1084"
android:gravity="center_vertical" android:padding="5dp">
android:layout_gravity="center">
<!-- Left Text: "Оплатите лицом" --> <!-- Inner Card (Purple Background) -->
<TextView <androidx.cardview.widget.CardView
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="170dp"
android:text="Оплатить\nлицом" app:cardCornerRadius="12dp"
android:textSize="18sp" android:layout_gravity="center"
android:paddingLeft="10dp" android:background="@android:color/holo_purple">
android:textStyle="normal"
android:textColor="@color/white"
android:layout_weight="1" />
<!-- Right Text: ">" --> <!-- Center the GIF Image -->
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<pl.droidsonroids.gif.GifImageView
android:layout_width="98dp"
android:layout_height="124dp"
android:src="@drawable/faceid" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Text in Outer Card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="20sp" android:layout_marginTop="10dp"
android:textStyle="bold" android:orientation="horizontal"
android:paddingRight="10dp" android:gravity="center_vertical"
android:text=">" android:layout_gravity="center">
android:textColor="@color/white" />
<!-- Left Text: "Оплатите лицом" -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Оплатить\nлицом"
android:textSize="18sp"
android:paddingLeft="10dp"
android:textStyle="normal"
android:textColor="@color/white"
android:layout_weight="1" />
<!-- Right Text: ">" -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
android:paddingRight="10dp"
android:text=">"
android:textColor="@color/white" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </androidx.cardview.widget.CardView>
</androidx.cardview.widget.CardView>
<!-- Right side - QR Code Card --> <!-- Right side - QR Code Card -->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_width="0dp" android:id="@+id/qrCodeContainer"
android:layout_height="260dp" android:layout_width="0dp"
android:layout_marginStart="2dp" android:layout_height="260dp"
android:layout_weight="1" android:layout_marginStart="2dp"
app:cardCornerRadius="18dp" android:layout_weight="1"
app:cardElevation="2dp" app:cardCornerRadius="18dp"
app:cardUseCompatPadding="true"> app:cardElevation="2dp"
app:cardUseCompatPadding="true">
<!-- Single Card with Purple Background --> <!-- Single Card with Purple Background -->
<LinearLayout <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#8c1084"
android:padding="3dp">
<!-- QR Code Image -->
<ImageView
android:id="@+id/qr_code_image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="170dp" android:layout_height="match_parent"
android:scaleType="fitCenter" android:orientation="vertical"
android:src="@drawable/am_qr_code_mag_black" android:background="#8c1084"
android:contentDescription="@string/qr_code_description" android:padding="3dp">
app:imageBitmap="@{vm.qrCodeBitmap}" />
<!-- Text Under QR Code --> <!-- QR Code Image -->
<TextView <ImageView
android:layout_width="wrap_content" android:id="@+id/qr_code_image"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_marginTop="10dp" android:layout_height="170dp"
android:text="Отсканируйте\nQR-код" android:scaleType="fitCenter"
android:textColor="@color/white" android:src="@drawable/am_qr_code_mag_black"
android:textSize="18sp" android:contentDescription="@string/qr_code_description"
android:textStyle="normal" app:imageBitmap="@{vm.qrCodeBitmap}" />
android:paddingBottom="5dp"
android:gravity="left" <!-- Text Under QR Code -->
android:layout_gravity="center" /> <TextView
</LinearLayout> android:layout_width="wrap_content"
</androidx.cardview.widget.CardView> android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Отсканируйте\nQR-код"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="normal"
android:paddingBottom="5dp"
android:gravity="left"
android:layout_gravity="center" />
</LinearLayout>
</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: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="37dp"
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="gone" />
</FrameLayout>
</FrameLayout>
</layout> </layout>