From 2315a32f56a786be71ec369eabb8c8fb99ac5011 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 10 Jun 2026 09:05:59 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20General=20=E7=8F=BE=E9=87=91=E5=BC=B7?= =?UTF-8?q?=E5=8C=96=E7=89=88=E5=90=88=E4=BD=B5=EF=BC=88Part=202-2?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移植 Luzui 現金 Dialog 的硬體容錯邏輯(卡鈔偵測+退鈔重試、零錢不足拒收+ 退款保障、暫存區兩階段入帳、找零失敗自癒、百元鈔庫存輪詢、3分鐘閒置逾時), 上報層改接 TaiwanStar 新 MQTT(移除 B600/B650,改 DispatchDialog 統一 Finalize)。 採「獨立類別 + flavor 路由」避免 duplicate class、不污染已上線 flavor: - 新增 src/General CashPayDialogGeneral(Luzui 強化版改名,只 General 編譯) + General layout override dialog_checkout_cash_pay.xml 補 buttonRetryEscrow - showDispatchDialog 改 MQTT 路徑、移除 callB600AndProceed/httpAPI - onCreate(Context) 移除 throws(對齊 TaiwanStar DialogAbstract 簽名) - DialogAbstract.cashDialogClass() 經 SaleFlowHandler 解析各 flavor 現金 Dialog - 三個 SaleFlowHandler 加 cashDialogClass():General→CashPayDialogGeneral、 晟崴/中國醫→標準 CashPayDialog - 3 個呼叫點(BuyDialog/InvoiceBarcodeDialog/InvoiceCustomerIdentifierDialog) case 9 改 changeDialog(cashDialogClass()) 三 flavor 編譯通過、General APK 打包成功。現金硬體容錯需實機驗證(階段 7)。 Co-Authored-By: Claude Opus 4.8 --- .../smartdevice/ui/SaleFlowHandler.java | 5 + .../smartdevice/ui/SaleFlowHandler.java | 5 + .../smartdevice/ui/SaleFlowHandler.java | 5 + .../ui/dialog/CashPayDialogGeneral.java | 1821 +++++++++++++++++ .../res/layout/dialog_checkout_cash_pay.xml | 168 ++ .../unibuy/smartdevice/DialogAbstract.java | 17 + .../smartdevice/ui/dialog/BuyDialog.java | 2 +- .../ui/dialog/InvoiceBarcodeDialog.java | 2 +- .../InvoiceCustomerIdentifierDialog.java | 2 +- 9 files changed, 2024 insertions(+), 3 deletions(-) create mode 100644 app/src/General/java/com/unibuy/smartdevice/ui/dialog/CashPayDialogGeneral.java create mode 100644 app/src/General/res/layout/dialog_checkout_cash_pay.xml diff --git a/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java index b8b1bd0..4faa0a6 100644 --- a/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java +++ b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java @@ -42,6 +42,11 @@ public class SaleFlowHandler { return false; } + /** 晟崴版使用標準現金 Dialog。 */ + public Class cashDialogClass() { + return com.unibuy.smartdevice.ui.dialog.CashPayDialog.class; + } + public void onBarcodeReceived(String rawData) { // 晟崴版不處理掃碼 } diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java index d26c191..acf7216 100644 --- a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java @@ -97,6 +97,11 @@ public class SaleFlowHandler { return false; } + /** 中國醫版使用標準現金 Dialog。 */ + public Class cashDialogClass() { + return com.unibuy.smartdevice.ui.dialog.CashPayDialog.class; + } + public void onBarcodeReceived(String rawData) { // 若是經由其他方式傳入的 Barcode 可在此處直接處理 try { diff --git a/app/src/General/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java b/app/src/General/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java index ce99997..59a5b10 100644 --- a/app/src/General/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java +++ b/app/src/General/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java @@ -50,6 +50,11 @@ public class SaleFlowHandler { return true; } + /** General 使用 Luzui 移植的現金強化版(卡鈔/找零保障/自癒),上報接 TaiwanStar 新 MQTT。 */ + public Class cashDialogClass() { + return com.unibuy.smartdevice.ui.dialog.CashPayDialogGeneral.class; + } + /** 購物車浮鈕:開啟多品項購物車 Fragment。 */ public void onShoppingCartRequested() { if (!(context instanceof FontendActivity)) { diff --git a/app/src/General/java/com/unibuy/smartdevice/ui/dialog/CashPayDialogGeneral.java b/app/src/General/java/com/unibuy/smartdevice/ui/dialog/CashPayDialogGeneral.java new file mode 100644 index 0000000..31ed431 --- /dev/null +++ b/app/src/General/java/com/unibuy/smartdevice/ui/dialog/CashPayDialogGeneral.java @@ -0,0 +1,1821 @@ +package com.unibuy.smartdevice.ui.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.InputType; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.unibuy.smartdevice.DialogAbstract; +import com.unibuy.smartdevice.MyApp; +import com.unibuy.smartdevice.R; +import com.unibuy.smartdevice.databinding.DialogCheckoutCashPayBinding; +import com.unibuy.smartdevice.exception.ErrorCode; +import com.unibuy.smartdevice.exception.LogsIOException; +import com.unibuy.smartdevice.structure.BuyStructure; +import com.unibuy.smartdevice.structure.ReportFlowInfoStructure; +import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure; +import com.unibuy.smartdevice.tools.HandlerMain; +import com.unibuy.smartdevice.tools.SimpleCashAcceptorHandler; +import com.unibuy.smartdevice.tools.ToastHandlerMain; +import com.unibuy.smartdevice.tools.Tools; +import com.unibuy.smartdevice.ui.FontendActivity; + + +import java.util.ArrayList; +import java.util.List; + +/** + * 現金支付對話框,處理現金支付相關的邏輯,包括: + * - 接收硬幣和紙鈔 + * - 計算找零 + * - 與 MDB 設備通訊 + * - 處理支付流程 + */ +public class CashPayDialogGeneral extends DialogAbstract { + + // 輔助類別,用來儲存找零指令資訊 + private static class ChangeCommand { + public final String command; + public final int denomination; + public final int count; + + public ChangeCommand(String command, int denomination, int count) { + this.command = command; + this.denomination = denomination; + this.count = count; + } + } + + // 新增:找零檢查結果類別 + private static class ChangeResult { + public final boolean canChange; + public final String reason; + + public ChangeResult(boolean canChange, String reason) { + this.canChange = canChange; + this.reason = reason; + } + } + + private DialogCheckoutCashPayBinding binding; + + int fourthValueDecimal; + private int inCash; + private int OrderAmount; + SimpleCashAcceptorHandler simpleCashAcceptorHandler; + private int coin50_count; + private int coin10_count; + private int coin5_count; + private int coin1_count; + private int note100_count; + + int redundantCash; + + int inCashSAVE; + + // 標記位:避免重複關閉串口 + private boolean isSerialPortClosed = false; + private final Object closeLock = new Object(); + + // B600失敗出貨 與正常出貨 + private boolean isDispatchDialogShown = false; + + private boolean isProcessingExit; + + // 標記位:設備狀態封包是否已收到(note100_count 才有效) + // 收到第一次 35-byte 狀態封包後設為 true,之前禁止投幣 + private boolean isDeviceReady = false; + private boolean isJammed = false; + + // 新增:待確認接收金額(暫估金額) + private int pendingAcceptanceAmount = 0; + + // 定時輪詢相關 + private Handler pollingHandler = new Handler(Looper.getMainLooper()); + private Runnable pollingRunnable; + private boolean isFastPolling = false; + + // 吸鈔超時檢測(防止滾輪卡鈔) + private Handler acceptTimeoutHandler = new Handler(Looper.getMainLooper()); + private Runnable acceptTimeoutRunnable; + + // 卡鈔超時定時器 + private Handler jamTimeoutHandler = new Handler(Looper.getMainLooper()); + private Runnable jamTimeoutRunnable; + + // 無操作逾時自動關閉(3 分鐘無投幣動作,防止 MDB 串口長時間佔用) + private static final long IDLE_TIMEOUT_MS = 3 * 60 * 1000L; // 3 分鐘 + private Handler idleTimeoutHandler = new Handler(Looper.getMainLooper()); + private Runnable idleTimeoutRunnable; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // 先呼叫父類別的 onCreate 方法,這會執行所有必要的初始化 + super.onCreate(savedInstanceState); + // 接著,覆寫父類別的設定,將對話框置中 + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams layoutParams = window.getAttributes(); + layoutParams.gravity = Gravity.CENTER; + window.setAttributes(layoutParams); + } + binding = DialogCheckoutCashPayBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + fourthValueDecimal = 0; + + OrderAmount = Integer.parseInt(String.valueOf(getTotalPrice())); + inCash = 0; + + // 顯示應付總金額 + binding.textPrice.setText("" + OrderAmount); + binding.textStatus.setText("已投入的金額:" + inCash); + binding.textStatus2.setText("未投入的金額:" + OrderAmount); + + // 實例化 Handler,並傳入事件監聽器 + simpleCashAcceptorHandler = new SimpleCashAcceptorHandler(getCtx(), + event -> new Handler(Looper.getMainLooper()).post(() -> handleMdbEvent(event))); + + // 啟動一個新執行緒來處理連線 + new Thread(() -> { + String result = simpleCashAcceptorHandler.connect(); + new Handler(Looper.getMainLooper()).post(() -> { + // Toast.makeText(getCtx(), result, Toast.LENGTH_LONG).show(); + if ("成功連接到 USB 轉接器!".equals(result)) { + + messageShow("設備初始化中,請稍候...", 10000); + + simpleCashAcceptorHandler.startReading(); + // 依序發送初始化指令 + // 先送 reset(0A)確保機器 Recycler 計數狀態同步,再執行完整初始化序列 + + sendMdbCommand("0A", simpleCashAcceptorHandler, 800, () -> { + sendMdbCommand("0CFFFFFFFF", simpleCashAcceptorHandler, 1000, () -> { + sendMdbCommand("34FFFFFFFF", simpleCashAcceptorHandler, 500, () -> { + sendMdbCommand("370100000002", simpleCashAcceptorHandler, 500, () -> { + sendMdbCommand("3703", simpleCashAcceptorHandler, 500, () -> { + sendMdbCommand("3704000003000000000000000000000000000000", simpleCashAcceptorHandler, + 1000, () -> { + sendMdbCommand("3705", simpleCashAcceptorHandler, 500, () -> { + sendMdbCommand("0A", simpleCashAcceptorHandler, 500, () -> { + getLogs().info("[初始化] 指令序列發送完畢。"); + if (!isDeviceReady) { + getLogs().info("[初始化] 設備尚未就緒,啟動快速輪詢狀態..."); + startPollingStatus(true); // 啟動快速輪詢 + } else { + startPollingStatus(false); // 啟動正常輪詢 + } + }); + }); + }); + }); + }); + }); + }); + }); + + } else { + messageShow(result, 3000); + cancel(); + } + }); + }).start(); + + // 取消按鈕 + binding.buttonCancel.setOnClickListener(v -> dialogCancel()); + + // 嘗試退鈔並重試按鈕 + binding.buttonRetryEscrow.setOnClickListener(v -> performRetryEscrow()); + + // 測試指令 按中間圖片 + // binding.testOne.setOnClickListener(v -> + // showMdbCommandDialog(simpleCashAcceptorHandler)); + } + + /** + * [MQTT Finalize 重構](自 Luzui 移植並改寫上報層) + * 顯示出貨對話框。原本先呼叫 B600/B650(reportFlowInfo/postInvoiceByGreen)再延遲 2 秒跳轉, + * 因 Race Condition 風險且不符 TaiwanStar 新 MQTT 架構,已廢棄。 + * 改為直接將現金支付資訊寫入 MyApp 全域狀態,再跳轉 DispatchDialog, + * 完整交易資料於所有出貨動作完成後,由 DispatchDialog 統一透過 MQTT Finalize 送出。 + */ + private void showDispatchDialog() { + if (getCtx() == null) return; + + // 填入現金支付資訊至全域狀態(供 TransactionFinalizeBuilder 使用) + ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData(); + flowInfo.setFlowType(9); // 現金支付 + flowInfo.setFlowSendInfo("收現金:" + inCashSAVE + "元/找零:" + redundantCash + "元"); + flowInfo.setFlowRequestInfo(""); // 現金無原始請求字串 + flowInfo.setOrderId(Tools.generateTimeBasedUUID()); + flowInfo.setTotalPrice(getTotalPrice()); + flowInfo.setFlowBarCode(""); // 現金交易無條碼 + flowInfo.setFlowStatusType(1); // 1 = 成功 + flowInfo.setGiveChange(redundantCash); + flowInfo.setUsePoints(0); + + // 現金支付無需等待後端確認,直接跳轉出貨(發票/結案由後續 MQTT Finalize 統一處理) + if (!isDispatchDialogShown) { + isDispatchDialogShown = true; + changeDialog(DispatchDialog.class); + } + } + /// //// + + // 接收設備的回應 + /** + * 處理 MDB 設備的事件回應 + * + * @param event 設備發送的原始事件字串 + */ + private void handleMdbEvent(String event) { + if (event != null) { + getLogs().info("[MDB事件接收] 原始事件: " + event); + } + + // 全局卡鈔檢測:只要訊息包含 30 05,即視為卡鈔 + if (event != null && event.contains("30 05")) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "偵測到紙鈔機卡鈔!(contains 30 05)"); + handleBillJam(); + return; + } + + if (event.length() >= 5) { + String code = event.substring(0, 5); + + // ─── 設備尚未就緒:退回所有投入的錢幣,不做任何入帳 ─── + if (!isDeviceReady) { + switch (code) { + case "08 53": // 50元硬幣 + sendMdbCommand("0D13", simpleCashAcceptorHandler); + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回50元硬幣"); + return; + case "08 52": // 10元硬幣 + sendMdbCommand("0D12", simpleCashAcceptorHandler); + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回10元硬幣"); + return; + case "08 51": // 5元硬幣 + sendMdbCommand("0D11", simpleCashAcceptorHandler); + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回5元硬幣"); + return; + case "08 50": // 1元硬幣(通常無法退,記錄即可) + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回1元硬幣"); + return; + case "30 90": // 100元鈔票(在暫存區,可退) + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回100元鈔票"); + return; + case "30 92": // 500元鈔票(在暫存區,可退) + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回500元鈔票"); + return; + case "30 93": // 1000元鈔票(在暫存區,可退) + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + messageShow("設備初始化中,請稍後再投幣。", 2000); + getLogs().info("[初始化] 設備尚未就緒,退回1000元鈔票"); + return; + default: + // 其他事件(如設備回應封包)繼續正常處理 + break; + } + } + // ───────────────────────────────────────────────────────── + + switch (code) { + + case "08 53": // 50 元 + getLogs().info("50元收到訊息:08 53 "); + resetIdleTimeout(); // 重設無操作逾時 + int potential_inCash_50 = inCash + 50; + int potential_redundantCash_50 = potential_inCash_50 - OrderAmount; + + sendMdbCommand("0A", simpleCashAcceptorHandler, 0, () -> { + }); + if (potential_inCash_50 < OrderAmount) { + // 還沒湊到商品價格,直接收,但要先檢查「退款保障」 + ChangeResult refundCheck = can_make_change(potential_inCash_50, false); + if (refundCheck.canChange) { + calculate_amount_update_UI(50); + } else { + messageShow("零錢不足,無法提供退款保障,請投入佰元鈔。" + refundCheck.reason, 3000); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("0D13", simpleCashAcceptorHandler), 2000); + getLogs().info("退款保障不足,拒收50元。 - " + refundCheck.reason); + } + } else { + ChangeResult result = can_make_change(potential_redundantCash_50, false); + if (result.canChange) { + // 湊到或超過價格,要先檢查能不能找零 + calculate_amount_update_UI(50); + } else { + // 找零不足,退幣 + messageShow("找零不足,無法接受50元硬幣。" + result.reason, 3000); + + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("0D13", simpleCashAcceptorHandler), 2000); + + getLogs().info("找零不足,無法接受50元硬幣。 退50塊 - " + result.reason); + } + } + break; + + case "08 52": // 10 元 + + getLogs().info("10元收到訊息:08 52 "); + resetIdleTimeout(); // 重設無操作逾時 + + int potential_inCash_10 = inCash + 10; + int potential_redundantCash_10 = potential_inCash_10 - OrderAmount; + sendMdbCommand("0A", simpleCashAcceptorHandler, 0, () -> { + }); + if (potential_inCash_10 < OrderAmount) { + // 還沒湊到商品價格,直接收,但要先檢查「退款保障」 + // 雖然 10 元很少,但為了邏輯一致性,我們統一在收錢前檢查 + ChangeResult refundCheck = can_make_change(potential_inCash_10, false); + if (refundCheck.canChange) { + calculate_amount_update_UI(10); + } else { + messageShow("零錢不足,無法提供退款保障,請投入佰元鈔。" + refundCheck.reason, 3000); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("0D12", simpleCashAcceptorHandler), 2000); + getLogs().info("退款保障不足,拒收10元。 - " + refundCheck.reason); + } + } else { + ChangeResult result = can_make_change(potential_redundantCash_10, false); + if (result.canChange) { + calculate_amount_update_UI(10); + } else { + messageShow("找零不足,無法接受10元硬幣。" + result.reason, 3000); + + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("0D12", simpleCashAcceptorHandler), 2000); + getLogs().info("找零不足,無法接受10元硬幣。 退10塊 - " + result.reason); + } + } + break; + + case "08 51": + getLogs().info("5元收到訊息:08 51 "); + resetIdleTimeout(); // 重設無操作逾時 + int potential_inCash_5 = inCash + 5; + int potential_redundantCash_5 = potential_inCash_5 - OrderAmount; + if (potential_inCash_5 < OrderAmount) { + // 還沒湊到商品價格,直接收,但要先檢查「退款保障」 + ChangeResult refundCheck = can_make_change(potential_inCash_5, false); + if (refundCheck.canChange) { + calculate_amount_update_UI(5); + } else { + messageShow("零錢不足,無法提供退款保障,請投入佰元鈔。" + refundCheck.reason, 3000); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("0D11", simpleCashAcceptorHandler), 2000); + getLogs().info("退款保障不足,拒收5元。 - " + refundCheck.reason); + } + } else { + ChangeResult result = can_make_change(potential_redundantCash_5, false); + if (result.canChange) { + // 湊到或超過價格,要先檢查能不能找零 + calculate_amount_update_UI(5); + } else { + // 找零不足,退幣 + messageShow("找零不足,無法接受5元硬幣。" + result.reason, 3000); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("0D11", simpleCashAcceptorHandler), 2000); + getLogs().info("找零不足,無法接受5元硬幣。 退五塊 - " + result.reason); + } + } + break; + + case "08 50": + getLogs().info("1元收到訊息:08 50 "); + calculate_amount_update_UI(1); + break; + + /// 50元 硬幣機滿了 掉進去內部儲存箱 + case "08 43": + getLogs().info("50元收到訊息:50元 硬幣機滿了 掉進去內部儲存箱"); + int potential_inCash_50_full = inCash + 50; + int potential_redundantCash_50_full = potential_inCash_50_full - OrderAmount; + if (potential_inCash_50_full <= OrderAmount) { + // 還沒湊到或剛好等於商品價格,直接收 + calculate_amount_update_UI(50); + } else { + ChangeResult result = can_make_change(potential_redundantCash_50_full, false); + if (result.canChange) { + // 超過價格,且可以找零 + calculate_amount_update_UI(50); + } else { + // 找零不足,但因為已經掉入內部儲存箱,無法退幣 + messageShow("找零不足,無法接受50元硬幣。" + result.reason, 3000); + sendMdbCommand("0D13", simpleCashAcceptorHandler); + getLogs().info("找零不足,無法接受50元硬幣。 - " + result.reason); + } + } + break; + /// 10元 硬幣機滿了 掉進去內部儲存箱 + case "08 42": + getLogs().info("10元收到訊息:10元 硬幣機滿了 掉進去內部儲存箱"); + int potential_inCash_10_full = inCash + 10; + int potential_redundantCash_10_full = potential_inCash_10_full - OrderAmount; + if (potential_inCash_10_full <= OrderAmount) { + calculate_amount_update_UI(10); + } else { + ChangeResult result = can_make_change(potential_redundantCash_10_full, false); + if (result.canChange) { + calculate_amount_update_UI(10); + } else { + messageShow("找零不足,無法接受10元硬幣。" + result.reason, 3000); + sendMdbCommand("0D12", simpleCashAcceptorHandler); + getLogs().info("找零不足,無法接受10元硬幣。 - " + result.reason); + } + } + break; + + /// 5元 硬幣機滿了 掉進去內部儲存箱 + case "08 41": + getLogs().info("5元收到訊息:5元 硬幣機滿了 掉進去內部儲存箱"); + int potential_inCash_5_full = inCash + 5; + int potential_redundantCash_5_full = potential_inCash_5_full - OrderAmount; + if (potential_inCash_5_full <= OrderAmount) { + calculate_amount_update_UI(5); + } else { + ChangeResult result = can_make_change(potential_redundantCash_5_full, false); + if (result.canChange) { + calculate_amount_update_UI(5); + } else { + messageShow("找零不足,無法接受5元硬幣。" + result.reason, 3000); + sendMdbCommand("0D11", simpleCashAcceptorHandler); + getLogs().info("找零不足,無法接受5元硬幣。 - " + result.reason); + } + } + break; + + /// 1元 硬幣機滿了 掉進去內部儲存箱 + case "08 40": + getLogs().info("1元收到訊息:1元 硬幣機滿了 掉進去內部儲存箱"); + calculate_amount_update_UI(1); + + break; + + case "30 90": // 100元 + getLogs().info("收到100元鈔票 訊息:30 90 "); + getLogs().info("收到100元鈔票 暫存區 先檢查是否足夠"); + resetIdleTimeout(); // 重設無操作逾時 + + // 計算如果接受這張之後會發生什麼 + int potential_inCash_100 = inCash + 100; + int changeNeeded100 = potential_inCash_100 - OrderAmount; // 正數表示需要找零 + + /// OrderAmount 是訂單金額 + /// changeNeeded100 要找零金額 + + // 如果會需要找零,先檢查能不能找零;不能就拒絕 + if (changeNeeded100 > 0) { + ChangeResult result = can_make_change(changeNeeded100, true); + if (!result.canChange) { + // 無法找零 -> 拒收 + messageShow("找零不足,無法接受100元鈔票。" + result.reason, 3000); + getLogs().info("找零不足,無法接受100元鈔票。 - " + result.reason); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + } else { + + getLogs().info("可以找零 -> 接受100進循環區"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3503", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 100; + getLogs().info("[暫估金額] 決定接收100元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 800); + } + } else { + + getLogs().info("不需要找零 -> 接受100進循環區A222"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3503", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 100; + getLogs().info("[暫估金額] 決定接收100元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 800); + } + break; + + /// 進去循環區 + case "30 80": // + getLogs().info("收到100元鈔票 30 80 真正進入循環區"); + if (pendingAcceptanceAmount >= 100) { + pendingAcceptanceAmount -= 100; + } else { + pendingAcceptanceAmount = 0; + } + cancelAcceptTimeoutCheck(); + getLogs().info("[暫估金額] 成功入箱,pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount); + calculate_amount_update_UI(100); + sendMdbCommand("3705", simpleCashAcceptorHandler, 0, () -> { // 刷新紙鈔機內容 + }); + break; + + /// 進去循環區 + case "30 B0": // + getLogs().info("收到100元鈔票 30 B0 真正進入循環區"); + if (pendingAcceptanceAmount >= 100) { + pendingAcceptanceAmount -= 100; + } else { + pendingAcceptanceAmount = 0; + } + cancelAcceptTimeoutCheck(); + getLogs().info("[暫估金額] 成功入箱,pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount); + calculate_amount_update_UI(100); + sendMdbCommand("3705", simpleCashAcceptorHandler, 0, () -> { // 刷新紙鈔機內容 + }); + break; + + case "30 92": // 500元 + int potential_inCash_500 = inCash + 500; + getLogs().info("收到500元鈔票 訊息30 92"); + resetIdleTimeout(); // 重設無操作逾時 + int changeNeeded500 = potential_inCash_500 - OrderAmount; + + if (changeNeeded500 > 0) { + // 會需要找零 → 先用 can_make_change() 判斷(已含 note100_count 檢查) + ChangeResult result = can_make_change(changeNeeded500, true); + if (!result.canChange) { + getLogs().info("找零不足,無法接受500元鈔票。 - " + result.reason); + messageShow("找零不足,無法接受500元鈔票。" + result.reason, 3000); + + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + } else { + getLogs().info("接受500元鈔票。進錢箱(循環區百鈔庫存: " + note100_count + "張)"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3501", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 500; + getLogs().info("[暫估金額] 決定接收500元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 1300); + } + } else { + // 不需要找零(還沒付清或剛好) + if (potential_inCash_500 < OrderAmount) { + // ✅ 尚未付清:檢查「退款保障」,確保萬一取消時退得出錢 + ChangeResult refundCheck = can_make_change(potential_inCash_500, true); + if (refundCheck.canChange) { + getLogs().info("收到500元鈔票,尚未付清但退款保障充足,準備進錢箱"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3501", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 500; + getLogs().info("[暫估金額] 決定接收500元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 1300); + } else { + getLogs().info("退款保障不足,拒收500元鈔票。 - " + refundCheck.reason); + messageShow("零錢不足,無法提供退款保障,請投入佰元鈔。" + refundCheck.reason, 3000); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + } + } else { + // ✅ 剛好付清:直接接受,準備出貨 + getLogs().info("收到500元鈔票,剛好付清,準備進錢箱出貨"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3501", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 500; + getLogs().info("[暫估金額] 決定接收500元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 1300); + } + } + break; + + /// 進去循環區 + case "30 82": // + getLogs().info("500元鈔票 30 82真正進入錢箱"); + if (pendingAcceptanceAmount >= 500) { + pendingAcceptanceAmount -= 500; + } else { + pendingAcceptanceAmount = 0; + } + cancelAcceptTimeoutCheck(); + getLogs().info("[暫估金額] 成功入箱,pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount); + calculate_amount_update_UI(500); + break; + + case "30 93": // 1000元 + int potential_inCash_1000 = inCash + 1000; + int changeNeeded1000 = potential_inCash_1000 - OrderAmount; + resetIdleTimeout(); // 重設無操作逾時 + + if (changeNeeded1000 > 0) { + // 會需要找零 → 先用 can_make_change() 判斷(已含 note100_count 檢查) + ChangeResult result = can_make_change(changeNeeded1000, true); + if (!result.canChange) { + messageShow("找零不足,無法接受1000元鈔票。" + result.reason, 3000); + Log.d("MDB_EVENT", "1000元找零庫存不足,拒收。 - " + result.reason); + getLogs().info("1000元找零庫存不足,拒收。 - " + result.reason); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + } else { + + getLogs().info("接受1000元鈔票。進錢箱(循環區百鈔庫存: " + note100_count + "張)"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3501", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 1000; + getLogs().info("[暫估金額] 決定接收1000元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 1300); + } + } else { + // 不需要找零(還沒付清或剛好) + if (potential_inCash_1000 < OrderAmount) { + // ✅ 尚未付清:檢查「退款保障」,確保萬一取消時退得出錢 + ChangeResult refundCheck = can_make_change(potential_inCash_1000, true); + if (refundCheck.canChange) { + getLogs().info("收到1000元鈔票,尚未付清但退款保障充足,準備進錢箱"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3501", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 1000; + getLogs().info("[暫估金額] 決定接收1000元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 1300); + } else { + getLogs().info("1000元找零庫存不足,拒收。 - " + refundCheck.reason); + messageShow("零錢不足,無法提供退款保障,請投入佰元鈔。" + refundCheck.reason, 3000); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> sendMdbCommand("3500", simpleCashAcceptorHandler), 500); + } + } else { + // ✅ 剛好付清:直接接受,準備出貨 + getLogs().info("收到1000元鈔票,剛好付清,準備進錢箱出貨"); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> { + sendMdbCommand("3501", simpleCashAcceptorHandler); + pendingAcceptanceAmount += 1000; + getLogs().info("[暫估金額] 決定接收1000元,pendingAcceptanceAmount增加至: " + pendingAcceptanceAmount); + startAcceptTimeoutCheck(); + }, 1300); + } + } + break; + + /// 進去循環區 + case "30 83": // + getLogs().info("收到1000元鈔票 30 83真正進入錢箱"); + if (pendingAcceptanceAmount >= 1000) { + pendingAcceptanceAmount -= 1000; + } else { + pendingAcceptanceAmount = 0; + } + cancelAcceptTimeoutCheck(); + getLogs().info("[暫估金額] 成功入箱,pendingAcceptanceAmount減少至: " + pendingAcceptanceAmount); + calculate_amount_update_UI(1000); + break; + + case "30 05": // 紙鈔機卡鈔 (Validator Jammed) + getLogs().error(ErrorCode.UNKNOWN_ERROR, "偵測到紙鈔機卡鈔!"); +// handleBillJam(); + break; + + } + + // 處理狀態封包 (VMC_52_CHECK_VMC_STATUS) + // 正常 ASCII 封包長度預期為 40 (含標頭與校驗) + int eventLen = event != null ? event.trim().split(" ").length : 0; + if (event != null && (eventLen == 35 || eventLen == 40)) { + String[] parts = event.trim().split(" "); + boolean isValidFormat = true; + for (String part : parts) { + if (part.length() != 2) { + isValidFormat = false; + break; + } + } + if (isValidFormat) { + try { + // 根據封包長度判斷數據起始位置 + // 長度 40: FA FB 52 23 [DATA...] XOR -> DATA[0] 在 parts[4] + // 長度 35: [DATA...] -> DATA[0] 在 parts[0] + int dataStartIndex = (eventLen == 40) ? 4 : 0; + // 根據 CashMachineSettings,佰元鈔庫存是在 vmcData 的第 4 個位元組 (index 3) + int noteIndex = dataStartIndex + 3; + + if (noteIndex < parts.length) { + String noteValueHex = parts[noteIndex]; + // 根據 CashMachineSettings 邏輯,直接使用 Hex 解析 (Integer.parseInt(..., 16)) + int val = Integer.parseInt(noteValueHex, 16); + + // 暫存區(Recycler)物理上限為 30 張百元鈔 + if (val >= 0 && val <= 30) { + note100_count = val; + getLogs().info("循環區百鈔庫存更新: " + note100_count + "張(hex: " + noteValueHex + ",來源索引: " + noteIndex + ")"); + } else { + Log.w("MDB_EVENT", "循環區百鈔數值異常: " + val + ",保留上次庫存: " + note100_count); + } + } + // ✅ 收到第一次有效狀態封包,標記設備已就緒 + if (!isDeviceReady) { + isDeviceReady = true; + getLogs().info("[初始化] 設備狀態封包已收到,開放投幣(循環區百鈔: " + note100_count + "張)"); + + // 如果正在快速輪詢,切換回正常輪詢 + if (isFastPolling) { + startPollingStatus(false); + } + + new Handler(Looper.getMainLooper()).post(() -> { + messageShow("設備就緒,請投入紙鈔或硬幣。", 3000); + // 設備就緒後啟動無操作逾時計時器(3 分鐘無投幣自動關閉) + startIdleTimeout(); + }); + + // 觸發自癒退款檢查 + checkAndProcessPendingRefund(); + } + return; + } catch (NumberFormatException e) { + Log.e("MDB_EVENT", "十六進制轉換失敗,不處理此事件: " + e.getMessage()); + } + } + } + + if (event != null && event.trim().split(" ").length == 19) { + String[] parts = event.trim().split(" "); + boolean isValidFormat = true; + for (String part : parts) { + if (part.length() != 2) { + isValidFormat = false; + break; + } + } + if (isValidFormat) { + try { + int oneYuanCount = Integer.parseInt(parts[2], 16); + int fiveYuanCount = Integer.parseInt(parts[3], 16); + int tenYuanCount = Integer.parseInt(parts[4], 16); + int fiftyYuanCount = Integer.parseInt(parts[5], 16); + coin1_count = oneYuanCount; + coin5_count = fiveYuanCount; + coin10_count = tenYuanCount; + coin50_count = fiftyYuanCount; + String message = String.format( + "硬幣機庫存已更新:1元=%d, 5元=%d, 10元=%d, 50元=%d", + coin1_count, coin5_count, coin10_count, coin50_count); + // Toast.makeText(getCtx(), message, Toast.LENGTH_LONG).show(); + getLogs().info("[硬幣庫存] " + message); + return; + } catch (NumberFormatException e) { + Log.e("MDB_EVENT", "十六進制轉換失敗,不處理此事件: " + e.getMessage()); + } + } + } + } + Log.d("MDB_EVENT", "收到事件: " + event); + // Toast.makeText(getCtx(), "收到事件: " + event, Toast.LENGTH_LONG).show(); + } + + /** + * 檢查是否有足夠的零錢可以找 + * + * @param changeAmount 需要找零的金額 + * @return 是否可以找零 + */ + // private boolean can_make_change(int changeAmount) { + // + // int note100_temp = note100_count; + // int coin50_temp = coin50_count; + // int coin10_temp = coin10_count; + // int coin5_temp = coin5_count; + // int coin1_temp = coin1_count; + // + // if (changeAmount >= 100 && note100_temp > 0) { + // int hundredsToGive = changeAmount / 100; + // int actualGive = Math.min(hundredsToGive, note100_temp); + // changeAmount -= actualGive * 100; + // note100_temp -= actualGive; + // } + // if (changeAmount >= 50 && coin50_temp > 0) { + // int fiftiesToGive = changeAmount / 50; + // int actualGive = Math.min(fiftiesToGive, coin50_temp); + // changeAmount -= actualGive * 50; + // coin50_temp -= actualGive; + // } + // if (changeAmount >= 10 && coin10_temp > 0) { + // int tensToGive = changeAmount / 10; + // int actualGive = Math.min(tensToGive, coin10_temp); + // changeAmount -= actualGive * 10; + // coin10_temp -= actualGive; + // } + // if (changeAmount >= 5 && coin5_temp > 0) { + // int fivesToGive = changeAmount / 5; + // int actualGive = Math.min(fivesToGive, coin5_temp); + // changeAmount -= actualGive * 5; + // coin5_temp -= actualGive; + // } + // if (changeAmount > 0 && coin1_temp > 0) { + // int onesToGive = changeAmount / 1; + // int actualGive = Math.min(onesToGive, coin1_temp); + // changeAmount -= actualGive * 1; + // coin1_temp -= actualGive; + // } + // return changeAmount == 0; + // } + + private ChangeResult can_make_change(int changeAmount, boolean isBanknote) { + int originalChangeAmount = changeAmount; + int note100_temp = note100_count; + int coin50_temp = coin50_count; + int coin10_temp = coin10_count; + int coin5_temp = coin5_count; + int coin1_temp = coin1_count; + + // 1. 優先處理百鈔(如果是紙鈔退款,必須有足夠百鈔處理 100 元以上的部分) + if (changeAmount >= 100 && note100_temp > 0) { + int hundredsToGive = changeAmount / 100; + int actualGive = Math.min(hundredsToGive, note100_temp); + changeAmount -= actualGive * 100; + note100_temp -= actualGive; + } + + // ✅ 新增:紙鈔退款保障檢查。如果 100 元面額部分無法用百鈔支付,則失敗 + if (isBanknote && changeAmount >= 100) { + return new ChangeResult(false, String.format("佰元鈔庫存不足(僅有%d張),無法提供紙鈔退款保障", note100_count)); + } + + // 2. 處理 50 元硬幣(如果是紙鈔退款,禁止使用 50 元硬幣) + if (!isBanknote && changeAmount >= 50 && coin50_temp > 0) { + int fiftiesToGive = changeAmount / 50; + int actualGive = Math.min(fiftiesToGive, coin50_temp); + changeAmount -= actualGive * 50; + coin50_temp -= actualGive; + } else if (!isBanknote && changeAmount >= 50) { + return new ChangeResult(false, String.format("需要%d個50元硬幣,但庫存不足(僅有%d個)", + (changeAmount / 50), coin50_temp)); + } else if (isBanknote && changeAmount >= 50) { + // 紙鈔退款雖然不能用 50 元,但如果後續 10 元硬幣足夠,仍可繼續檢查 + // 此處不做 return false,讓 10 元硬幣邏輯去處理 + getLogs().debug("紙鈔退款跳過 50 元硬幣檢查,改用 10 元硬幣嘗試"); + } + + if (changeAmount >= 10 && coin10_temp > 0) { + int tensToGive = changeAmount / 10; + int actualGive = Math.min(tensToGive, coin10_temp); + changeAmount -= actualGive * 10; + coin10_temp -= actualGive; + } else if (changeAmount >= 10) { + return new ChangeResult(false, String.format("需要%d個10元硬幣,但庫存不足(僅有%d個)", + (changeAmount / 10), coin10_temp)); + } + + if (changeAmount >= 5 && coin5_temp > 0) { + int fivesToGive = changeAmount / 5; + int actualGive = Math.min(fivesToGive, coin5_temp); + changeAmount -= actualGive * 5; + coin5_temp -= actualGive; + } else if (changeAmount >= 5) { + return new ChangeResult(false, String.format("需要%d個5元硬幣,但庫存不足(僅有%d個)", + (changeAmount / 5), coin5_temp)); + } + + if (changeAmount > 0 && coin1_temp > 0) { + int onesToGive = changeAmount / 1; + int actualGive = Math.min(onesToGive, coin1_temp); + changeAmount -= actualGive * 1; + coin1_temp -= actualGive; + } else if (changeAmount > 0) { + return new ChangeResult(false, String.format("需要%d個1元硬幣,但庫存不足(僅有%d個,還差%d元)", + changeAmount, coin1_temp, changeAmount)); + } + + if (changeAmount != 0) { + return new ChangeResult(false, String.format("無法完全找零,還差%d元", changeAmount)); + } + + return new ChangeResult(true, ""); + } + + /** + * 計算並更新 UI 顯示的金額 進行B600以及出貨 + * + * @param amount 投入的金額 + */ + private void calculate_amount_update_UI(int amount) { + + inCash += amount; + int remaining = OrderAmount - inCash; // 未投入的錢 + binding.textStatus.setText("已投入的金額: " + inCash); + binding.textStatus2.setText("未投入的金額: " + remaining); + + if (inCash >= OrderAmount) { + + redundantCash = inCash - OrderAmount; // 要找的錢 + inCashSAVE = inCash; + binding.textStatus2.setText("付款完成!即將找零:" + redundantCash + "元,並準備出貨"); + + binding.buttonCancel.setVisibility(View.GONE); + + // 修改這裡:傳入一個回調函數,在找零完成後執行,並允許百鈔退幣 (allowBanknote=true) + performChange(redundantCash, true, () -> { + // 這個回調會在找零完成後執行 + + // 顯示出貨畫面進行B600 602 + showDispatchDialog(); + // 使用 父類的取消畫面 + cancel(); + + }); + + inCash = 0; + } + + } + + /// ///// 找零方法 有三個 //////////// ///// 找零方法 有三個 ///////// + + /** + * 執行找零操作,支援 allowBanknote 以在紙鈔機卡鈔時強制進行純硬幣找零 + * + * @param changeAmount 需要找零的金額 + * @param allowBanknote 是否允許吐百元鈔 + * @param onComplete 找零完成後的回調 + */ + private void performChange(int changeAmount, boolean allowBanknote, Runnable onComplete) { + + int remainingChange = changeAmount; // 剩餘要找的錢 + + // ----------------------------- + // 100元紙鈔 - 先檢查是否需要吐鈔 + // ----------------------------- + int hundredsToGive = 0; + if (allowBanknote) { + hundredsToGive = Math.min(remainingChange / 100, note100_count); + } + + if (hundredsToGive > 0) { + // ✅ 關鍵修正:先執行紙鈔機激活準備(按測試頁面的精確延遲時間) + getLogs().info("[現金支付] 需要吐百鈔,先激活紙鈔機"); + + // 按照測試頁面的延遲時間:0ms, 1000ms, 600ms, 1000ms + final int finalHundredsToGive = hundredsToGive; + sendMdbCommand("3400000000", simpleCashAcceptorHandler, 0, () -> { + sendMdbCommand("370100000002", simpleCashAcceptorHandler, 1000, () -> { + sendMdbCommand("3703", simpleCashAcceptorHandler, 600, () -> { + sendMdbCommand("3704000003000000000000000000000000000000", simpleCashAcceptorHandler, 1000, + () -> { + getLogs().info("[現金支付] 紙鈔機激活完成,開始生成找零指令"); + + // 紙鈔機準備完成後,生成全部找零指令 + List commands = new ArrayList<>(); + int remaining = changeAmount; + + // Add coin change commands first + addOtherChangeCommands(commands, remaining - (finalHundredsToGive * 100)); + + // Add banknote change command last + String command = "370600000" + String.format("%01X", finalHundredsToGive); + getLogs().info("[現金支付] 準備吐出 100元紙鈔 x " + finalHundredsToGive + " 張,指令: " + command); + commands.add(new ChangeCommand(command, 100, finalHundredsToGive)); + + // 執行找零指令 + if (!commands.isEmpty()) { + executeChangeCommands(commands, 0, 3500, onComplete); + } else if (onComplete != null) { + onComplete.run(); + } + }); + }); + }); + }); + return; // 重要:提前返回,不繼續往下執行 + } + + // 如果不需要吐百鈔,直接生成硬幣找零指令 + List commands = new ArrayList<>(); + addOtherChangeCommands(commands, remainingChange); + + if (!commands.isEmpty()) { + executeChangeCommands(commands, 0, 3500, onComplete); + } else if (onComplete != null) { + onComplete.run(); + } + } + + // 新增輔助方法:加入硬幣找零指令 + /** + * 添加硬幣找零指令到指令列表 + * + * @param commands 指令列表 + * @param remainingChange 剩餘需要找零的金額 + */ + private void addOtherChangeCommands(List commands, int remainingChange) { + + // ----------------------------- + // 50元硬幣 + // ----------------------------- + int fiftiesToGive = Math.min(remainingChange / 50, coin50_count); + if (fiftiesToGive > 0) { + String command = "0D" + String.format("%01X", fiftiesToGive) + "3"; + commands.add(new ChangeCommand(command, 50, fiftiesToGive)); + remainingChange -= fiftiesToGive * 50; + } + + // ----------------------------- + // 10元硬幣 + // ----------------------------- + int tensToGive = Math.min(remainingChange / 10, coin10_count); + if (tensToGive > 0) { + String command = "0D" + String.format("%01X", tensToGive) + "2"; + commands.add(new ChangeCommand(command, 10, tensToGive)); + remainingChange -= tensToGive * 10; + } + + // ----------------------------- + // 5元硬幣 + // ----------------------------- + int fivesToGive = Math.min(remainingChange / 5, coin5_count); + if (fivesToGive > 0) { + String command = "0D" + String.format("%01X", fivesToGive) + "1"; + commands.add(new ChangeCommand(command, 5, fivesToGive)); + remainingChange -= fivesToGive * 5; + } + + // ----------------------------- + // 1元硬幣 + // ----------------------------- + int onesToGive = Math.min(remainingChange, coin1_count); + if (onesToGive > 0) { + String command = "0D" + String.format("%01X", onesToGive) + "0"; + commands.add(new ChangeCommand(command, 1, onesToGive)); + remainingChange -= onesToGive; + } + + // ----------------------------- + // 如果剩餘金額還有 > 0,表示找零不足 + // ----------------------------- + if (remainingChange > 0) { + getLogs().info("[現金支付] 警告:找零不足,還差 " + remainingChange + "元"); + messageShow("找零不足,無法完成找零。請洽服務人員處理。", 3000); + savePendingRefund(remainingChange); // 儲存未退還餘額,用於自癒 + } + } + + /** + * 依序執行找零指令 + * + * @param commands 找零指令列表 + * @param index 當前執行的指令索引 + * @param onComplete 所有指令執行完成後的回調 + */ + private void executeChangeCommands(List commands, int index, int nextDelay, Runnable onComplete) { + // Check recursion termination condition + if (index >= commands.size()) { + // 延遲 nextDelay 毫秒(確保最後一個退幣指令物理吐幣完畢),再執行關閉與完成回調 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + sendMdbCommand("3400000000", simpleCashAcceptorHandler); + if (onComplete != null) { + onComplete.run(); + } + }, nextDelay); + return; + } + + final ChangeCommand currentCommand = commands.get(index); + + // Send command sequentially with delay + new Handler(Looper.getMainLooper()).postDelayed(() -> { + + if (currentCommand != null && simpleCashAcceptorHandler != null) { + + getLogs().info("[\u627e\u96f6\u6307\u4ee4\u767c\u9001] \u6307\u4ee4: " + currentCommand.command + "\uff0c\u9762\u984d: " + currentCommand.denomination + "\uff0c\u5f35\u6578/\u500b\u6578: " + currentCommand.count); + + // 使用帶有 Callback 版本的 sendMdbCommand 以支援寫入狀態檢查(斷線偵測) + sendMdbCommand(currentCommand.command, simpleCashAcceptorHandler, success -> { + if (success) { + messageShow("\u5410\u5e63\u4e2d\u8acb\u7a0d\u5019", 1000); + + // Update inventory count based on denomination + switch (currentCommand.denomination) { + case 50: + coin50_count -= currentCommand.count; + break; + case 10: + coin10_count -= currentCommand.count; + break; + case 5: + coin5_count -= currentCommand.count; + break; + case 1: + coin1_count -= currentCommand.count; + break; + case 100: + note100_count -= currentCommand.count; + break; + } + + int delay = 3500; + if (currentCommand.denomination == 100) { + // Banknote dynamic delay: 2.5s per sheet + 3s buffer + delay = (currentCommand.count * 2500) + 3000; + } + executeChangeCommands(commands, index + 1, delay, onComplete); + } else { + // 寫入失敗!說明 MDB 斷線,設備無法通訊 + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[找零失敗] 指令寫入失敗,硬體可能中斷通訊。指令: " + currentCommand.command); + + // 計算剩餘未退幣的金額 + int unrefunded = 0; + for (int i = index; i < commands.size(); i++) { + ChangeCommand cmd = commands.get(i); + unrefunded += cmd.denomination * cmd.count; + } + + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[找零失敗] 本次找零未成功退還金額: " + unrefunded + " 元"); + savePendingRefund(unrefunded); + + messageShow("設備通訊中斷,找零退款失敗!\n未退還金額: " + unrefunded + " 元,請洽服務人員處理。", 10000); + completeCancelProcess(); + } + }); + } else { + executeChangeCommands(commands, index + 1, 0, onComplete); + } + }, nextDelay); // + } + + /// ///// 找零方法 有三個 //////////// ///// 找零方法 有三個 ///////// + + // 下位機溝通用 + public byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + // 安全關閉串口的核心方法(帶延遲與回調參數) + /** + * 安全關閉串口(帶延遲與回調參數) + * + * @param delayBeforeCloseMs 關閉前的延遲時間(毫秒) + * @param onComplete 串口關閉完成後的回調 + */ + private void safeCloseSerialPort(int delayBeforeCloseMs, Runnable onComplete) { + synchronized (closeLock) { + if (isSerialPortClosed) { + getLogs().info("[CashPayDialogGeneral] 串口已經關閉過了,跳過重複關閉"); + if (onComplete != null) { + onComplete.run(); + } + return; + } + isSerialPortClosed = true; + getLogs().info("[CashPayDialogGeneral] 開始安全關閉串口,延遲: " + delayBeforeCloseMs + "ms"); + } + + if (simpleCashAcceptorHandler != null) { + // 使用 Handler 確保即使對話框關閉也能執行 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + new Thread(() -> { + try { + // 停止接收紙幣和硬幣 + getLogs().info("[CashPayDialogGeneral] 發送停止接收紙幣硬幣指令"); + sendMdbCommand("0C00000000", simpleCashAcceptorHandler); + Thread.sleep(1000); + + // 停止找零 + getLogs().info("[CashPayDialogGeneral] 發送停止找零指令"); + sendMdbCommand("3400000000", simpleCashAcceptorHandler); + Thread.sleep(1000); + + // 關閉連線 + getLogs().info("[CashPayDialogGeneral] 關閉串口連線"); + simpleCashAcceptorHandler.close(); + + stopPollingStatus(); + + getLogs().info("[CashPayDialogGeneral] 串口關閉完成"); + } catch (Exception e) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[CashPayDialogGeneral] 關閉串口時發生錯誤: " + e.getMessage()); + } finally { + if (onComplete != null) { + new Handler(Looper.getMainLooper()).post(onComplete); + } + } + }).start(); + }, delayBeforeCloseMs); + } else { + if (onComplete != null) { + onComplete.run(); + } + } + } + + private void safeCloseSerialPort(int delayBeforeCloseMs) { + safeCloseSerialPort(delayBeforeCloseMs, null); + } + + /** + * 啟動吸鈔超時檢測定時器(防止滾輪卡鈔,因為滾輪卡鈔時只會一直報 30 03 忙碌,而沒有 30 05) + */ + private void startAcceptTimeoutCheck() { + cancelAcceptTimeoutCheck(); // 確保先清除舊的 + + acceptTimeoutRunnable = () -> { + // 如果 6 秒後,待確認金額仍大於 0(紙鈔仍未成功入箱/入循環區) + if (pendingAcceptanceAmount > 0 && !isJammed) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[卡鈔判定] 紙鈔吸入超時(已過 6 秒仍未成功入箱,且持續忙碌中),判定為滾輪卡鈔!"); + handleBillJam(); + } + }; + acceptTimeoutHandler.postDelayed(acceptTimeoutRunnable, 6000); + } + + /** + * 取消吸鈔超時檢測定時器 + */ + private void cancelAcceptTimeoutCheck() { + if (acceptTimeoutRunnable != null) { + acceptTimeoutHandler.removeCallbacks(acceptTimeoutRunnable); + acceptTimeoutRunnable = null; + } + } + + /** + * 取消卡鈔超時定時器,避免幽靈取消 Bug + */ + private void cancelJamTimeout() { + if (jamTimeoutRunnable != null) { + jamTimeoutHandler.removeCallbacks(jamTimeoutRunnable); + jamTimeoutRunnable = null; + } + } + + /** + * 消費者手動觸發嘗試退鈔並重新啟用連線 + */ + private void performRetryEscrow() { + getLogs().info("[手動退鈔重試] 消費者點擊嘗試退鈔並重試"); + cancelJamTimeout(); + + new Handler(Looper.getMainLooper()).post(() -> { + if (binding != null && binding.buttonRetryEscrow != null) { + binding.buttonRetryEscrow.setVisibility(View.GONE); + } + messageShow("嘗試退鈔中,請稍後...", 5000); + }); + + getLogs().info("[手動退鈔重試] 發送 3500 退鈔指令"); + sendMdbCommand("3500", simpleCashAcceptorHandler); + + // 延遲 4 秒讓物理吐鈔動作執行完畢後,重新啟用並連線紙鈔機 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (!isShowing()) { + getLogs().info("[手動退鈔重試] 對話框已關閉,放棄重啟連線"); + return; + } + getLogs().info("[手動退鈔重試] 退鈔完畢,重新啟用並連線紙鈔機"); + + // 重新發送啟用與初始化指令序列 + sendMdbCommand("3704000003000000000000000000000000000000", simpleCashAcceptorHandler, 200, () -> { + sendMdbCommand("3705", simpleCashAcceptorHandler, 200, () -> { + sendMdbCommand("0A", simpleCashAcceptorHandler, 200, () -> { + getLogs().info("[手動退鈔重試] 初始化指令發送完畢,重置暫估金額,重啟輪詢"); + + pendingAcceptanceAmount = 0; // 暫估清零(因已退鈔) + isJammed = false; // 重設 Jammed 狀態為 false,以利自癒判定與再次卡鈔時的偵測 + startPollingStatus(false); + + // 延遲 1.5 秒確認狀態 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (!isShowing()) { + getLogs().info("[手動退鈔重試] 對話框已關閉,放棄狀態確認"); + return; + } + if (!isJammed) { + // 代表故障已排除,恢復現金支付啟用狀態 + try { + Context ctx = getContext(); + if (ctx != null) { + android.content.SharedPreferences sp = ctx.getSharedPreferences("MyShoppingCartSettings", Context.MODE_PRIVATE); + sp.edit().putBoolean("isCashModuleEnabled", true).apply(); + getLogs().info("[手動退鈔重試] 卡鈔自癒成功,現金支付模組重新啟用 (isCashModuleEnabled = true)"); + } + } catch (Exception ex) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "重新啟用現金模組異常: " + ex.getMessage()); + } + + new Handler(Looper.getMainLooper()).post(() -> { + if (!isShowing()) return; + if (binding != null) { + if (binding.buttonCancel != null) { + binding.buttonCancel.setText("取消並退幣"); + binding.buttonCancel.setVisibility(View.VISIBLE); // 重新顯示取消按鈕 + } + if (binding.buttonRetryEscrow != null) { + binding.buttonRetryEscrow.setVisibility(View.GONE); + } + } + messageShow("請拿回退出的紙鈔,可繼續交易。", 5000); + isJammed = false; // 重設 Jammed 狀態 + }); + } else { + // 重新啟用後如果依然卡鈔,Poll 會再次回報 30 05 並觸發 handleBillJam,那裡會處理 UI + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[手動退鈔重試] 重連後依然偵測為卡鈔狀態"); + } + }, 1500); + }); + }); + }); + }, 4000); + } + + /// //////// 取消交易 找零 有二個 //////// + /** + * 對話框取消按鈕點擊處理 + */ + public void dialogCancel() { + if (this.isShowing()) { + cancelAcceptTimeoutCheck(); + // 如果卡鈔中取消,立刻將現金模組設為停用 + if (isJammed) { + try { + Context ctx = getContext(); + if (ctx != null) { + android.content.SharedPreferences sp = ctx.getSharedPreferences("MyShoppingCartSettings", Context.MODE_PRIVATE); + sp.edit().putBoolean("isCashModuleEnabled", false).apply(); + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[取消交易] 卡鈔中取消交易,已停用現金模組 (isCashModuleEnabled = false)"); + } + } catch (Exception e) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "停用現金模組異常: " + e.getMessage()); + } + } + + int totalRefund = isJammed ? inCash : (inCash + pendingAcceptanceAmount); + int pendingLoss = isJammed ? pendingAcceptanceAmount : 0; + + if (totalRefund > 0) { + if (isJammed && pendingLoss > 0) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "卡鈔取消交易:已入箱 " + inCash + " 元以硬幣退還,卡在暫存區之 " + pendingLoss + " 元無法退出,由人工排除"); + messageShow("設備異常已中止!\n已退還入箱金額 " + inCash + " 元,卡住之 " + pendingLoss + " 元請洽服務人員處理。", 8000); + } else { + getLogs().info("取消交易,退還 " + totalRefund + "元 (inCash: " + inCash + ", pendingAcceptanceAmount: " + pendingAcceptanceAmount + ")"); + messageShow("取消交易,退還 " + totalRefund + " 元", 3000); + } + + // 執行退幣,若已卡鈔,只找硬幣 + performChange(totalRefund, !isJammed, () -> { + completeCancelProcess(); + }); + + // 重置金額 + inCash = 0; + pendingAcceptanceAmount = 0; + } else { + if (isJammed && pendingLoss > 0) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "卡鈔取消交易:無已入箱金額,卡在暫存區之 " + pendingLoss + " 元無法退出,由人工排除"); + messageShow("設備異常已中止!\n卡住之 " + pendingLoss + " 元請洽服務人員處理。", 8000); + + new Handler(Looper.getMainLooper()).postDelayed(() -> { + completeCancelProcess(); + }, 500); + } else { + getLogs().info("沒有現金需要退還 先吐暫存區3500再關關閉串口"); + new Handler(Looper.getMainLooper()).postDelayed(() -> { + sendMdbCommand("3500", simpleCashAcceptorHandler); + getLogs().info("...已發送退鈔指令3500"); + + new Handler(Looper.getMainLooper()).postDelayed(() -> { + getLogs().info("aa執行completeCancelProcess"); + completeCancelProcess(); + }, 500); + }, 100); + } + + // 重置金額 + inCash = 0; + pendingAcceptanceAmount = 0; + } + } + } + + /** + * 取消交易流程,包括退幣和清理資源 + */ + private void completeCancelProcess() { + getLogs().info("[CashPayDialogGeneral] 執行 completeCancelProcess"); + + // 清除優惠代碼 and 購物車 + MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, "")); + getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), ""); + MyApp.getInstance().getBuyList().clear(); + + // 提示使用者正在清理連線 + if (!isJammed) { + messageShow("交易已取消,清理硬體連線中,請稍後...", 4000); + } + + safeCloseSerialPort(3000, () -> { + // 確保串口安全釋放後,才真正關閉對話框 + cancel(); + }); + } + + /** + * 處理紙鈔機卡鈔異常狀態 + * 立即停用硬體、隱藏取消按鈕、提示並暫停 30 秒以利警示,隨後自動重置退回待機頁面 + */ + private void handleBillJam() { + if (isJammed) return; + isJammed = true; + + getLogs().info("[卡鈔] 偵測到卡鈔,啟動手動退鈔重試畫面"); + + // 1. 停止狀態輪詢,防範干擾 + stopPollingStatus(); + + // 2. 更新 UI:顯示重試按鈕、將取消按鈕字樣改為「取消交易」 + new Handler(Looper.getMainLooper()).post(() -> { + if (binding != null) { + if (binding.buttonRetryEscrow != null) { + binding.buttonRetryEscrow.setVisibility(View.VISIBLE); + } + if (binding.buttonCancel != null) { + binding.buttonCancel.setVisibility(View.GONE); // 卡鈔時隱藏取消交易按鈕 + } + } + // 3. 彈出提示視窗 + messageShow("設備異常:紙鈔通道卡鈔!\n請點選「嘗試退鈔並重試」", 30000); + }); + + // 4. 設定 60 秒超時保護,若消費者無操作則自動執行取消交易 + cancelJamTimeout(); // 先清除舊的,防範幽靈定時器 + jamTimeoutRunnable = () -> { + if (isJammed && isShowing()) { + getLogs().info("[卡鈔超時] 超時 60 秒無操作,自動取消交易"); + dialogCancel(); + } + }; + jamTimeoutHandler.postDelayed(jamTimeoutRunnable, 60000); + } + + /// //////// 取消交易 找零 有二個 //////// + + // 新增:MDB 指令發送回調介面 + private interface MdbCommandCallback { + void onResult(boolean success); + } + + // 新增:發送 MDB 任務,支援回調以檢測寫入是否成功(斷線檢測) + private void sendMdbCommand(final String hexCommand, final SimpleCashAcceptorHandler handler, final MdbCommandCallback callback) { + new Thread(() -> { + boolean success = false; + try { + byte[] command = hexStringToByteArray(hexCommand); + int bytesWritten = handler.writeData(command); + if (bytesWritten > 0) { + getLogs().info("[MDB] 指令發送成功: " + hexCommand); + success = true; + } else { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[MDB] 指令發送失敗: " + hexCommand + " (回傳碼: " + bytesWritten + ")"); + } + } catch (Exception e) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "[MDB] 指令發送異常: " + e.getMessage()); + } + if (callback != null) { + final boolean finalSuccess = success; + new Handler(Looper.getMainLooper()).post(() -> callback.onResult(finalSuccess)); + } + }).start(); + } + + /** + * 發送MDB任務 + * + */ + private void sendMdbCommand(final String hexCommand, final SimpleCashAcceptorHandler handler) { + sendMdbCommand(hexCommand, handler, null); + } + + /** + * 發送MDB任務 參數多兩個 delay跟 Runnable + * + */ + private void sendMdbCommand(String command, SimpleCashAcceptorHandler handler, int delay, Runnable onComplete) { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + sendMdbCommand(command, handler); + if (onComplete != null) { + onComplete.run(); + } + }, delay); + } + + /// Cancel時會進入安全關閉 + @Override + public void dismiss() { + cancelAcceptTimeoutCheck(); + cancelJamTimeout(); + cancelIdleTimeout(); // 取消無操作逾時計時器 + getLogs().info("[CashPayDialogGeneral] dismiss() 被調用,確保串口安全關閉"); + // 異常關閉場景:給予 3 秒緩衝時間,以防正在找零 + safeCloseSerialPort(3000); + stopPollingStatus(); + super.dismiss(); + } + + /// Activity 進背景時 安全關閉 + @Override + protected void onStop() { + super.onStop(); + cancelIdleTimeout(); // 取消無操作逾時計時器 + getLogs().info("[CashPayDialogGeneral] onStop() 被調用,確保串口安全關閉"); + // 異常關閉場景:給予 3 秒緩衝時間,以防正在找零 + safeCloseSerialPort(3000); + stopPollingStatus(); + + Log.d("CashPayDialogGeneral", "onStop(): 關閉串口與停止讀取"); + } + + /** + * 啟動無操作逾時計時器(3 分鐘無投幣自動關閉) + * 防止 MDB 串口長時間佔用,導致機器 Recycler 計數被清零 + */ + private void startIdleTimeout() { + cancelIdleTimeout(); + idleTimeoutRunnable = () -> { + getLogs().info("[無操作逾時] " + (IDLE_TIMEOUT_MS / 60000) + " 分鐘無投幣動作,自動關閉現金支付視窗"); + if (isShowing()) { + new Handler(Looper.getMainLooper()).post(() -> { + messageShow("超過 " + (IDLE_TIMEOUT_MS / 60000) + " 分鐘無操作,投幣等候已展時。", 3000); + // 3 秒後自動關閉 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (isShowing()) { + getLogs().info("[無操作逾時] 導引自動 cancel()"); + cancel(); + } + }, 3000); + }); + } + }; + idleTimeoutHandler.postDelayed(idleTimeoutRunnable, IDLE_TIMEOUT_MS); + getLogs().info("[無操作逾時] 已啟動 " + (IDLE_TIMEOUT_MS / 60000) + " 分鐘無操作自動關閉計時器"); + } + + /** + * 重設無操作逾時計時器(每次收到投幣事件時呼叫) + */ + private void resetIdleTimeout() { + if (isDeviceReady) { + startIdleTimeout(); // 重新啟動即可重設計時 + } + } + + /** + * 取消無操作逾時計時器 + */ + private void cancelIdleTimeout() { + if (idleTimeoutRunnable != null) { + idleTimeoutHandler.removeCallbacks(idleTimeoutRunnable); + idleTimeoutRunnable = null; + } + } + + /** + * 啟動定時輪詢設備狀態 + * @param fast 是否使用快速輪詢(1秒一次,用於初始化後等待就緒) + */ + private void startPollingStatus(boolean fast) { + stopPollingStatus(); // 先確保之前的停止 + this.isFastPolling = fast; + int interval = fast ? 1000 : 10000; + + getLogs().info("[輪詢] 啟動狀態輪詢,模式: " + (fast ? "快速 (1s)" : "正常 (10s)")); + + pollingRunnable = new Runnable() { + @Override + public void run() { + if (simpleCashAcceptorHandler != null) { + // getLogs().info("[輪詢] 請求設備庫存狀態..."); + // 請求紙鈔機回收器庫存 + sendMdbCommand("3705", simpleCashAcceptorHandler); + // 請求硬幣機筒位狀態 + new Handler(Looper.getMainLooper()).postDelayed(() -> + sendMdbCommand("0A", simpleCashAcceptorHandler), 400); + } + pollingHandler.postDelayed(this, interval); + } + }; + pollingHandler.post(pollingRunnable); + } + + /** + * 停止定時輪詢 + */ + private void stopPollingStatus() { + if (pollingHandler != null && pollingRunnable != null) { + getLogs().info("[輪詢] 停止設備狀態輪詢"); + pollingHandler.removeCallbacks(pollingRunnable); + pollingRunnable = null; + } + } + + /** + * 計算購物車總價 + * + * @return 總價 + */ + public int getTotalPrice() { + int totalPrice = 0; + if (MyApp.getInstance().getBuyList().size() == 0) + return totalPrice; + for (BuyStructure buy : MyApp.getInstance().getBuyList()) { + if (!buy.isFreeGift()) + totalPrice += buy.getFullPrice(); + } + if (MyApp.getInstance().getReportFreeGiftCodeStructure() != null) { + switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) { + case 0: + break; + case 1: + totalPrice -= MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountAmount(); + break; + case 2: + totalPrice = (int) (totalPrice + * MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountPercentage()); + break; + } + } + if (totalPrice == 0) + totalPrice = 1; + return totalPrice; + } + + // 儲存待退款金額至本地 + private void savePendingRefund(int amount) { + if (amount <= 0) return; + try { + Context ctx = getCtx(); + if (ctx != null) { + android.content.SharedPreferences sp = ctx.getSharedPreferences("CashRefundPrefs", Context.MODE_PRIVATE); + int existingAmount = sp.getInt("pending_refund", 0); + int newAmount = existingAmount + amount; + sp.edit().putInt("pending_refund", newAmount).apply(); + getLogs().info("[退款紀錄] 已儲存待退款金額: " + amount + " 元,累計待退款: " + newAmount + " 元"); + } + } catch (Exception e) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "儲存待退款金額失敗: " + e.getMessage()); + } + } + + // 獲取待退款金額 + private int getPendingRefund() { + try { + Context ctx = getCtx(); + if (ctx != null) { + android.content.SharedPreferences sp = ctx.getSharedPreferences("CashRefundPrefs", Context.MODE_PRIVATE); + return sp.getInt("pending_refund", 0); + } + } catch (Exception e) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "讀取待退款金額失敗: " + e.getMessage()); + } + return 0; + } + + // 清除待退款金額 + private void clearPendingRefund() { + try { + Context ctx = getCtx(); + if (ctx != null) { + android.content.SharedPreferences sp = ctx.getSharedPreferences("CashRefundPrefs", Context.MODE_PRIVATE); + sp.edit().remove("pending_refund").apply(); + getLogs().info("[退款紀錄] 已清除待退款紀錄"); + } + } catch (Exception e) { + getLogs().error(ErrorCode.UNKNOWN_ERROR, "清除待退款金額失敗: " + e.getMessage()); + } + } + + // 檢查並執行上次未退款的自癒流程 + private void checkAndProcessPendingRefund() { + final int pendingRefund = getPendingRefund(); + if (pendingRefund <= 0) return; + + getLogs().info("[退款紀錄] 偵測到上次未退還金額: " + pendingRefund + " 元,開始處理自癒退款流程..."); + + new Handler(Looper.getMainLooper()).postDelayed(() -> { + messageShow("偵測到上次未退還金額 " + pendingRefund + " 元,系統正在自動退還...", 5000); + + // 嘗試退款 + performChange(pendingRefund, true, () -> { + getLogs().info("[退款紀錄] 上次未退還金額 " + pendingRefund + " 元已成功退還!"); + clearPendingRefund(); + messageShow("成功退還上次未退金額 " + pendingRefund + " 元", 3000); + }); + }, 2000); + } + + /// ////////////不重要 + + /** + * 顯示提示訊息 + * + * @param message 訊息內容 + * @param second 顯示時間(毫秒) + */ + void messageShow(String message, int second) { + Builder builder = new Builder(getCtx()); + final AlertDialog dialog = builder.create(); + View dialogView = View.inflate(getCtx(), R.layout.mydialog1, null); + TextView t1 = dialogView.findViewById(R.id.text01); + TextView t2 = dialogView.findViewById(R.id.text02); + t1.setText("提示!"); + t2.setText(message); + dialog.setView(dialogView); + if (dialog.getWindow() != null) { + dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); + dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT); + } + dialog.show(); + if (second > 0) + new Handler().postDelayed(dialog::dismiss, second); + } + + /** + * 顯示 MDB 指令發送對話框(用於測試) + * + * @param handler MDB 處理器 + */ + private void showMdbCommandDialog(SimpleCashAcceptorHandler handler) { + Builder builder = new Builder(getCtx()); + builder.setTitle("MDB 指令發送"); + final EditText input = new EditText(getCtx()); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint("請輸入HEX指令 (例如: 34FFFFFFFF)"); + builder.setView(input); + builder.setPositiveButton("發送", (dialog, which) -> { + String hexCommand = input.getText().toString().trim(); + if (!hexCommand.isEmpty()) { + sendMdbCommand(hexCommand, handler); + } else { + // Toast.makeText(getCtx(), "指令不能為空", Toast.LENGTH_SHORT).show(); + } + }); + builder.setNegativeButton("取消", (dialog, which) -> dialog.cancel()); + builder.show(); + } + + @Override + protected void onCreate(Context context) { + } + + public CashPayDialogGeneral(Context context, HandlerMain srcHandlerMain) { + super(context, srcHandlerMain); + } + + @Override + protected Context setCtx() { + return getContext(); + } + + @Override + protected Class setCls() { + return getClass(); + } + + @Override + protected HandlerMain setHandlerMain() { + return new ToastHandlerMain(getContext(), getLogs()) { + @Override + protected void execute(Context context, int commandCode, String message) { + } + }; + } + + /// ////////////不重要 + +} \ No newline at end of file diff --git a/app/src/General/res/layout/dialog_checkout_cash_pay.xml b/app/src/General/res/layout/dialog_checkout_cash_pay.xml new file mode 100644 index 0000000..9eebc7f --- /dev/null +++ b/app/src/General/res/layout/dialog_checkout_cash_pay.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +