Compare commits

..

No commits in common. "ui-design" and "main" have entirely different histories.

237 changed files with 6309 additions and 17036 deletions

View File

@ -28,8 +28,8 @@ trigger: always_on
依據公司標準規範,實體機台發佈之 APK 與版本名稱需嚴格符合以下欄位對照:
```
來源 _ 安卓號 _ 主板號 _ 機器系列 _ 螢幕大小 _ 次版號 _ 修補版號 _ 環境
S _ 12 _ XY _ U _ 10 _ 08 _ 8 _ R
來源 _ 安卓號 _ 主板號 _ 機器系列 _ 專案名稱 _ 螢幕大小 _ 次版號 _ 修補版號 _ 環境
S _ 12 _ XY _ U _ Chengwai _ 10 _ 08 _ 8 _ R
```
### 欄位解析:
@ -37,6 +37,7 @@ S _ 12 _ XY _ U _ 10 _ 08 _ 8 _ R
* **`12` (安卓號)**Android 系統主版本號。
* **`XY` (主板號)**:主板型號規格。
* **`U` (機器系列 / 機型規格)**:例如代表 U 系列機型。
* **`Chengwai` (專案名稱)**:客戶/銷售場景專案代碼(如 `General`, `Chengwai`, `Cmuh`)。
* **`10` (螢幕大小)**:對應 Gradle 中的 `verMajor`
* **`08` (次版號)**:對應 Gradle 中的 `verMinor`(格式化為雙位數)。
* **`8` (修補版號)**:對應 Gradle 中的 `verPatch`
@ -54,32 +55,38 @@ S _ 12 _ XY _ U _ 10 _ 08 _ 8 _ R
---
## 4. 單維度硬體規格 (Hardware Flavor) 與 APK 命名規範
## 4. 雙維度產品風味 (Product Flavors) 與 APK 命名規範
本專案原本採用雙維度,但目前客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發)動態決定,因此不再使用 flavor 分家。
目前僅保留**單一維度 `hardware`**,專門處理底層硬體規格差異。
本專案採用**雙維度 Flavor** 架構,以完美分離「客戶銷售場景」與「底層硬體規格」,方便後續快速擴充。
### 4.1 硬體維度定義 (flavorDimensions)
本專案於 `app/build.gradle` 中定義了唯一的 Flavor 維度 `hardware`
### 4.1 維度定義 (flavorDimensions)
本專案於 `app/build.gradle` 中定義了兩個 Flavor 維度
* **`S12XYU`** (10 吋預設硬體):代表 `S` (來源) + `12` (安卓號) + `XY` (主板號) + `U` (機器系列/機型規格)。
* **`S9XYU`** (21.5 吋硬體):代表中國醫現場使用的 Android 9 / 21.5 吋硬體規格。
1. **`project` 維度 (客戶/銷售場景區隔)**
* **`General`** (通用主幹):預設顯示付款方式選單(彈出 `BuyDialog`)。未來新客戶最常用此通用主幹。
* **`Chengwai`** (晟崴客製):直接跳出員工卡刷卡畫面(彈出 `MemberCardPickupDialog`),不顯示付款選單。
* **`Cmuh`** (中國醫客製):支援 QR Code 掃碼、處方籤醫令離線 AES 解密與解析領藥流程。
2. **`hardware` 維度 (硬體與系統規格區隔)**
* **`S12XYU`**:代表 `S` (來源) + `12` (安卓號) + `XY` (主板號) + `U` (機器系列/機型規格)。
* **`S9XYU`**:代表中國醫現場使用的 Android 9 / 21.5 吋硬體規格;除非明確指定中國醫 10 吋或 `S12XYU`,中國醫 (`Cmuh`) 相關修正與驗證預設必須使用此硬體 flavor。
### 4.2 Flavor 驗證與編譯指令選擇
修改後,必須編譯對應硬體規格的 Gradle variant不可只用通用 `assembleDebug`
針對特定客戶或現場問題修改後,必須編譯對應的 Gradle variant不可只用通用 `assembleDebug` 或錯誤硬體 flavor 代替。
* **10 吋一般機台版 (含晟崴、通用等)**`./gradlew :app:assembleS12XYUDebug --no-daemon`
* **21.5 吋中國醫等大螢幕機台版**`./gradlew :app:assembleS9XYUDebug --no-daemon`
* **中國醫現場版 (`Cmuh`) 驗證指令**`./gradlew :app:assembleCmuhS9XYUDebug --no-daemon`
* **晟崴版 (`Chengwai`) 預設驗證指令**`./gradlew :app:assembleChengwaiS12XYUDebug --no-daemon`
* **通用版 (`General`) 預設驗證指令**`./gradlew :app:assembleGeneralS12XYUDebug --no-daemon`
### 4.3 實體機台 APK 命名鐵律
為了符合實體機台 OTA 平台發佈規範,打包輸出的 APK 檔名必須遵循以下格式:
`app-[格式化硬體規格]-[版本號]-[BuildType].apk`
為了符合實體機台 OTA 平台發佈規範,打包輸出的 APK 檔名必須嚴格連動雙維度,並遵循以下格式:
`app-[格式化硬體規格]_[專案名稱]-[版本號]-[BuildType].apk`
* **格式化規則**`S12XYU` 會在 Gradle 打包時被自動展開格式化為 `S_12_XY_U`
* **輸出檔名範例**
* `app-S_12_XY_U-10_08_8_R-release.apk`
* `app-S_9_XY_U-21_08_8_R-release.apk`
* `app-S_12_XY_U_General-10_08_8_R-release.apk`
* `app-S_12_XY_U_Chengwai-10_08_8_R-release.apk`
* `app-S_12_XY_U_Cmuh-10_08_8_R-release.apk`
* **注意**:此檔名轉換規則已完全由 `app/build.gradle` 頂部 Gradle 變數與 `applicationVariants.all` 自動連動生成,**嚴禁手動修改輸出檔名**。
---

View File

@ -0,0 +1,34 @@
---
name: android-deploy
description: 用於 TaiwanStar 專案的 Android App 編譯、安裝與自動啟動。當使用者要求「安裝到手機」、「部署 APK」或「更新 App」時應使用此技能。
---
# Android 部署技能 (TaiwanStar 專案專用)
此技能定義了在 WSL 環境下編譯 Android App 並透過 Windows ADB 安裝至實體手機的標準流程。
## 前置要求
1. **Java 版本**:必須使用 OpenJDK 17。
2. **ADB 工具**:使用 Windows 端安裝的 ADB (路徑:`/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe`)。
3. **手機連線**:手機必須開啟 USB 調試,且已連線至電腦。
## 標準部署流程
執行以下複合指令以完成「編譯 + 安裝 + 自動啟動」:
```bash
# 1. 設定環境變數
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
# 2. 執行編譯與安裝 (包含自動開啟 HomeActivity)
./gradlew :app:assembleS_12_XY_U_Standard_Debug && \
/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe install -r -d -t app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk && \
/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity
```
## 注意事項
- **安裝失敗**:若出現 `INSTALL_FAILED_UPDATE_INCOMPATIBLE`,代表簽名或版本衝突,請先執行 `adb uninstall com.unibuy.smartdevice`
- **ADB 找不到裝置**:請確認手機 USB 模式是否為「檔案傳輸」或「PTP」並檢查 Windows 端的 ADB server 是否正常執行。
- **Mock Mode**:此部署版本已包含 Mock Mode可在無串口設備的手機上正常運作不崩潰。

View File

@ -0,0 +1,27 @@
#!/bin/bash
# TaiwanStar Android 部署腳本
# 自動執行:編譯 -> 安裝 -> 啟動
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
APK_PATH="app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk"
PACKAGE_NAME="com.unibuy.smartdevice"
LAUNCHER_ACTIVITY=".HomeActivity"
echo "🚀 開始編譯 APK..."
./gradlew :app:assembleS_12_XY_U_Standard_Debug
if [ $? -eq 0 ]; then
echo "✅ 編譯成功,正在安裝至手機..."
$ADB_PATH install -r -d -t $APK_PATH
if [ $? -eq 0 ]; then
echo "🎉 安裝成功,正在啟動 App..."
$ADB_PATH shell am start -n $PACKAGE_NAME/$LAUNCHER_ACTIVITY
else
echo "❌ 安裝失敗,請檢查手機連線狀態。"
fi
else
echo "❌ 編譯失敗,請檢查程式碼錯誤。"
fi

View File

@ -4,19 +4,13 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
# 編譯與安裝 APK 工作流 (compiler-apk)
這個 Workflow 協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk`AI 會自動執行下列步驟。
> [!IMPORTANT]
> **客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),不再用 flavor 分家。**
> 打包對象只剩 **硬體規格hardware flavor**`S12XYU`10 吋)/ `S9XYU`21.5 吋 / Android 9
> 同一支 APK 在現場會依雲端 `shopping_mode` 自動切換為 basic購物車/ employee_card員工卡/ pickup_sheet取貨單
這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk`AI 會自動為您執行以下步驟:
> [!TIP]
> **參數 = 硬體規格:**
> * 預設(不帶參數):編譯 **`S12XYU`10 吋)**。
> * `/compiler-apk S9`(或 `S9XYU` / `21.5` / `大螢幕`)→ 編譯 **`S9XYU`21.5 吋 / Android 9**。
> * `/compiler-apk S12`(或 `S12XYU` / `10`)→ 編譯 **`S12XYU`10 吋)**。
> * 相容舊用語:`中國醫`→S9XYU、`晟崴`/`通用`→S12XYU僅作硬體對應流程仍由雲端決定
> **支援智慧硬體判斷與參數化覆寫:**
> * 預設(不帶參數):編譯 **`Chengwai` 晟崴版 (S12XYU)**。
> * **智慧判斷**:當編譯 **`中國醫 (Cmuh)`** 時,會自動識別並預設編譯 **`S9XYU` (Android 9 / 21.5 吋)**;編譯 **`General`** 與 **`Chengwai`** 時則預設編譯 **`S12XYU` (10 吋)**。
> * **參數手動覆寫**:支援手動指定硬體尺寸。例如 `/compiler-apk Cmuh S12` 可強行編譯中國醫的 10 吋版本;`/compiler-apk General S9` 可強行編譯通用版的大螢幕 21.5 吋版本。
## 步驟零:自動檢查變更與升級版號
在開始編譯之前AI 助手會自動檢查是否有程式碼變更,以落實實體機台的 OTA 升級規範:
@ -25,49 +19,60 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
3. 遞增完成後,向使用者呈報最新的版本號資訊,接著繼續執行步驟一。
## 步驟一:設定環境並編譯 Debug APK
使用 Java 17 與 Gradle 編譯對應硬體規格的 Debug 程式碼。
使用 Java 17 與 Gradle 編譯對應 Flavor 與硬體規格的 Debug 程式碼。
```bash
// turbo
# 終止殘留的 Gradle Daemon 程序,避免多個 Daemon 互搶資源,同時避免誤殺 VS Code Gradle 擴充功能
# 防呆:先優雅 --stop僅在仍殘留時才升級終止先 TERM 再 KILL避免無差別強殺撞鎖導致 clean 編譯隨機失敗
#
# ⚠️ 關鍵pgrep/pkill 的比對字串必須「拆開」拼接,不可在本檔字面寫出完整 class 名。
# 否則在背景任務bash -c "...eval '整段腳本'...")情境下,腳本自己的命令列會含有
# 完整的 GradleDaemon class 字串 → pgrep -f 比對到自己 → pkill 把跑腳本的 bash 自殺
# → exit 144編譯根本還沒開始。再加 -v $$ 排除自身 PID 雙重保險。
DAEMON_PAT='org.gradle.launcher.daemon.bootstrap''.GradleDaemon' # 字面被拆開pgrep 不會命中本腳本
daemon_alive() { pgrep -f "$DAEMON_PAT" 2>/dev/null | grep -vx "$$" | grep -q .; }
./gradlew --stop 2>/dev/null || true
sleep 2
if daemon_alive; then
pkill -TERM -f "$DAEMON_PAT" 2>/dev/null || true
sleep 3
if daemon_alive; then
pkill -9 -f "$DAEMON_PAT" 2>/dev/null || true
sleep 2
fi
fi
./gradlew --stop 2>/dev/null; pkill -9 -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null; sleep 3
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
# 取得目標硬體規格(預設 S12XYU接受 S9/S12 與相容舊用語)
# 取得目標 Flavor 參數
RAW_INPUT="$1"
TARGET_HARDWARE="S12XYU"
TARGET_FLAVOR="Chengwai"
if [ -n "$RAW_INPUT" ]; then
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
case "$LOWER" in
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
*) echo "⚠️ 無法識別「$RAW_INPUT」沿用預設 S12XYU" ;;
esac
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
TARGET_FLAVOR="General"
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
TARGET_FLAVOR="Chengwai"
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
TARGET_FLAVOR="Cmuh"
else
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
fi
echo "🚀 開始編譯: assemble${TARGET_HARDWARE}Debug"
# 根據 Flavor 動態設定智慧硬體規格預設值
TARGET_HARDWARE="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
TARGET_HARDWARE="S9XYU"
fi
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
RAW_HW_INPUT="$2"
if [ -n "$RAW_HW_INPUT" ]; then
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
TARGET_HARDWARE="S9XYU"
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
TARGET_HARDWARE="S12XYU"
fi
fi
# 安全防護:檢查是否為支援的客製項目
if [ "$TARGET_FLAVOR" != "General" ] && [ "$TARGET_FLAVOR" != "Chengwai" ] && [ "$TARGET_FLAVOR" != "Cmuh" ]; then
echo "❌ 不支援的 Flavor: $TARGET_FLAVOR請使用 General, Chengwai 或 Cmuh。"
exit 1
fi
echo "🚀 開始編譯: ${TARGET_FLAVOR}${TARGET_HARDWARE}Debug"
# 使用 clean 確保完整重新編譯,避免舊快取遮蔽 import 缺失等錯誤
./gradlew clean :app:assemble${TARGET_HARDWARE}Debug --no-daemon
./gradlew clean :app:assemble${TARGET_FLAVOR}${TARGET_HARDWARE}Debug --no-daemon
```
## 步驟二:導出 APK 到 outputs 資料夾
@ -78,44 +83,80 @@ echo "🚀 開始編譯: assemble${TARGET_HARDWARE}Debug"
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
mkdir -p outputs
# 取得 Flavor 參數
RAW_INPUT="$1"
TARGET_HARDWARE="S12XYU"
TARGET_FLAVOR="Chengwai"
if [ -n "$RAW_INPUT" ]; then
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
case "$LOWER" in
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
esac
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
TARGET_FLAVOR="General"
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
TARGET_FLAVOR="Chengwai"
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
TARGET_FLAVOR="Cmuh"
else
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
fi
# 根據 Flavor 動態設定智慧硬體規格預設值
TARGET_HARDWARE="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
TARGET_HARDWARE="S9XYU"
fi
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
RAW_HW_INPUT="$2"
if [ -n "$RAW_HW_INPUT" ]; then
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
TARGET_HARDWARE="S9XYU"
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
TARGET_HARDWARE="S12XYU"
fi
fi
# 解析硬體規格來決定輸出檔案名格式 (S9 -> S_9, S12 -> S_12)
FORMATTED_HW=""
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
FORMATTED_HW="S_9_XY_U"; MAJOR=21
FORMATTED_HW="S_9_XY_U"
else
FORMATTED_HW="S_12_XY_U"; MAJOR=10
FORMATTED_HW="S_12_XY_U"
fi
# 動態尋找編譯產出的最新 APK 檔案(檔名已無 project 段app-<HW>-<ver>-debug.apk
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_HARDWARE}/debug/ -name "app-${FORMATTED_HW}-*-debug.apk" | head -n 1)
# 動態尋找編譯產出的最新 APK 檔案
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}${TARGET_HARDWARE}/debug/ -name "app-${FORMATTED_HW}_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
if [ -n "$APK_SOURCE" ]; then
APK_NAME=$(basename "$APK_SOURCE")
APK_DEST="outputs/$APK_NAME"
cp "$APK_SOURCE" "$APK_DEST"
echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST"
# 提取與計算版本資訊
MAJOR=$(grep "def verMajor" app/build.gradle | awk '{print $4}')
# 若在 build.gradle 內 Major 是由 hardware 動態判定,直接讀取並在 Shell 依據當前規格計算
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
MAJOR=21
else
MAJOR=10
fi
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
FLAVOR="${TARGET_FLAVOR}${TARGET_HARDWARE}"
echo "=================================================="
echo "📢 後台更新 keyin 參考資料:"
echo "--------------------------------------------------"
echo "* 硬體規格 (hardware)$TARGET_HARDWARE${MAJOR} 吋)"
echo "* 產品風味 (Flavor)$FLAVOR"
echo "* 版本名稱 (versionName)$VERSION_NAME"
echo "* 版本代碼 (versionCode)$VERSION_CODE"
echo "* APK 檔案名稱:$APK_NAME"
echo "* 購買流程:由雲端 shopping_mode 決定basic / employee_card / pickup_sheet"
echo "* 更新說明:[請在此處附上口語化的一句話更新說明]"
echo "=================================================="
else
@ -131,27 +172,54 @@ fi
// turbo
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
# 取得 Flavor 參數
RAW_INPUT="$1"
TARGET_HARDWARE="S12XYU"
TARGET_FLAVOR="Chengwai"
if [ -n "$RAW_INPUT" ]; then
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
case "$LOWER" in
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
esac
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
TARGET_FLAVOR="General"
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
TARGET_FLAVOR="Chengwai"
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
TARGET_FLAVOR="Cmuh"
else
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
fi
# 根據 Flavor 動態設定智慧硬體規格預設值
TARGET_HARDWARE="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
TARGET_HARDWARE="S9XYU"
fi
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
RAW_HW_INPUT="$2"
if [ -n "$RAW_HW_INPUT" ]; then
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
TARGET_HARDWARE="S9XYU"
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
TARGET_HARDWARE="S12XYU"
fi
fi
# 解析硬體規格來決定輸出檔案名格式 (S9 -> S_9, S12 -> S_12)
FORMATTED_HW=""
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
FORMATTED_HW="S_9_XY_U"
else
FORMATTED_HW="S_12_XY_U"
fi
# 檢查是否有設備連接
DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | wc -l)
if [ "$DEVICE_COUNT" -gt 0 ]; then
echo "📱 偵測到 $DEVICE_COUNT 個設備,開始安裝..."
APK_TARGET=$(find outputs/ -name "app-${FORMATTED_HW}-*-debug.apk" | head -n 1)
APK_TARGET=$(find outputs/ -name "app-${FORMATTED_HW}_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
if [ -n "$APK_TARGET" ]; then
$ADB_PATH install -r -d -t "/home/mama/projects/TaiwanStar_general_size_s_Chengwai/$APK_TARGET"
echo "🚀 安裝完成,正在啟動 App..."

View File

@ -4,26 +4,19 @@ description: 自動編譯正式發行 Release APK、升級 Patch 版本號、並
# 生產環境發版打包工作流 (release-apk)
這個 Workflow 專門為應用程式「生產環境上線/發版」設計。呼叫 `/release-apk` 將自動完成嚴格的發行檢查、版號遞增、Release 簽章打包與發版資訊生成。
> [!IMPORTANT]
> **客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),不再用 flavor 分家。**
> 發版對象只剩 **硬體規格hardware flavor**`S12XYU`10 吋)/ `S9XYU`21.5 吋 / Android 9
> 同一支 APK 上線後,現場機台由雲端 `shopping_mode` 決定購買流程。
> **部署鐵律**發布統一版前star-cloud 必須先為每台現役機設好 `shopping_mode`
> (晟崴=employee_card、中國醫=pickup_sheet否則預設 basic 會跑錯流程。
這個 Workflow 專門為應用程式「生產環境上線/發版」設計。當您準備好將特定客戶的最新功能派送至實體販賣機時,呼叫 `/release-apk` 將自動為您完成嚴格的發行檢查、版號遞增、Release 簽章打包與發版資訊生成。
> [!TIP]
> **參數 = 硬體規格**
> * 預設(不帶參數):打包 **`S12XYU`10 吋)**。
> * `/release-apk S9`(或 `S9XYU` / `中國醫`)→ 打包 **`S9XYU`21.5 吋)**。
> * `/release-apk S12`(或 `S12XYU` / `晟崴` / `通用`)→ 打包 **`S12XYU`10 吋)**。
> **支援參數化指定編譯變體:**
> * 預設(不帶參數):打包 **`Chengwai` (晟崴客製正式版)**。
> * 您可以呼叫 `/release-apk General`(或 `general`)打包 **`General` (通用正式版)**。
> * 您可以呼叫 `/release-apk Cmuh`(或 `cmuh`)打包 **`Cmuh` (中國醫客製正式版)**。
---
## 步驟零:發行檢查與版號自動遞增 (OTA 鐵律)
為了確保實體機台能夠順利觸發 OTA 自動下載覆蓋安裝,新編譯的 `versionCode` 必須嚴格大於現場的舊版號:
1. AI 助手將讀取 `app/build.gradle` 並自動將 `verPatch`(修補版號)**遞增 1**,確保 versionCode 順利變大。
1. AI 助手將讀取 `app/build.gradle` 並自動將 `verPatch`(修補版號)**遞增 1**(例如從 `8` 自動升級為 `9`,確保 versionCode 順利變大。
2. 自動在 terminal 呈報即將打包的最新版本號資訊。
## 步驟一:清理快取並進行正式簽章編譯
@ -32,37 +25,33 @@ description: 自動編譯正式發行 Release APK、升級 Patch 版本號、並
```bash
// turbo
# 終止殘留的 Gradle Daemon 程序,避免多個 Daemon 互搶資源,同時避免誤殺 VS Code Gradle 擴充功能
# 防呆:先優雅 --stop僅在仍殘留時才升級終止先 TERM 再 KILL避免無差別強殺撞鎖導致 clean 編譯隨機失敗
./gradlew --stop 2>/dev/null || true
sleep 2
if pgrep -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" >/dev/null 2>&1; then
pkill -TERM -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null || true
sleep 3
if pgrep -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" >/dev/null 2>&1; then
pkill -9 -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null || true
sleep 2
fi
fi
./gradlew --stop 2>/dev/null; pkill -9 -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null; sleep 3
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
# 取得目標硬體規格(預設 S12XYU
RAW_INPUT="$1"
TARGET_HARDWARE="S12XYU"
if [ -n "$RAW_INPUT" ]; then
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
case "$LOWER" in
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
*) echo "⚠️ 無法識別「$RAW_INPUT」沿用預設 S12XYU" ;;
esac
# 取得目標 Flavor 參數(預設為 Chengwai支援 General, Chengwai, Cmuh
TARGET_FLAVOR="Chengwai"
if [ -n "$1" ]; then
TARGET_FLAVOR=$(echo "$1" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
echo "📦 [Release] 開始編譯正式版變體: assemble${TARGET_HARDWARE}Release"
# 安全防護:檢查是否為支援的客製項目
if [ "$TARGET_FLAVOR" != "General" ] && [ "$TARGET_FLAVOR" != "Chengwai" ] && [ "$TARGET_FLAVOR" != "Cmuh" ]; then
echo "❌ 不支援的 Flavor: $TARGET_FLAVOR請使用 General, Chengwai 或 Cmuh。"
exit 1
fi
# 根據專案 Flavor 判定硬體 Flavor中國醫 Cmuh 使用 S9XYU其餘使用 S12XYU
HW_FLAVOR="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
HW_FLAVOR="S9XYU"
fi
echo "📦 [Release] 開始編譯正式版變體: ${TARGET_FLAVOR}${HW_FLAVOR}Release"
# 使用 clean 確保完整重新編譯,避開 incremental 快取問題
./gradlew clean :app:assemble${TARGET_HARDWARE}Release --no-daemon
./gradlew clean :app:assemble${TARGET_FLAVOR}${HW_FLAVOR}Release --no-daemon
```
## 步驟二:導出正式 APK 並產出後台 Keyin 派送參考資料
@ -73,24 +62,19 @@ echo "📦 [Release] 開始編譯正式版變體: assemble${TARGET_HARDWARE}Rele
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
mkdir -p outputs
RAW_INPUT="$1"
TARGET_HARDWARE="S12XYU"
if [ -n "$RAW_INPUT" ]; then
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
case "$LOWER" in
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
esac
# 取得 Flavor 參數
TARGET_FLAVOR="Chengwai"
if [ -n "$1" ]; then
TARGET_FLAVOR=$(echo "$1" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
FORMATTED_HW="S_9_XY_U"; MAJOR=21
else
FORMATTED_HW="S_12_XY_U"; MAJOR=10
HW_FLAVOR="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
HW_FLAVOR="S9XYU"
fi
# 動態尋找編譯產出的最新正式版 APK 檔案(檔名已無 project 段)
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_HARDWARE}/release/ -name "app-${FORMATTED_HW}-*-release.apk" | head -n 1)
# 動態尋找編譯產出的最新正式版 APK 檔案
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}${HW_FLAVOR}/release/ -name "app-S_*_${TARGET_FLAVOR}-*-release.apk" | head -n 1)
if [ -n "$APK_SOURCE" ]; then
APK_NAME=$(basename "$APK_SOURCE")
@ -99,21 +83,31 @@ if [ -n "$APK_SOURCE" ]; then
echo "=================================================="
echo "✅ 正式版 APK 封裝成功,已導出至:$APK_DEST"
echo "=================================================="
# 提取與計算版本資訊
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
# 根據硬體規格決定 Major (S9XYU 為 21 吋S12XYU 為 10 吋)
if [ "$HW_FLAVOR" = "S9XYU" ]; then
MAJOR=21
else
MAJOR=10
fi
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
FLAVOR="${TARGET_FLAVOR}${HW_FLAVOR}"
echo "🚀 實體販賣機 OTA 後台新增版本 Keyin 參考資料:"
echo "--------------------------------------------------"
echo "1. 硬體規格 (hardware) : $TARGET_HARDWARE${MAJOR} 吋)"
echo "1. 專案/客戶風味 (Flavor) : $FLAVOR"
echo "2. 版本代碼 (versionCode) : $VERSION_CODE"
echo "3. 版本名稱 (versionName) : $VERSION_NAME"
echo "4. 正式版 APK 檔名 : $APK_NAME"
echo "5. 購買流程 : 由雲端 shopping_mode 決定(須先於後台設好)"
echo "4. 螢幕吋數 (Screen Size) : ${MAJOR} 吋"
echo "5. 正式版 APK 檔名 : $APK_NAME"
echo "--------------------------------------------------"
echo "📢 [溫馨提醒] 請將此 APK 上傳至 OTA 平台(依硬體分版),並確認該機 shopping_mode 已設定。"
echo "📢 [溫馨提醒] 請將此 APK 上傳至 OTA 平台,並設定為對應機型的強迫升級。"
echo "=================================================="
else
echo "❌ 找不到編譯產出的正式版 APK 檔案,封裝失敗!"
@ -122,13 +116,14 @@ fi
```
## 步驟三:自動化 Git 發版標記引導
為了落實公司發版規範第 5 條「必須在主分支合併節點打上 Git Tag以利日後追溯特定現場機台的版本程式碼」AI 助手會自動計算出對應的版本 Tag 標籤與提示:
為了落實公司發版規範第 5 條「必須在主分支合併節點打上 Git Tag以利日後追溯特定現場機台的版本程式碼」AI 助手會自動為您計算出對應的版本 Tag 標籤與提示:
```bash
// turbo
MAJOR=$(grep "def verMajor" app/build.gradle | awk '{print $4}')
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
TAG_NAME="v1.${MINOR}.${PATCH}"
TAG_NAME="v${MAJOR}.${MINOR}.${PATCH}"
echo "=================================================="
echo "🔔 Git 發版追溯標記指南"

View File

@ -1,25 +0,0 @@
---
description: 編譯 Debug APK、導出 outputs/、偵測到設備則安裝(依硬體規格 S9/S12 打包;購買流程由雲端 shopping_mode 決定)
argument-hint: [S9|S12]
allowed-tools: Bash, Read, Edit
---
執行本專案的 APK 編譯安裝 workflow定義於 `.agents/workflows/compiler-apk.md`(單一真相來源)。
本次參數:「$ARGUMENTS」硬體規格S9 / S12可能為空預設 S12XYU
> 注意:客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),打包對象只剩硬體規格。
> 同一支 APK 在現場依雲端 `shopping_mode` 切換 basic / employee_card / pickup_sheet。
## 執行方式
1. 先 Read `.agents/workflows/compiler-apk.md`,理解步驟零~三。
2. 步驟零(版號):執行 `git status --porcelain app/`;若有未提交改動且本輪對話尚未升過版,讀 `app/build.gradle``verPatch` 遞增 1 並回報新版號。
3. 步驟一~三:依序執行 workflow 內每個 bash 區塊。**關鍵**workflow 的 bash 以位置參數 `$1` 取得硬體規格,所以執行每個區塊前,先在區塊最前面注入本次參數:
set -- $ARGUMENTS
讓 workflow 內的 `$1` 正確解析S9/S12或相容舊用語 中國醫→S9、晟崴/通用→S12。若本次無參數workflow 自身預設 S12XYU。
4. 環境用 workflow 指定的 Java 17`JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64`)。
5. 編譯/導出屬長任務 → 用 run_in_background完成再回報。
6. 步驟三會偵測 adb 設備:有連設備才安裝並啟動 App沒設備就保留 APK 結束。
7. 最後完整回報 workflow 輸出的「後台 keyin 參考資料」(硬體規格 / versionName / versionCode / APK 檔名)。

View File

@ -1,26 +0,0 @@
---
description: 正式版 Release APK 打包、升 Patch 版號、產出 OTA keyin 與 Git tag 指引(依硬體 S9/S12流程由雲端 shopping_mode 決定)
argument-hint: [S9|S12]
allowed-tools: Bash, Read, Edit
---
執行本專案的正式發版 workflow定義於 `.agents/workflows/release-apk.md`(單一真相來源)。
本次參數:「$ARGUMENTS」硬體規格S9 / S12可能為空預設 S12XYU
> 注意:客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),發版對象只剩硬體規格。
> 部署鐵律:發布前 star-cloud 必須先為每台現役機設好 `shopping_mode`,否則預設 basic 跑錯流程。
## 執行方式
1. 先 Read `.agents/workflows/release-apk.md`,理解步驟零~三。
2. 步驟零OTA 鐵律):讀 `app/build.gradle``verPatch` 遞增 1確保 versionCode 變大可觸發 OTA回報新版號。
3. 步驟一~三:依序執行 workflow 內每個 bash 區塊。**關鍵**workflow 的 bash 以位置參數 `$1` 取得硬體規格,執行每個區塊前先注入本次參數:
set -- $ARGUMENTS
無參數時預設 S12XYU接受 S9/S12或相容 中國醫→S9、晟崴/通用→S12
4. 環境用 Java 17`JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64`Release 會跑混淆與 lintVital較久 → 用 run_in_background。
5. 步驟二導出正式簽章 APK 到 `outputs/` 並印出 OTA 後台 keyin 對照資料。
6. 步驟三回報 Git 發版 tag 指引。
> 注意:這是「正式發版」,會升版號並產正式簽章 APK。除非使用者明確指示不要自行 commit / push / 打 tag只回報指引。

View File

@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git fetch *)",
"Bash(git switch *)",
"Bash(grep -n 'is_spring_slot\\\\|protected $fillable\\\\|protected $casts\\\\|'\\\\''settings'\\\\''' app/Models/Machine/Machine.php)"
]
}
}

View File

@ -1,22 +0,0 @@
root = true
# 全專案預設UTF-8不含 BOM、LF、檔尾換行
# 重點charset = utf-8非 utf-8-bom可避免編輯器在 XML 開頭塞入 BOM
# 否則 aapt2 會報「Cannot read field "elmName" because "root" is null」導致打包失敗。
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{xml,java,kt,kts,gradle,pro}]
charset = utf-8
indent_style = space
indent_size = 4
# 二進位資源不套用規則
[*.{png,webp,jpg,jpeg,gif,ttf,otf,9.png}]
charset = unset
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset

View File

@ -24,8 +24,7 @@
- 禁止直接手改成品 `versionCode`/`versionName` 值。
- 版號管理透過 `verMajor`、`verMinor`、`verPatch` 變數。
- OTA 發版時,`versionCode` 必須嚴格遞增。
- 21.5 吋大螢幕現場版(如中國醫)修正預設以 `:app:assembleS9XYUDebug` 驗證。
- 10 吋一般現場版(如晟崴、通用)修正預設以 `:app:assembleS12XYUDebug` 驗證。
- 中國醫現場版 (`Cmuh`) 修正預設以 `:app:assembleCmuhS9XYUDebug` 驗證。
## When Rules Conflict

View File

@ -1,6 +1,5 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
// =========================================================================
@ -9,8 +8,8 @@ plugins {
// (verMajor) Flavor (S12XYU / S9XYU)
//
// =========================================================================
def verMinor = 2
def verPatch = 2
def verMinor = 1
def verPatch = 67
@ -30,13 +29,23 @@ android {
}
// =====================================================================
// Flavorhardware / IPC
// shopping_mode B014 flavor
// hardware minSdk COM port ComPort.getFilepath
// Flavorproject× hardware
// =====================================================================
flavorDimensions "hardware"
flavorDimensions "project", "hardware"
productFlavors {
General {
dimension "project"
}
Chengwai {
dimension "project"
}
Cmuh {
dimension "project"
}
S12XYU {
dimension "hardware"
}
@ -58,7 +67,8 @@ android {
//
// _安卓號_主板號_機器系列_專案名稱_螢幕大小_次版號_修補版號_環境
applicationVariants.all { variant ->
def hardware = variant.productFlavors[0].name // hardware dimension
def project = variant.productFlavors[0].name // project dimension
def hardware = variant.productFlavors[1].name // hardware dimension
def buildType = variant.buildType.name
// (verMajor)
@ -89,8 +99,8 @@ android {
def hwFormatted = hardware
.replaceAll(/^([A-Z])(\d+)([A-Z]+)([A-Z])$/, '$1_$2_$3_$4')
// app-S_12_XY_U-10_08_8_R-release.apk shopping_mode
output.outputFileName = "app-${hwFormatted}-${calculatedVersionName}-${buildType}.apk"
// app-S_12_XY_U_Chengwai-10_08_8_R-release.apk
output.outputFileName = "app-${hwFormatted}_${project}-${calculatedVersionName}-${buildType}.apk"
}
}
@ -101,10 +111,6 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '11'
}
buildFeatures {
viewBinding true
buildConfig true
@ -140,11 +146,4 @@ dependencies {
implementation 'io.netty:netty-codec-http:4.1.94.Final'
implementation 'io.netty:netty-handler:4.1.94.Final'
implementation 'com.google.code.gson:gson:2.10.1'
// Kotlin / MVVMbasic
implementation libs.kotlin.stdlib
implementation libs.lifecycle.viewmodel.ktx
implementation libs.lifecycle.livedata.ktx
implementation libs.lifecycle.runtime.ktx
implementation libs.brvah
}

View File

@ -0,0 +1,42 @@
package com.unibuy.smartdevice.ui;
import android.content.Context;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.MemberCardPickupDialog;
/**
* Chengwai (晟崴版) 銷售流程處理器
* 點選商品/購物車時直接進入員工卡刷卡取貨 Dialog (MemberCardPickupDialog)
*/
public class SaleFlowHandler {
protected final Context context;
protected final SaleFlowCallback callback;
protected final HandlerMain handlerMain;
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
this.context = context;
this.handlerMain = handlerMain;
this.callback = callback;
}
public void onSetupUI() {
// 晟崴版 UI 設定預設無動作
}
public void onSetupUI(android.view.View rootView) {
// 晟崴版 UI 設定預設無動作
}
public void onBuyRequested() {
new MemberCardPickupDialog(context, handlerMain).show();
}
public void onBarcodeReceived(String rawData) {
// 晟崴版不處理掃碼
}
public void onPickupRequested() {
// 晟崴版不處理取貨碼
}
}

View File

@ -81,6 +81,7 @@ public class BuyPickupCodeDialog extends DialogAbstract {
setButtonCancelText(Integer.valueOf(message));
break;
case SET_PRICE:
binding.textPrice.setText(String.valueOf(getTotalPrice()));
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM:
@ -129,6 +130,7 @@ public class BuyPickupCodeDialog extends DialogAbstract {
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
binding.textPrice.setText(String.valueOf(getTotalPrice()));
}
public int getTotalPrice() {

View File

@ -46,7 +46,6 @@ public class FontendPickupCodeDialog extends DialogAbstract {
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
SET_STATUS_TEXT(8),
SET_SCAN_INPUT(9),
;
private int option;
@ -152,11 +151,6 @@ public class FontendPickupCodeDialog extends DialogAbstract {
case SET_STATUS_TEXT:
setStatusText(message);
break;
case SET_SCAN_INPUT:
// 掃碼即時回饋把掃到的內容顯示在輸入框讓使用者確認有掃到
binding.editTextNumber.setText(message);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
break;
}
}
};
@ -180,18 +174,6 @@ public class FontendPickupCodeDialog extends DialogAbstract {
MyApp.getInstance().initialReportData();
setInputMode(true);
binding.btnModeKeypad.setOnClickListener(v -> {
setInputMode(true);
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
});
binding.btnModeScan.setOnClickListener(v -> {
setInputMode(false);
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
});
isChangeDispatch = true;
openBarcode = true;
httpAPI = new HttpAPI(getHandlerMain());
@ -296,23 +278,11 @@ public class FontendPickupCodeDialog extends DialogAbstract {
closeDialogCountdown.setCountdown(40);
});
// 立即開始聽掃描器原本延遲 5 導致對話框剛彈出時掃碼被整個丟掉 時好時壞無回饋
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
// 進入頁面時立即檢查取貨口 (使用貨道1作為代表)
// callback null表示檢查通過後不做額外動作僅在檢查失敗時顯示警告
// pickup_sheet Cmuh流程不做取貨口檢查維持原行為
if (!"pickup_sheet".equals(MyApp.getInstance().getShoppingMode())) {
checkSlotHaveProduct(1, 1, null);
}
}
private void setInputMode(boolean isKeyboard) {
binding.btnModeKeypad.setSelected(isKeyboard);
binding.btnModeScan.setSelected(!isKeyboard);
binding.linearNumpad.setVisibility(isKeyboard ? android.view.View.VISIBLE : android.view.View.GONE);
binding.imageViewScanHint.setVisibility(isKeyboard ? android.view.View.GONE : android.view.View.VISIBLE);
checkSlotHaveProduct(1, 1, null);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
@ -336,18 +306,21 @@ public class FontendPickupCodeDialog extends DialogAbstract {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<9; i++) { // 45 秒讀取窗口覆蓋 40 秒對話框存活時間
for (int i=0; i<7; i++) {
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {
String barCodeString = new String(buffer, 0, size).trim();
// 掃碼即時回饋先把掃到的內容顯示在輸入框再送出驗證
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
String barCodeString = new String(buffer, 0, size);
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+""+ Tools.randomReplace(barCodeString, 0.3)+""+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
if ("pickup_sheet".equals(MyApp.getInstance().getShoppingMode())) {
submitViaMemberVerification(barCodeString);
} else {
submitPickupCode(barCodeString);
try {
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(barCodeString);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
openBarcode = false;
@ -384,94 +357,25 @@ public class FontendPickupCodeDialog extends DialogAbstract {
}
public void changeOk() {
String number = binding.editTextNumber.getText().toString().trim();
// pickup_sheet Cmuh取貨碼走舊 memberVerification API維持原行為
if ("pickup_sheet".equals(MyApp.getInstance().getShoppingMode())) {
submitViaMemberVerification(number);
return;
}
// 長度檢查
if (number.isEmpty() || number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), "取貨碼長度不能少於 8 碼");
return;
}
submitPickupCode(number);
}
/** pickup_sheet 模式:取貨碼/條碼走舊 {@code httpAPI.memberVerification}(沿用 Cmuh 行為)。 */
private void submitViaMemberVerification(String code) {
try {
String number = binding.editTextNumber.getText().toString();
// 長度檢查
if (number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
return;
}
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(code);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
/**
* 取貨碼驗證改走 StarCloudAPI B660邏輯比照員工卡 B680
* 成功回傳 code_id 與要出貨的 slot_no據此建立 BuyList保存 code_id MQTT
* finalizepayment_type=6 取貨碼再進入取貨確認 出貨
*/
private void submitPickupCode(String code) {
// 防護取貨碼從屬取貨模組任一關閉即不開放呼叫 B660即使 UI 漏出入口也擋下
if (!(MyApp.getInstance().getDevSet().isPickupModule() && MyApp.getInstance().getDevSet().isPickupCode())) {
openBarcode = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "取貨碼功能未開放");
return;
}
// 送出前顯示驗證中避免使用者以為沒反應
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
.verifyPickupCode(code, new com.unibuy.smartdevice.external.StarCloudAPI.PickupVerificationCallback() {
@Override
public void onSuccess(String codeId, int slotNo) {
try {
SlotStructure slotProduct = MyApp.getInstance().getSlotData(SlotField.VMC.getField(), slotNo);
if (slotProduct == null) {
setStatusText(getContext().getString(R.string.product_not_found));
openBarcode = true;
return;
}
if (slotProduct.isLock()) {
getSrcHandlerMain().start("FontendPickupCodeDialog", "@" + getContext().getString(R.string.product_sales_suspended));
dialogCancel();
return;
}
if (slotProduct.getCount() == 0) {
getSrcHandlerMain().start("FontendPickupCodeDialog", "@" + getContext().getString(R.string.product_out_of_stock));
dialogCancel();
return;
}
MyApp.getInstance().getBuyList().clear();
MyApp.getInstance().getBuyList().add(
new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1));
MyApp.getInstance().getReportFlowInfoData().setFlowType(6); // 取貨碼 payment_type 6
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
MyApp.getInstance().getReportFlowInfoData().setTotalPrice(0); // 取貨碼為已預付/補出貨付款金額 0
MyApp.getInstance().getReportAccessCodeStructure().setAccessCodeId(codeId);
changeBuy(); // 開啟取貨確認 BuyPickupCodeDialog 出貨
} catch (LogsEmptyException e) {
setStatusText(getContext().getString(R.string.product_not_found));
openBarcode = true;
}
}
@Override
public void onFailure(String message) {
openBarcode = true;
setStatusText(message);
}
});
}
public void changeBuy() {
closeDialogCountdown.shutdown();
BuyPickupCodeDialog buyDialog = new BuyPickupCodeDialog(getContext(), getSrcHandlerMain());
@ -491,7 +395,7 @@ public class FontendPickupCodeDialog extends DialogAbstract {
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("取消 " + countdown + "");
binding.buttonCancel.setText(getContext().getString(R.string.return_to)+"("+countdown+")");//返回
}
public void setStatusText(String message) {

View File

@ -84,7 +84,7 @@ public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<
int price = machinePrice > 0? machinePrice: sellingPrice;
holder.binding.textPrice.setText("$"+price);
holder.binding.textCount.setText("出貨數量:" + count + "");
holder.binding.textCount.setText("x"+String.valueOf(count));
logs.info("field:" + buyProduct.getField());
}

View File

@ -36,7 +36,7 @@ import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* pickup_sheet 模式 Cmuh 中國醫版銷售流程
* Cmuh (中國醫版) 銷售流程處理器
* 負責
* 1. 綁定40秒內刷條碼按鈕 (scanbarcodebtn)
* 2. 連接 USB 掃描器背景讀取
@ -44,37 +44,49 @@ import javax.crypto.spec.SecretKeySpec;
* 4. 庫存核對與自動加入購物車
* 5. 顯示領藥與錯誤 Dialog
*/
public class PickupSheetSaleFlow extends SaleFlowHandler {
public class SaleFlowHandler {
private static final String ORDC_EMPTY = "ORDC_EMPTY";
private static final String MATERIAL_NOT_FOUND = "MATERIAL_NOT_FOUND";
private static final String STOCK_EMPTY = "STOCK_EMPTY";
private static final String STOCK_NOT_ENOUGH = "STOCK_NOT_ENOUGH";
protected final Context context;
protected final SaleFlowCallback callback;
protected final HandlerMain handlerMain;
private boolean openBarcode = true;
private boolean haveValuestate = false;
private long lastClickTime = 0;
public PickupSheetSaleFlow(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
super(context, handlerMain, callback);
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
this.context = context;
this.handlerMain = handlerMain;
this.callback = callback;
}
/**
* Activity onResume() 中會被呼叫此時尋找並綁定 VmcFragment 中的掃碼按鈕
*/
@Override
public void onSetupUI() {
onSetupUI(null);
}
public void onSetupUI(View rootView) {
if (context instanceof Activity) {
View btnScan = rootView != null ? rootView.findViewById(R.id.scanbarcodebtn) : ((Activity) context).findViewById(R.id.scanbarcodebtn);
if (btnScan != null) {
btnScan.setOnClickListener(new BtnQrCodeOnClick(handlerMain, context));
MyApp.getInstance().getLogs().info("✅ 成功在 PickupSheetSaleFlow 綁定掃碼按鈕!");
MyApp.getInstance().getLogs().info("✅ 成功在 Cmuh SaleFlowHandler 綁定掃碼按鈕!");
} else {
MyApp.getInstance().getLogs().info("❌ 在 PickupSheetSaleFlow 找不到掃碼按鈕 R.id.scanbarcodebtn");
MyApp.getInstance().getLogs().info("❌ 在 Cmuh SaleFlowHandler 找不到掃碼按鈕 R.id.scanbarcodebtn");
}
}
}
@Override
public void onBuyRequested() {
// 中國醫版沒有購物車付款此方法不執行
}
public void onBarcodeReceived(String rawData) {
// 若是經由其他方式傳入的 Barcode 可在此處直接處理
try {
@ -87,9 +99,8 @@ public class PickupSheetSaleFlow extends SaleFlowHandler {
}
/**
* 點選取貨按鈕開啟輸入取貨碼的 Dialogpickup_sheet 走舊 API 驗證 FontendPickupCodeDialog
* CMUH 點選取貨按鈕開啟輸入取貨碼的 Dialog
*/
@Override
public void onPickupRequested() {
new FontendPickupCodeDialog(context, handlerMain).show();
}
@ -118,7 +129,7 @@ public class PickupSheetSaleFlow extends SaleFlowHandler {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100); // 預讀清除緩衝
for (int i = 0; i < 40; i++) { // 總計約 40
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 1000); // 超時為 1
@ -205,7 +216,7 @@ public class PickupSheetSaleFlow extends SaleFlowHandler {
if (encryptedBytes.length % 16 != 0) {
throw new IllegalArgumentException("錯誤:加密數據長度不是 16 的倍數,可能需要填充");
}
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
@ -220,7 +231,7 @@ public class PickupSheetSaleFlow extends SaleFlowHandler {
String decryptedStr = MyApp.getInstance().getDecryptedData();
MyApp.getInstance().getLogs().info("decryptedStr=" + decryptedStr);
JSONObject jsonObject = new JSONObject(decryptedStr);
if (checkORDCDispatchNumber(decryptedStr)) {
return false;
@ -279,7 +290,7 @@ public class PickupSheetSaleFlow extends SaleFlowHandler {
JSONObject itemObject = itemsArray.getJSONObject(i);
String ordc = itemObject.optString("ORDC", "").trim();
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
// 重新初始化這些變數
MedicineName = "";
MedicineCount = 0;
@ -306,10 +317,10 @@ public class PickupSheetSaleFlow extends SaleFlowHandler {
for (int i = 0; i < itemsArray.length(); i++) {
JSONObject itemObject = itemsArray.getJSONObject(i);
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
if (requestQty <= medicineList.get(i).getMedicineCount()) {
haveValuestate = true;
if (requestQty > 1) {
class TempSlot {
int slotNo;

View File

@ -0,0 +1,263 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.databinding.DialogPickupCodeBuyBinding;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogPickupCodeBuyListAdpter;
import java.util.HashMap;
import java.util.Map;
public class BuyPickupCodeDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
SET_PRICE(3),
RESET_COUNTDOWM(4),
RESET_COUNTDOWM_TOAST(5),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getContext(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
if (MyApp.getInstance().getBuyList().size() <= 0) {
dialogCancel();
} else {
Option option = optionMap.get(commandCode);
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonCancelText(Integer.valueOf(message));
break;
case SET_PRICE:
binding.textPrice.setText(String.valueOf(getTotalPrice()));
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM:
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM_TOAST:
super.execute(context, commandCode, message);
closeDialogCountdown.setCountdown(40);
break;
}
}
}
};
}
private DialogPickupCodeBuyBinding binding;
private RecyclerDialogPickupCodeBuyListAdpter recyclerDialogPickupCodeBuyListAdpter;
private CloseDialogOnThreadHandler closeDialogCountdown;
private boolean isChangeDispatch;
public BuyPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
getLogs().info("open:" + getClass().getSimpleName());
}
@Override
protected void onCreate(Context context) {
binding = DialogPickupCodeBuyBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
isChangeDispatch = true;
this.recyclerDialogPickupCodeBuyListAdpter = new RecyclerDialogPickupCodeBuyListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogPickupCodeBuyListAdpter);
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
MyApp.getInstance().getReportFlowInfoData().setFlowType(6);
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
this.binding.buttonGetProduct.setOnClickListener(v -> gotoGetProduct());
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
binding.textPrice.setText(String.valueOf(getTotalPrice()));
}
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
totalPrice += buy.getFullPrice();
}
return totalPrice;
}
public void gotoGetProduct() {
if (isChangeDispatch) {
closeDialogCountdown.shutdown();
isChangeDispatch = false;
changeDialog(DispatchDialog.class);
cancel();
}
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
cancel();
}
public void cancelOnThreadCountdown() {
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
getTools().dialogClose();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("取消("+countdown+")");
}
public void gotoDialog() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
if (flowType == 5) {
changeCheckout();
} else {
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
if (MyApp.getInstance().getDevSet().isInvoice()) {
changeDialog(InvoiceBarcodeDialog.class);
} else {
changeCheckout();
}
} else {
if (MyApp.getInstance().getDevSet().isInvoice()) {
BuyStructure buyData = MyApp.getInstance().getBuyList().get(0);
try {
SlotField slotField = SlotField.getSlotField(buyData.getField());
if (slotField.isInvoice()) {
changeDialog(InvoiceBarcodeDialog.class);
} else {
changeCheckout();
}
} catch (LogsEmptyException e) {
changeDialog(InvoiceBarcodeDialog.class);
}
} else {
changeCheckout();
}
}
}
}
public void changeCheckout() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
switch (flowType) {
case 1:
changeDialog(CheckoutNfcCardDialog.class);
break;
case 2:
changeDialog(CheckoutNfcRechargeDialog.class);
break;
case 3:
changeDialog(CheckoutEsunPayDialog.class);
break;
case 4:
changeDialog(CheckoutNfcPhoneDialog.class);
break;
case 5:
changeDialog(CheckoutAccessCodeDialog.class);
break;
case 9:
changeDialog(CheckoutCashDialog.class);
break;
case 70:
changeDialog(CheckoutLinePayDialog.class);
break;
}
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
}

View File

@ -0,0 +1,429 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.databinding.DialogAccessCodeBinding;
import com.unibuy.smartdevice.databinding.DialogPickupCodeFontendBinding;
import com.unibuy.smartdevice.devices.DeviceType;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.devices.UsbDev;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.exception.LogsIOException;
import com.unibuy.smartdevice.exception.LogsParseException;
import com.unibuy.smartdevice.exception.LogsSettingEmptyException;
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
import com.unibuy.smartdevice.external.HttpAPI;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.HandlerMainScheduler;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.FontendActivity;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class FontendPickupCodeDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
INVOICE_SUCCESSFUL(4),
INVOICE_FAILED(5),
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
SET_STATUS_TEXT(8),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
Option option = optionMap.get(commandCode);
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonCancelText(Integer.valueOf(message));
break;
case INVOICE_SUCCESSFUL:
MyApp.getInstance().getInvoiceData().setCarrier(message);
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("carrier:" + MyApp.getInstance().getInvoiceData().getCarrier());
break;
case INVOICE_FAILED:
break;
case CHANGE_DISPATCH:
reportAccessCodeData();//上傳通行碼資訊到後台系統
changeDispatch();
break;
case CHANGE_BUY:
try {
SlotStructure slotProduct = MyApp.getInstance().getSlotDataByPid(message);
SlotField slotField = SlotField.getSlotField(slotProduct.getField());
switch (slotField) {
case VMC:
if (slotProduct.isLock()) {
getSrcHandlerMain().start("FontendPickupCodeDialog", "@商品暫停販售");
dialogCancel();
} else if (slotProduct.getCount() == 0) {
getSrcHandlerMain().start("FontendPickupCodeDialog", "@商品無存貨");
dialogCancel();
}
BuyStructure buyProduct = new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1);
MyApp.getInstance().getBuyList().add(buyProduct);
changeBuy();
break;
case ELECTRIC:
if (slotProduct.isLock())
MyApp.getInstance().getSlotDataByPid(message).setLock(false);
getSrcHandlerMain().start("FontendPickupCodeDialog", "@解鎖" + slotProduct.getProduct().getProductName());
getSrcHandlerMain().start("FontendPickupCodeDialog", FontendActivity.Option.RESET_DATA.getOption(), "reset data");
dialogCancel();
break;
}
} catch (LogsEmptyException e) {
setStatusText("找不到商品");
}
break;
case SET_STATUS_TEXT:
setStatusText(message);
break;
}
}
};
}
private DialogPickupCodeFontendBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private boolean isChangeDispatch;
private boolean openBarcode;
private HttpAPI httpAPI;
public FontendPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
}
@Override
protected void onCreate(Context context) {
binding = DialogPickupCodeFontendBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
MyApp.getInstance().initialReportData();
isChangeDispatch = true;
openBarcode = true;
httpAPI = new HttpAPI(getHandlerMain());
binding.buttonFinish.setOnClickListener(v -> changeOk());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
binding.buttonR1.setOnClickListener(v -> {
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text.substring(0, text.length()-1));
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.buttonC1.setOnClickListener(v -> {
binding.editTextNumber.setText("");
closeDialogCountdown.setCountdown(40);
});
binding.button10.setOnClickListener(v -> {
String btnName = binding.button10.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button11.setOnClickListener(v -> {
String btnName = binding.button11.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button12.setOnClickListener(v -> {
String btnName = binding.button12.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button13.setOnClickListener(v -> {
String btnName = binding.button13.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button14.setOnClickListener(v -> {
String btnName = binding.button14.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button15.setOnClickListener(v -> {
String btnName = binding.button15.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button16.setOnClickListener(v -> {
String btnName = binding.button16.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button17.setOnClickListener(v -> {
String btnName = binding.button17.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button18.setOnClickListener(v -> {
String btnName = binding.button18.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button19.setOnClickListener(v -> {
String btnName = binding.button19.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
public ReadBarCodeOnScheduler(HandlerMain handlerMain) {
super(handlerMain);
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void execute(HandlerMain handlerMain) {
try {
int totalPrice = 0;
for (BuyStructure buy : MyApp.getInstance().getBuyList()) {
totalPrice += buy.getFullPrice();
}
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<7; i++) {
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {
String barCodeString = new String(buffer, 0, size);
getHandlerMain().start(getLogs().getClassName(), "收到條碼:"+ Tools.randomReplace(barCodeString, 0.3)+",送出交易");
try {
// TODO: [新後台改接] 待未來新後台啟用後此處取貨碼驗證需改串接新的 StarCloudAPI B660 介面
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(barCodeString);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
openBarcode = false;
}
}
}
usbDev.closePort();
} catch (LogsUnsupportedOperationException | LogsIOException | LogsEmptyException e) {
getLogs().warning(e);
}
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
totalPrice += buy.getFullPrice();
}
return totalPrice;
}
public void changeDispatch() {
if (isChangeDispatch) {
closeDialogCountdown.shutdown();
isChangeDispatch = false;
changeDialog(DispatchDialog.class);
}
}
public void changeOk() {
try {
String number = binding.editTextNumber.getText().toString();
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
public void changeBuy() {
closeDialogCountdown.shutdown();
BuyPickupCodeDialog buyDialog = new BuyPickupCodeDialog(getContext(), getSrcHandlerMain());
buyDialog.show();
cancel();
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void cancelOnThreadCountdown() {
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("返回("+countdown+")");
}
public void setStatusText(String message) {
binding.textStatus.setText(message);
}
public void reportAccessCodeData() {
try {
MyApp.getInstance().getReportAccessCodeStructure().setField(MyApp.getInstance().getReportAccessCodeStructure().getField());
MyApp.getInstance().getReportAccessCodeStructure().setSlot(MyApp.getInstance().getBuyList().get(0).getSlot());
MyApp.getInstance().getReportAccessCodeStructure().setProductId(MyApp.getInstance().getReportAccessCodeStructure().getProductId());
httpAPI.reportAccessCode(MyApp.getInstance().getReportAccessCodeStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
public void updatePickupCodeData() {
try {
String number = binding.editTextNumber.getText().toString();
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
handlerMain.start(getClass().getSimpleName(), CheckoutCashDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
}

View File

@ -0,0 +1,193 @@
package com.unibuy.smartdevice.ui.recycler;
import android.content.Context;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.unibuy.smartdevice.databinding.RecyclerDialogBuyListBinding;
import com.unibuy.smartdevice.databinding.RecyclerDialogPickupCodeBuyListBinding;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
import com.unibuy.smartdevice.ui.tools.ImageGlide;
import java.util.List;
public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<RecyclerDialogPickupCodeBuyListAdpter.ViewHolder> {
private Logs logs;
private Context context;
private final List<BuyStructure> buyList;
private HandlerMain handlerMain;
public RecyclerDialogPickupCodeBuyListAdpter(Context context, List<BuyStructure> buyList, HandlerMain handlerMain) {
this.logs = new Logs(this.getClass());
this.context = context;
this.buyList = buyList;
this.handlerMain = handlerMain;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
RecyclerDialogPickupCodeBuyListBinding binding;
public ViewHolder(@NonNull RecyclerDialogPickupCodeBuyListBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 使用 ViewBinding 來綁定佈局
RecyclerDialogPickupCodeBuyListBinding binding = RecyclerDialogPickupCodeBuyListBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
logs.info("position:" + position);
BuyStructure buyProduct = buyList.get(position);
holder.binding.textPosition.setText(String.valueOf(position + 1));
String productID = buyProduct.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
} else {
holder.binding.textProductName.setText("點我選擇商品");
}
// holder.binding.textDelete.setOnClickListener(new textDeleteOnClickListener(position));
String urlString = buyProduct.getProduct().getProductImg();
try {
new ImageGlide(context).fileload(urlString, holder.binding.imageProductPicture);
} catch (LogsEmptyException e) {
this.logs.warning(e);
}
int machinePrice = buyProduct.getProduct().getMachinePrice();
int sellingPrice = buyProduct.getProduct().getSellingPrice();
int memberPrice = buyProduct.getProduct().getMemberPrice();
int slotUpperLimit = buyProduct.getProduct().getSlotUpperLimit();
int count = buyProduct.getCount();
int price = machinePrice > 0? machinePrice: sellingPrice;
holder.binding.textPrice.setText("$"+price);
holder.binding.textCount.setText("x"+String.valueOf(count));
logs.info("field:" + buyProduct.getField());
}
public class addition1OnClickListener implements View.OnClickListener {
private Context context;
private RecyclerDialogBuyListBinding binding;
private int position;
public addition1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
this.context = context;
this.binding = binding;
this.position = position;
}
@Override
public void onClick(View v) {
BuyStructure buyProduct = buyList.get(position);
int count = buyProduct.getCount();
count++;
buyList.get(position).setCount(count);
binding.textCount.setText("x"+String.valueOf(count));
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(), "total price");
}
}
public class subtraction1OnClickListener implements View.OnClickListener {
private Context context;
private RecyclerDialogBuyListBinding binding;
private int position;
public subtraction1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
this.context = context;
this.binding = binding;
this.position = position;
}
@Override
public void onClick(View v) {
BuyStructure buyProduct = buyList.get(position);
int count = buyProduct.getCount();
count--;
if (count <= 0) {
buyList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, buyList.size() - position);
handlerMain.start(getClass().getSimpleName(),"刪除一筆資料");
} else {
buyList.get(position).setCount(count);
binding.textCount.setText("x"+String.valueOf(count));
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(),"total price");
}
}
}
public class editCountOnEditorActionListener implements TextView.OnEditorActionListener{
private ViewHolder holder;
private int position;
public editCountOnEditorActionListener(ViewHolder holder, int position) {
this.holder = holder;
this.position = position;
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEND) {
int count = Integer.valueOf(v.getText().toString());
buyList.get(position).setCount(count);
notifyItemChanged(position);
return true;
}
return false;
}
}
public class textDeleteOnClickListener implements View.OnClickListener{
private int position;
public textDeleteOnClickListener(int position) {
this.position = position;
}
@Override
public void onClick(View v) {
delProductData(position);
}
}
@Override
public int getItemCount() {
return buyList.size();
}
public void addProductData(BuyStructure buyProduct) {
buyList.add(buyProduct);
notifyItemInserted(buyList.size() -1);
}
public void delProductData(int position) {
buyList.remove(position);
notifyItemRemoved(position);
}
}

View File

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray"
>
<LinearLayout
android:id="@+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/red"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textVersion"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="20250108ver1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textThreadCount"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="TC:0"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnswitchlanTW"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="中"
android:visibility="gone"
android:textSize="30sp"
android:textColor="@color/white"
android:textStyle="bold"
/>
<TextView
android:id="@+id/btnswitchlanEn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="En"
android:visibility="gone"
android:textSize="30sp"
android:textColor="@color/white"
android:textStyle="bold"
/>
<TextView
android:id="@+id/btnswitchlanJa"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="日"
android:visibility="gone"
android:textSize="30sp"
android:textColor="@color/white"
android:textStyle="bold"
/>
<TextView
android:id="@+id/textTemperature"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="0 ℃"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textTime"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="1911/01/01 01:01:01"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textNetwork"
android:layout_width="150dp"
android:layout_height="match_parent"
android:gravity="center_vertical|end"
android:text="@string/no_internet"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="60dp"
android:layout_marginBottom="2dp"
android:background="@color/black"
app:layout_constraintDimensionRatio="18:21"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.media3.ui.PlayerView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_subtitle_button="true"
app:use_artwork="true"
app:use_controller="false"
app:resize_mode="fit"
app:surface_type="texture_view" />
<ImageView
android:id="@+id/imageAd"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="gone"
/>
</RelativeLayout>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout" />
<LinearLayout
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonVmc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/drinksale"
app:cornerRadius="12dp"
android:backgroundTint="@android:color/transparent"
app:strokeColor="@color/gray"
android:textSize="38sp"
app:strokeWidth="2dp"
android:visibility="gone"
android:layout_margin="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonElectric"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/board_games"
app:cornerRadius="12dp"
android:backgroundTint="@android:color/transparent"
app:strokeColor="@color/gray"
android:textSize="38sp"
android:visibility="gone"
app:strokeWidth="2dp"
android:layout_margin="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonOther"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:text="@string/other"
app:cornerRadius="12dp"
android:backgroundTint="@android:color/transparent"
app:strokeColor="@color/gray"
android:textSize="38sp"
app:strokeWidth="2dp"
android:layout_margin="4dp"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/floatButtonPickupCode"
android:layout_width="50dp"
android:layout_height="150dp"
android:text="取\n貨\n碼"
android:textSize="28sp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="80dp"
android:backgroundTint="@color/gray"
android:textColor="@color/black"
android:alpha="0.8"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/floatButtonShoppingCart"
app:layout_constraintEnd_toEndOf="@+id/bottom_navigation"
android:textStyle="bold"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatButtonShoppingCart"
android:layout_width="58dp"
android:layout_height="51dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginEnd="24dp"
android:layout_marginBottom="80dp"
android:contentDescription="TODO"
android:visibility="gone"
app:backgroundTint="@color/gray"
app:layout_constraintBottom_toBottomOf="@+id/fragment_container"
app:layout_constraintEnd_toEndOf="@+id/bottom_navigation"
app:maxImageSize="60dp"
app:srcCompat="@drawable/baseline_add_shopping_cart_24" />
<Button
android:id="@+id/btnTopRight"
android:layout_width="200dp"
android:layout_height="100dp"
android:text="進入後台"
android:textSize="24sp"
android:backgroundTint="@color/red"
android:textColor="@android:color/white"
android:alpha="1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:layout_marginTop="60dp"
android:layout_marginEnd="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -133,7 +133,7 @@
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/error"
android:background="@color/red"
android:gravity="center"
android:text="出貨明細:"
android:textColor="@color/white"

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/light_gray">
<LinearLayout
android:id="@+id/linearLayout4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@color/light_gray"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/red"
android:gravity="center"
android:text="購物車"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="1dp"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.15"
android:gravity="center|end"
android:text="合計金額:"
android:textColor="@color/red"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textPrice"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="5dp"
android:layout_weight="0.85"
android:gravity="center|start"
android:text="0"
android:textColor="@color/red"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerDialogBuyList"
android:layout_width="match_parent"
android:layout_height="750dp"
/>
<LinearLayout
android:id="@+id/layoutMemberTransactions"
android:layout_width="match_parent"
android:layout_height="340dp"
android:layout_gravity="center"
android:visibility="gone"
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/buttonCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:text="取消(40)"
android:textSize="20sp"
android:textColor="@color/black" />
<Button
android:id="@+id/buttonGetProduct"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:text="取貨"
android:textColor="@color/black"
android:textSize="20sp"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/light_gray">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/light_gray"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/red"
android:gravity="center"
android:text="取貨碼"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="1dp"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/textStatus"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:gravity="center"
android:text="輸入取貨碼 或 掃描QRCode"
android:textColor="@color/red"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearMark1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="20dp"
android:background="@color/white"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<EditText
android:id="@+id/editTextNumber"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:ems="10"
android:textSize="20sp"
android:hint="取貨碼"
android:gravity="center"
android:inputType="number" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/button10"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="0" />
<Button
android:id="@+id/button11"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="1" />
<Button
android:id="@+id/button12"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="2" />
<Button
android:id="@+id/button13"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="3" />
<Button
android:id="@+id/button14"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="4" />
<Button
android:id="@+id/buttonR1"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/light_gray"
android:text="←" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/button15"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="5" />
<Button
android:id="@+id/button16"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="6" />
<Button
android:id="@+id/button17"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="7" />
<Button
android:id="@+id/button18"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="8" />
<Button
android:id="@+id/button19"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:text="9" />
<Button
android:id="@+id/buttonC1"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/light_gray"
android:text="C" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/buttonCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:text="返回(40)"
android:textSize="20sp"
android:textColor="@color/black" />
<Button
android:id="@+id/buttonFinish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:text="輸入完成"
android:textSize="20sp"
android:textColor="@color/black" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".ui.devs.vmc.VmcFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerBuy"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="500dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/scanbarcodebtn"
android:layout_width="400dp"
android:layout_height="400dp"
android:layout_marginTop="-50dp"
android:padding="10dp"
android:text="掃描單據\n按鈕"
android:textColor="@android:color/white"
android:textSize="70sp"
app:strokeColor="@color/gray"
app:strokeWidth="5dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="1dp"
android:paddingBottom="1dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="1dp"
android:background="@color/white"
android:orientation="horizontal"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textPosition"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="5dp"
android:layout_marginEnd="2dp"
android:layout_weight="0.3"
android:gravity="center"
android:text="0"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="visible" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.5"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<ImageView
android:id="@+id/imageProductPicture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/default_product"
app:layout_constraintDimensionRatio="16:9" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@color/white"
android:gravity="center"
android:orientation="vertical"
tools:layout_editor_absoluteY="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@color/white"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textProductName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="商品名稱"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@color/white"
android:orientation="horizontal">
<TextView
android:id="@+id/textPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="$0"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textCount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="x0"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,42 @@
package com.unibuy.smartdevice.ui;
import android.content.Context;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
/**
* General (通用版) 銷售流程處理器
* 點選商品/購物車時彈出標準付款 Dialog (BuyDialog)
*/
public class SaleFlowHandler {
protected final Context context;
protected final SaleFlowCallback callback;
protected final HandlerMain handlerMain;
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
this.context = context;
this.handlerMain = handlerMain;
this.callback = callback;
}
public void onSetupUI() {
// 通用版 UI 設定預設無動作
}
public void onSetupUI(android.view.View rootView) {
// 通用版 UI 設定預設無動作
}
public void onBuyRequested() {
new BuyDialog(context, handlerMain).show();
}
public void onBarcodeReceived(String rawData) {
// 通用版不處理掃碼
}
public void onPickupRequested() {
// 通用版不處理取貨碼
}
}

View File

@ -0,0 +1,264 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.DialogPickupCodeBuyBinding;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogPickupCodeBuyListAdpter;
import java.util.HashMap;
import java.util.Map;
public class BuyPickupCodeDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
SET_PRICE(3),
RESET_COUNTDOWM(4),
RESET_COUNTDOWM_TOAST(5),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getContext(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
if (MyApp.getInstance().getBuyList().size() <= 0) {
dialogCancel();
} else {
Option option = optionMap.get(commandCode);
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonCancelText(Integer.valueOf(message));
break;
case SET_PRICE:
binding.textPrice.setText(String.valueOf(getTotalPrice()));
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM:
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM_TOAST:
super.execute(context, commandCode, message);
closeDialogCountdown.setCountdown(40);
break;
}
}
}
};
}
private DialogPickupCodeBuyBinding binding;
private RecyclerDialogPickupCodeBuyListAdpter recyclerDialogPickupCodeBuyListAdpter;
private CloseDialogOnThreadHandler closeDialogCountdown;
private boolean isChangeDispatch;
public BuyPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
getLogs().info("open:" + getClass().getSimpleName());
}
@Override
protected void onCreate(Context context) {
binding = DialogPickupCodeBuyBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
isChangeDispatch = true;
this.recyclerDialogPickupCodeBuyListAdpter = new RecyclerDialogPickupCodeBuyListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogPickupCodeBuyListAdpter);
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
MyApp.getInstance().getReportFlowInfoData().setFlowType(6);
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
this.binding.buttonGetProduct.setOnClickListener(v -> gotoGetProduct());
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
binding.textPrice.setText(String.valueOf(getTotalPrice()));
}
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
totalPrice += buy.getFullPrice();
}
return totalPrice;
}
public void gotoGetProduct() {
if (isChangeDispatch) {
closeDialogCountdown.shutdown();
isChangeDispatch = false;
changeDialog(DispatchDialog.class);
cancel();
}
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
cancel();
}
public void cancelOnThreadCountdown() {
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
getTools().dialogClose();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText(getContext().getString(R.string.cancel)+"("+countdown+")");
}
public void gotoDialog() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
if (flowType == 5) {
changeCheckout();
} else {
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
if (MyApp.getInstance().getDevSet().isInvoice()) {
changeDialog(InvoiceBarcodeDialog.class);
} else {
changeCheckout();
}
} else {
if (MyApp.getInstance().getDevSet().isInvoice()) {
BuyStructure buyData = MyApp.getInstance().getBuyList().get(0);
try {
SlotField slotField = SlotField.getSlotField(buyData.getField());
if (slotField.isInvoice()) {
changeDialog(InvoiceBarcodeDialog.class);
} else {
changeCheckout();
}
} catch (LogsEmptyException e) {
changeDialog(InvoiceBarcodeDialog.class);
}
} else {
changeCheckout();
}
}
}
}
public void changeCheckout() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
switch (flowType) {
case 1:
changeDialog(CheckoutNfcCardDialog.class);
break;
case 2:
changeDialog(CheckoutNfcRechargeDialog.class);
break;
case 3:
changeDialog(CheckoutEsunPayDialog.class);
break;
case 4:
changeDialog(CheckoutNfcPhoneDialog.class);
break;
case 5:
changeDialog(CheckoutAccessCodeDialog.class);
break;
case 9:
changeDialog(CashPayDialog.class);
break;
case 70:
changeDialog(CheckoutLinePayDialog.class);
break;
}
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
}

View File

@ -0,0 +1,591 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.DialogAccessCodeBinding;
import com.unibuy.smartdevice.databinding.DialogPickupCodeFontendBinding;
import com.unibuy.smartdevice.devices.DeviceType;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.devices.UsbDev;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.exception.LogsIOException;
import com.unibuy.smartdevice.exception.LogsParseException;
import com.unibuy.smartdevice.exception.LogsSettingEmptyException;
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
import com.unibuy.smartdevice.external.HttpAPI;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.HandlerMainScheduler;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.FontendActivity;
import android.app.AlertDialog;
import android.os.Handler;
import android.os.Looper;
import com.unibuy.smartdevice.controller.DevController;
import com.unibuy.smartdevice.devices.PortTools;
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class FontendPickupCodeDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
INVOICE_SUCCESSFUL(4),
INVOICE_FAILED(5),
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
SET_STATUS_TEXT(8),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
Option option = optionMap.get(commandCode);
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonCancelText(Integer.valueOf(message));
break;
case INVOICE_SUCCESSFUL:
MyApp.getInstance().getInvoiceData().setCarrier(message);
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("carrier:" + MyApp.getInstance().getInvoiceData().getCarrier());
break;
case INVOICE_FAILED:
break;
case CHANGE_DISPATCH:
//reportAccessCodeData();//上傳通行碼資訊到後台系統
changeDispatch();
break;
case CHANGE_BUY:
try {
getLogs().info("CHANGE_BUY triggered with message: " + message);
SlotStructure slotProduct = MyApp.getInstance().getSlotDataByPid(message);
if (slotProduct == null) {
setStatusText(getContext().getString(R.string.product_not_found));
return;
}
getLogs().info("Slot found: " + slotProduct.getSlot() + ", Field: " + slotProduct.getField());
SlotField slotField = SlotField.getSlotField(slotProduct.getField());
switch (slotField) {
case VMC:
if (slotProduct.isLock()) {
getSrcHandlerMain().start("FontendPickupCodeDialog", "@"+getContext().getString(R.string.product_sales_suspended));//商品暫停販售
dialogCancel();
} else if (slotProduct.getCount() == 0) {
getSrcHandlerMain().start("FontendPickupCodeDialog", "@"+getContext().getString(R.string.product_out_of_stock));//商品無存貨
dialogCancel();
} else {
// 取貨口檢查已在 onCreate 執行這裡直接進行
BuyStructure buyProduct = new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1);
MyApp.getInstance().getBuyList().add(buyProduct);
changeBuy();
}
break;
case ELECTRIC:
if (slotProduct.isLock())
MyApp.getInstance().getSlotDataByPid(message).setLock(false);
getSrcHandlerMain().start("FontendPickupCodeDialog", "@"+getContext().getString(R.string.unlock) + slotProduct.getProduct().getProductName());//解鎖
getSrcHandlerMain().start("FontendPickupCodeDialog", FontendActivity.Option.RESET_DATA.getOption(), "reset data");
dialogCancel();
break;
}
} catch (LogsEmptyException e) {
setStatusText(getContext().getString(R.string.product_not_found));//找不到商品
} catch (Exception e) {
getLogs().info("Error in CHANGE_BUY: " + e.getMessage());
e.printStackTrace();
// 發生未知錯誤時嘗試繼續或顯示錯誤
setStatusText("System Error: " + e.getMessage());
}
break;
case SET_STATUS_TEXT:
setStatusText(message);
break;
}
}
};
}
private DialogPickupCodeFontendBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private boolean isChangeDispatch;
private boolean openBarcode;
private HttpAPI httpAPI;
private boolean haveThings, doorError;
public FontendPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
}
@Override
protected void onCreate(Context context) {
binding = DialogPickupCodeFontendBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
MyApp.getInstance().initialReportData();
isChangeDispatch = true;
openBarcode = true;
httpAPI = new HttpAPI(getHandlerMain());
binding.buttonFinish.setOnClickListener(v -> changeOk());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
binding.buttonR1.setOnClickListener(v -> {
String text = binding.editTextNumber.getText().toString();
if (text.length() > 0) { // 避免長度為 0 崩潰
binding.editTextNumber.setText(text.substring(0, text.length() - 1));
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
}
closeDialogCountdown.setCountdown(40);
});
binding.buttonC1.setOnClickListener(v -> {
binding.editTextNumber.setText("");
closeDialogCountdown.setCountdown(40);
});
binding.button10.setOnClickListener(v -> {
String btnName = binding.button10.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button11.setOnClickListener(v -> {
String btnName = binding.button11.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button12.setOnClickListener(v -> {
String btnName = binding.button12.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button13.setOnClickListener(v -> {
String btnName = binding.button13.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button14.setOnClickListener(v -> {
String btnName = binding.button14.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button15.setOnClickListener(v -> {
String btnName = binding.button15.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button16.setOnClickListener(v -> {
String btnName = binding.button16.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button17.setOnClickListener(v -> {
String btnName = binding.button17.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button18.setOnClickListener(v -> {
String btnName = binding.button18.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
binding.button19.setOnClickListener(v -> {
String btnName = binding.button19.getText().toString();
String text = binding.editTextNumber.getText().toString();
binding.editTextNumber.setText(text+btnName);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
closeDialogCountdown.setCountdown(40);
});
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
// 進入頁面時立即檢查取貨口 (使用貨道1作為代表)
// callback null表示檢查通過後不做額外動作僅在檢查失敗時顯示警告
checkSlotHaveProduct(1, 1, null);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
public ReadBarCodeOnScheduler(HandlerMain handlerMain) {
super(handlerMain);
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void execute(HandlerMain handlerMain) {
try {
int totalPrice = 0;
for (BuyStructure buy : MyApp.getInstance().getBuyList()) {
totalPrice += buy.getFullPrice();
}
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<7; i++) {
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {
String barCodeString = new String(buffer, 0, size);
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+""+ Tools.randomReplace(barCodeString, 0.3)+""+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
try {
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(barCodeString);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
openBarcode = false;
}
}
}
usbDev.closePort();
} catch (LogsUnsupportedOperationException | LogsIOException | LogsEmptyException e) {
getLogs().warning(e);
}
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
totalPrice += buy.getFullPrice();
}
return totalPrice;
}
public void changeDispatch() {
if (isChangeDispatch) {
closeDialogCountdown.shutdown();
isChangeDispatch = false;
changeDialog(DispatchDialog.class);
}
}
public void changeOk() {
try {
String number = binding.editTextNumber.getText().toString();
// 長度檢查
if (number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
return;
}
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
public void changeBuy() {
closeDialogCountdown.shutdown();
BuyPickupCodeDialog buyDialog = new BuyPickupCodeDialog(getContext(), getSrcHandlerMain());
buyDialog.show();
cancel();
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void cancelOnThreadCountdown() {
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText(getContext().getString(R.string.return_to)+"("+countdown+")");//返回
}
public void setStatusText(String message) {
binding.textStatus.setText(message);
}
public void reportAccessCodeData() {
try {
MyApp.getInstance().getReportAccessCodeStructure().setField(MyApp.getInstance().getReportAccessCodeStructure().getField());
MyApp.getInstance().getReportAccessCodeStructure().setSlot(MyApp.getInstance().getBuyList().get(0).getSlot());
MyApp.getInstance().getReportAccessCodeStructure().setProductId(MyApp.getInstance().getReportAccessCodeStructure().getProductId());
httpAPI.reportAccessCode(MyApp.getInstance().getReportAccessCodeStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
public void updatePickupCodeData() {
try {
String number = binding.editTextNumber.getText().toString();
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
handlerMain.start(getClass().getSimpleName(), CashPayDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
public void checkSlotHaveProduct(int slotnum, int slotcount, Runnable onSafe) {
haveThings = false;
doorError = false;
// 使用 AtomicBoolean 防止重複執行 (Timeout vs Callback)
// 預設為 false只要有一方執行過就設為 true避免重複觸發
java.util.concurrent.atomic.AtomicBoolean isHandled = new java.util.concurrent.atomic.AtomicBoolean(false);
// 設定 3000ms 超時機制如果機器沒反應視為正常並繼續
// 增加這段是因為如果 Slot 為空VMC 可能不會回傳任何資料導致程式卡住
// 但如果 onSafe null (代表僅監控不阻塞流程)則不啟動超時以免太快判定為安全而蓋過隨後回傳的 Error
if (onSafe != null) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!isHandled.getAndSet(true)) {
getLogs().info("VMC Check Slot Timeout - Approving Safe (Timeout Safeguard)");
onSafe.run();
}
}, 3000);
}
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlotPre(slotnum,slotcount));
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slotnum));
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(getHandlerMain(), onSafe, isHandled));
}
public class DevCheckSlotByVMC extends DevController.ReadBufferOnScheduler {
private Runnable onSafe;
private java.util.concurrent.atomic.AtomicBoolean isHandled;
public DevCheckSlotByVMC(HandlerMain handlerMain, Runnable onSafe, java.util.concurrent.atomic.AtomicBoolean isHandled) {
super(handlerMain);
this.onSafe = onSafe;
this.isHandled = isHandled;
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected Class<?> setCls() {
return getClass();
}
@Override
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
// 每次檢查前先重置門狀態旗標
doorError = false;
for (byte[] buffer: devController.getReadBufferByMax()) {
if (buffer[0] == 0x02) {
getLogs().info("XinYuan Read:" + PortTools.showHex(buffer));
if (buffer[1] == 0x05) {
haveThings = true;
break;
}else if (buffer[1] == 0x06 || buffer[1] == 0x09) {
// 0x06取貨口門沒關上
// 0x09微波爐取貨口門關閉錯誤
getLogs().info("檢測到取貨口門未正常關閉,狀態碼=" + String.format("%02X", buffer[1]));
doorError = true;
break;
}
}
}
if(haveThings){
// 防止 Timeout 後又執行這裡或者重複執行
if (isHandled.getAndSet(true)) return;
// 使用主執行緒顯示 AlertDialog
new Handler(Looper.getMainLooper()).post(() -> {
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.abnormal_reminder)
.setMessage(R.string.there_is_something_at_the_pickup_port)
.setCancelable(false)
.setPositiveButton(R.string.i_knew, (d, which) -> {
d.dismiss();
// 如果有東西可能需要重置或關閉
FontendPickupCodeDialog.this.dialogCancel();
})
.create();
dialog.show();
// 加入 10 秒後自動觸發點擊確定
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (dialog.isShowing()) {
dialog.dismiss();
FontendPickupCodeDialog.this.dialogCancel();
}
}, 10_000); // 10 10000 毫秒
});
} else if (doorError) {
if (isHandled.getAndSet(true)) return;
// 取貨口門未關閉提示使用者並阻止後續購買流程
handlerMain.start("FontendPickupCodeDialog", "取貨口門未正常關閉,請先確認門已關好");
// 使用主執行緒顯示 AlertDialog
new Handler(Looper.getMainLooper()).post(() -> {
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.abnormal_reminder)
.setMessage("取貨口門未正常關閉,請先確認門已關好。")
.setCancelable(false)
.setPositiveButton(R.string.i_knew, (d, which) -> {
d.dismiss();
FontendPickupCodeDialog.this.dialogCancel();
})
.create();
dialog.show();
// 加入 10 秒後自動觸發點擊確定
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (dialog.isShowing()) {
dialog.dismiss();
FontendPickupCodeDialog.this.dialogCancel();
}
}, 10_000); // 10 10000 毫秒
});
} else {
// 沒有其他狀況執行後續動作
// 注意這裡假設 VMC 回傳就代表檢測完成如果 VMC 會分段回傳可能需要更嚴謹的判斷
// 但依照原邏輯只要沒 error 就算 pass
if (!isHandled.getAndSet(true)) {
if (onSafe != null) {
new Handler(Looper.getMainLooper()).post(onSafe);
}
}
}
}
}
}

View File

@ -0,0 +1,194 @@
package com.unibuy.smartdevice.ui.recycler;
import android.content.Context;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.RecyclerDialogBuyListBinding;
import com.unibuy.smartdevice.databinding.RecyclerDialogPickupCodeBuyListBinding;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
import com.unibuy.smartdevice.ui.tools.ImageGlide;
import java.util.List;
public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<RecyclerDialogPickupCodeBuyListAdpter.ViewHolder> {
private Logs logs;
private Context context;
private final List<BuyStructure> buyList;
private HandlerMain handlerMain;
public RecyclerDialogPickupCodeBuyListAdpter(Context context, List<BuyStructure> buyList, HandlerMain handlerMain) {
this.logs = new Logs(this.getClass());
this.context = context;
this.buyList = buyList;
this.handlerMain = handlerMain;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
RecyclerDialogPickupCodeBuyListBinding binding;
public ViewHolder(@NonNull RecyclerDialogPickupCodeBuyListBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 使用 ViewBinding 來綁定佈局
RecyclerDialogPickupCodeBuyListBinding binding = RecyclerDialogPickupCodeBuyListBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
logs.info("position:" + position);
BuyStructure buyProduct = buyList.get(position);
holder.binding.textPosition.setText(String.valueOf(position));
String productID = buyProduct.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
} else {
holder.binding.textProductName.setText(context.getString(R.string.click_select_product));//
}
// holder.binding.textDelete.setOnClickListener(new textDeleteOnClickListener(position));
String urlString = buyProduct.getProduct().getProductImg();
try {
new ImageGlide(context).fileload(urlString, holder.binding.imageProductPicture);
} catch (LogsEmptyException e) {
this.logs.warning(e);
}
int machinePrice = buyProduct.getProduct().getMachinePrice();
int sellingPrice = buyProduct.getProduct().getSellingPrice();
int memberPrice = buyProduct.getProduct().getMemberPrice();
int slotUpperLimit = buyProduct.getProduct().getSlotUpperLimit();
int count = buyProduct.getCount();
int price = machinePrice > 0? machinePrice: sellingPrice;
holder.binding.textPrice.setText("$"+price);
holder.binding.textCount.setText("x"+String.valueOf(count));
logs.info("field:" + buyProduct.getField());
}
public class addition1OnClickListener implements View.OnClickListener {
private Context context;
private RecyclerDialogBuyListBinding binding;
private int position;
public addition1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
this.context = context;
this.binding = binding;
this.position = position;
}
@Override
public void onClick(View v) {
BuyStructure buyProduct = buyList.get(position);
int count = buyProduct.getCount();
count++;
buyList.get(position).setCount(count);
binding.textCount.setText("x"+String.valueOf(count));
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(), "total price");
}
}
public class subtraction1OnClickListener implements View.OnClickListener {
private Context context;
private RecyclerDialogBuyListBinding binding;
private int position;
public subtraction1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
this.context = context;
this.binding = binding;
this.position = position;
}
@Override
public void onClick(View v) {
BuyStructure buyProduct = buyList.get(position);
int count = buyProduct.getCount();
count--;
if (count <= 0) {
buyList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, buyList.size() - position);
handlerMain.start(getClass().getSimpleName(),context.getString(R.string.delete_a_record));//刪除一筆資料
} else {
buyList.get(position).setCount(count);
binding.textCount.setText("x"+String.valueOf(count));
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(),"total price");
}
}
}
public class editCountOnEditorActionListener implements TextView.OnEditorActionListener{
private ViewHolder holder;
private int position;
public editCountOnEditorActionListener(ViewHolder holder, int position) {
this.holder = holder;
this.position = position;
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEND) {
int count = Integer.valueOf(v.getText().toString());
buyList.get(position).setCount(count);
notifyItemChanged(position);
return true;
}
return false;
}
}
public class textDeleteOnClickListener implements View.OnClickListener{
private int position;
public textDeleteOnClickListener(int position) {
this.position = position;
}
@Override
public void onClick(View v) {
delProductData(position);
}
}
@Override
public int getItemCount() {
return buyList.size();
}
public void addProductData(BuyStructure buyProduct) {
buyList.add(buyProduct);
notifyItemInserted(buyList.size() -1);
}
public void delProductData(int position) {
buyList.remove(position);
notifyItemRemoved(position);
}
}

View File

@ -20,9 +20,9 @@
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/starcloud_logo_circle"
android:icon="@drawable/cpu"
android:label="@string/app_name"
android:roundIcon="@drawable/starcloud_logo_circle"
android:roundIcon="@drawable/cpu"
android:supportsRtl="true"
android:theme="@style/Theme.SmartDevice"
android:windowSoftInputMode="adjustPan"

View File

@ -43,8 +43,8 @@ public abstract class AppCompatActivityAbstract extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 讀取最後設定語言的設定集中在 base所有 Activity inflate 前都會套用
LanguageHelper.applySavedLanguage(this);
// 讀取最後設定語言的設定
// LanguageHelper.applySavedLanguage(this);
ctx = setCtx();
this.logs = new Logs(setCls());

View File

@ -6,13 +6,8 @@ public final class AppEntry {
private AppEntry() {
}
/**
* 是否為取貨單購買流程 Cmuh 中國醫版
* 改由雲端 B014 shopping_mode 決定pickup_sheet取代舊 project flavor 判定
* 開機時 {@code MyApp.initialSystemSet} 已載入持久化值 {@link #initialActivity()} 取得到
*/
public static boolean isCmuh() {
return "pickup_sheet".equals(MyApp.getInstance().getShoppingMode());
return "Cmuh".equalsIgnoreCase(BuildConfig.FLAVOR_project);
}
public static Class<?> initialActivity() {

View File

@ -88,7 +88,6 @@ public abstract class DialogAbstract extends AlertDialog {
// 設定對話框寬度為螢幕的 90%
Window window = getWindow();
if (window != null) {
window.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
WindowManager.LayoutParams layoutParams = window.getAttributes();
// layoutParams.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.9);
@ -98,11 +97,11 @@ public abstract class DialogAbstract extends AlertDialog {
// 設定高度為包裹內容可調整為 MATCH_PARENT
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 設定對話框居中顯示
layoutParams.gravity = Gravity.CENTER;
// 設定對話框貼齊底部
layoutParams.gravity = Gravity.BOTTOM;
window.setAttributes(layoutParams);
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.HIDE_SALE_OVERLAY.getOption(), "hide sale overlay");
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.HIDDEN_MULTILANGUAGE_BUTTON.getOption(), "hidden button");
}
setCancelable(false);
@ -145,23 +144,6 @@ public abstract class DialogAbstract extends AlertDialog {
}
}
/**
* 解析當前 flavor 要使用的現金付款 Dialog 類別
* General 使用強化版 CashPayDialogGeneral其餘 flavor 使用標準 CashPayDialog
* 透過 SaleFlowHandler.cashDialogClass() 由各 flavor 決定避免共用碼直接相依 flavor 類別
*/
protected Class<? extends DialogAbstract> cashDialogClass() {
Context c = getCtx();
if (c instanceof FontendActivity) {
com.unibuy.smartdevice.ui.SaleFlowHandler handler =
((FontendActivity) c).getSaleFlowHandler();
if (handler != null) {
return handler.cashDialogClass();
}
}
return com.unibuy.smartdevice.ui.dialog.CashPayDialog.class;
}
// public void changeDialog(Class<? extends DialogAbstract> cls) {
// try {
@ -220,6 +202,6 @@ public abstract class DialogAbstract extends AlertDialog {
public void cancel() {
super.cancel();
// MyApp.getInstance().setShowDialog(false); // 移至 dismiss 統一處理
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.SHOW_SALE_OVERLAY.getOption(), "show sale overlay");
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.SHOW_MULTILANGUAGE_BUTTON.getOption(), "show button");
}
}

View File

@ -145,9 +145,6 @@ public class MainActivity extends AppCompatActivityAbstract {
showLoginDialog(getCtx(), this::updateLoginStatus);
} else {
MyApp.getInstance().setSuAccess("");
// 登出時一併清除身分與登入 Token避免唯讀判斷殘留
MyApp.getInstance().setIdentity("");
MyApp.getInstance().setLoginToken("");
updateLoginStatus();
Toast.makeText(getCtx(), "已登出後台權限", Toast.LENGTH_SHORT).show();
}

View File

@ -32,7 +32,6 @@ import com.unibuy.smartdevice.exception.LogsNoSuchElementException;
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
import com.unibuy.smartdevice.structure.AccessCodeStructure;
import com.unibuy.smartdevice.structure.CashSetStructure;
import com.unibuy.smartdevice.structure.HardwareSetStructure;
import com.unibuy.smartdevice.structure.CreditCardReceiptStructure;
import com.unibuy.smartdevice.structure.FreeGiftStructure;
import com.unibuy.smartdevice.structure.MemberVerificationStructure;
@ -102,7 +101,6 @@ public class MyApp extends Application {
private String cmuhPickupErrorMessage;
private Map<String, List<MemoStructure>> memoMap;
private CashSetStructure cashSetStructure;
private HardwareSetStructure hardwareSetStructure;
private AccessCodeStructure accessCodeStructure;
private FreeGiftStructure freeGiftStructure;
private MemberVerificationStructure memberVerificationStructure;
@ -112,18 +110,6 @@ public class MyApp extends Application {
private EsunpayStructure esunpay;
private MachineStructure machine;
private DevSetStructure devSet;
// 購買流程模式basic多品項購物車/ employee_card員工卡/ pickup_sheet取貨單
// 由雲端 B014 ShoppingMode 下發取代舊 project flavor 分家預設 basic
private String shoppingMode = "basic";
// 上傳 log 的寫檔門檻等級預設 INFOdebug.txt 只留 INFO/WARNING/ERROR/UPDATE過濾掉 DEBUG/IGNORE 雜訊
// 工程師現場除錯硬體時可設為 DEBUG hex/協議細節重新寫檔logcat 一律照印不受此值影響
private String logLevel = "INFO";
// 機台顯示語系由雲端 B014 LangSet 下發標準標籤(zh-TW/en/...)有序 0 個為預設
private java.util.List<String> machineLanguages = new java.util.ArrayList<>();
private String defaultLanguage = "";
// 商品名稱多語 mapB012 t060v01_i18nkey=商品IDvalue=底線代碼名稱 JSONObject
private JSONObject productNameI18nBlob = new JSONObject();
private JSONObject productSpecI18nBlob = new JSONObject();
private GreenInvoiceStructure greenInvoice;
private GreenpaySettingStructure greenpaySetting;
private GreenpayMerchantStructure greenpayMerchant;
@ -139,10 +125,6 @@ public class MyApp extends Application {
public boolean isDetectHost;
public String suAccess;
// 登入身分B000 回傳 identitysystem=系統方可編輯系統設定並回寫雲端/ company / staff唯讀
private String identity = "";
// B000 登入核發的使用者 Token供系統設定回寫B016, auth:sanctum使用不覆蓋 machine ApiKey
private String loginToken = "";
// 全域閒置計時器
// 記錄使用者最後一次觸碰螢幕的時間 (ms)
@ -210,29 +192,8 @@ public class MyApp extends Application {
this.comPort2ByNFCPay = ComPort.COM_PORT_S2;
}
// DevSet 初始預設值改由 res/values/devset_defaults.xml 提供
// flavor 可覆寫General override 購物車 + 現金/刷卡/掃碼/發票全開
android.content.res.Resources devSetRes = getResources();
this.devSet = new DevSetStructure(
devSetRes.getBoolean(R.bool.devset_default_vmc),
devSetRes.getBoolean(R.bool.devset_default_electric),
devSetRes.getBoolean(R.bool.devset_default_shopping_cart),
devSetRes.getBoolean(R.bool.devset_default_invoice),
devSetRes.getBoolean(R.bool.devset_default_nfc_pay),
devSetRes.getBoolean(R.bool.devset_default_esun_pay),
devSetRes.getBoolean(R.bool.devset_default_tap_pay),
devSetRes.getBoolean(R.bool.devset_default_cash),
devSetRes.getBoolean(R.bool.devset_default_line_pay),
devSetRes.getBoolean(R.bool.devset_default_tap_pay_30),
devSetRes.getBoolean(R.bool.devset_default_tap_pay_31),
devSetRes.getBoolean(R.bool.devset_default_tap_pay_32),
devSetRes.getBoolean(R.bool.devset_default_tap_pay_33),
devSetRes.getBoolean(R.bool.devset_default_tap_pay_34),
devSetRes.getBoolean(R.bool.devset_default_pickup_code),
devSetRes.getBoolean(R.bool.devset_default_pass_code));
// FunctionSet 母開關/來店禮不在建構子參數建構後補套 bootstrap 預設雲端 B014 一到即覆蓋
this.devSet.setPickupModule(devSetRes.getBoolean(R.bool.devset_default_pickup_module));
this.devSet.setWelcomeGift(devSetRes.getBoolean(R.bool.devset_default_welcome_gift));
this.devSet = new DevSetStructure(true, false, false, true, true,
true, true, false, false,true,true,true,true,true);
this.machine = new MachineStructure("");
this.esunpay = new EsunpayStructure("", "", "");
this.greenInvoice = new GreenInvoiceStructure("", "", "", "");
@ -257,8 +218,6 @@ public class MyApp extends Application {
"",
"");
this.cashSetStructure = new CashSetStructure(true, true, true, true, true, true, true);
// HardwareSet貨道型式預設全 false由雲端 B014 下發或系統方主控台設定後覆寫
this.hardwareSetStructure = new HardwareSetStructure(false, false, false, false, false, false);
this.accessCodeStructure = new AccessCodeStructure("");
this.freeGiftStructure = new FreeGiftStructure("");
@ -373,51 +332,11 @@ public class MyApp extends Application {
MyApp.getInstance().getDevSet().setDevTapPay(jsonObject.getBoolean("DevTapPay"));
MyApp.getInstance().getDevSet().setDevCash(jsonObject.getBoolean("DevCash"));
MyApp.getInstance().getDevSet().setDevLinePay(jsonObject.getBoolean("DevLinePay"));
// TapPay30~34 與取貨碼/通行碼 optBoolean 容忍舊紀錄缺 key避免整段載入中斷
MyApp.getInstance().getDevSet().setTapPay30(jsonObject.optBoolean("TapPay30", false));
MyApp.getInstance().getDevSet().setTapPay31(jsonObject.optBoolean("TapPay31", false));
MyApp.getInstance().getDevSet().setTapPay32(jsonObject.optBoolean("TapPay32", false));
MyApp.getInstance().getDevSet().setTapPay33(jsonObject.optBoolean("TapPay33", false));
MyApp.getInstance().getDevSet().setTapPay34(jsonObject.optBoolean("TapPay34", false));
MyApp.getInstance().getDevSet().setPickupCode(jsonObject.optBoolean("PickupCode", false));
MyApp.getInstance().getDevSet().setPassCode(jsonObject.optBoolean("PassCode", false));
// B014 新增開關DevSet 細項 + FunctionSet 模組optBoolean 容忍舊紀錄缺 key
MyApp.getInstance().getDevSet().setDevCreditCard(jsonObject.optBoolean("DevCreditCard", false));
MyApp.getInstance().getDevSet().setDevMobilePay(jsonObject.optBoolean("DevMobilePay", false));
MyApp.getInstance().getDevSet().setDevCardPay(jsonObject.optBoolean("DevCardPay", false));
MyApp.getInstance().getDevSet().setDevScanPay(jsonObject.optBoolean("DevScanPay", false));
MyApp.getInstance().getDevSet().setPickupModule(jsonObject.optBoolean("PickupModule", false));
MyApp.getInstance().getDevSet().setWelcomeGift(jsonObject.optBoolean("WelcomeGift", false));
MyApp.getInstance().getDevSet().setMemberSystem(jsonObject.optBoolean("MemberSystem", false));
MyApp.getInstance().getDevSet().setAmbientTemp(jsonObject.optBoolean("AmbientTemp", false));
MyApp.getInstance().setShoppingMode(jsonObject.optString("ShoppingMode", "basic"));
MyApp.getInstance().setLogLevel(jsonObject.optString("LogLevel", "INFO"));
// 機台顯示語系B014 LangSet
java.util.List<String> langs = new java.util.ArrayList<>();
JSONArray langArr = jsonObject.optJSONArray("MachineLanguages");
if (langArr != null) {
for (int i = 0; i < langArr.length(); i++) {
String l = langArr.optString(i, "");
if (l != null && !l.isEmpty()) langs.add(l);
}
}
MyApp.getInstance().setMachineLanguages(langs);
MyApp.getInstance().setDefaultLanguage(jsonObject.optString("DefaultLanguage", ""));
} catch (JSONException e) {
new Logs(MyApp.class).warning(e);
}
}
// 商品名稱多語 blobB012 t060v01_i18n離線重啟時供 fallback
if (memoDao.existOnlyOne("ProductNameI18n")) {
MyApp.getInstance().setProductNameI18nBlob(memoDao.getOnlyOne("ProductNameI18n").getData());
}
// 商品規格多語 blobB012 t060v03_i18n離線重啟時供 fallback
if (memoDao.existOnlyOne("ProductSpecI18n")) {
MyApp.getInstance().setProductSpecI18nBlob(memoDao.getOnlyOne("ProductSpecI18n").getData());
}
if (memoDao.existOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName())) {
JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()).getData();
logs.debug("initialSystemSet:" + jsonObject.toString());
@ -433,22 +352,6 @@ public class MyApp extends Application {
new Logs(MyApp.class).warning(e);
}
}
if (memoDao.existOnlyOne(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName())) {
JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName()).getData();
logs.debug("initialSystemSet:" + jsonObject.toString());
try {
HardwareSetStructure h = MyApp.getInstance().getHardwareSetStructure();
h.setSpringSlot1_10(jsonObject.optBoolean("SpringSlot1_10", false));
h.setSpringSlot11_20(jsonObject.optBoolean("SpringSlot11_20", false));
h.setSpringSlot21_30(jsonObject.optBoolean("SpringSlot21_30", false));
h.setSpringSlot31_40(jsonObject.optBoolean("SpringSlot31_40", false));
h.setSpringSlot41_50(jsonObject.optBoolean("SpringSlot41_50", false));
h.setSpringSlot51_60(jsonObject.optBoolean("SpringSlot51_60", false));
} catch (Exception e) {
new Logs(MyApp.class).warning(e);
}
}
}
public static void saveSystemSet(Context context) {
MemoDao memoDao = new MemoDao(context);
@ -464,25 +367,6 @@ public class MyApp extends Application {
systemSetByJsonObject.put("DevTapPay", MyApp.getInstance().getDevSet().isDevTapPay());
systemSetByJsonObject.put("DevCash", MyApp.getInstance().getDevSet().isDevCash());
systemSetByJsonObject.put("DevLinePay", MyApp.getInstance().getDevSet().isDevLinePay());
systemSetByJsonObject.put("TapPay30", MyApp.getInstance().getDevSet().isTapPay30());
systemSetByJsonObject.put("TapPay31", MyApp.getInstance().getDevSet().isTapPay31());
systemSetByJsonObject.put("TapPay32", MyApp.getInstance().getDevSet().isTapPay32());
systemSetByJsonObject.put("TapPay33", MyApp.getInstance().getDevSet().isTapPay33());
systemSetByJsonObject.put("TapPay34", MyApp.getInstance().getDevSet().isTapPay34());
systemSetByJsonObject.put("PickupCode", MyApp.getInstance().getDevSet().isPickupCode());
systemSetByJsonObject.put("PassCode", MyApp.getInstance().getDevSet().isPassCode());
systemSetByJsonObject.put("DevCreditCard", MyApp.getInstance().getDevSet().isDevCreditCard());
systemSetByJsonObject.put("DevMobilePay", MyApp.getInstance().getDevSet().isDevMobilePay());
systemSetByJsonObject.put("DevCardPay", MyApp.getInstance().getDevSet().isDevCardPay());
systemSetByJsonObject.put("DevScanPay", MyApp.getInstance().getDevSet().isDevScanPay());
systemSetByJsonObject.put("PickupModule", MyApp.getInstance().getDevSet().isPickupModule());
systemSetByJsonObject.put("WelcomeGift", MyApp.getInstance().getDevSet().isWelcomeGift());
systemSetByJsonObject.put("MemberSystem", MyApp.getInstance().getDevSet().isMemberSystem());
systemSetByJsonObject.put("AmbientTemp", MyApp.getInstance().getDevSet().isAmbientTemp());
systemSetByJsonObject.put("ShoppingMode", MyApp.getInstance().getShoppingMode());
systemSetByJsonObject.put("LogLevel", MyApp.getInstance().getLogLevel());
systemSetByJsonObject.put("MachineLanguages", new JSONArray(MyApp.getInstance().getMachineLanguages()));
systemSetByJsonObject.put("DefaultLanguage", MyApp.getInstance().getDefaultLanguage());
JSONObject cashSetByJsonObject = new JSONObject();
cashSetByJsonObject.put("BillF1000", MyApp.getInstance().getCashSetStructure().isBillF1000());
@ -493,14 +377,6 @@ public class MyApp extends Application {
cashSetByJsonObject.put("CoinD5", MyApp.getInstance().getCashSetStructure().isCoinD5());
cashSetByJsonObject.put("CoinC1", MyApp.getInstance().getCashSetStructure().isCoinC1());
JSONObject hardwareSetByJsonObject = new JSONObject();
hardwareSetByJsonObject.put("SpringSlot1_10", MyApp.getInstance().getHardwareSetStructure().isSpringSlot1_10());
hardwareSetByJsonObject.put("SpringSlot11_20", MyApp.getInstance().getHardwareSetStructure().isSpringSlot11_20());
hardwareSetByJsonObject.put("SpringSlot21_30", MyApp.getInstance().getHardwareSetStructure().isSpringSlot21_30());
hardwareSetByJsonObject.put("SpringSlot31_40", MyApp.getInstance().getHardwareSetStructure().isSpringSlot31_40());
hardwareSetByJsonObject.put("SpringSlot41_50", MyApp.getInstance().getHardwareSetStructure().isSpringSlot41_50());
hardwareSetByJsonObject.put("SpringSlot51_60", MyApp.getInstance().getHardwareSetStructure().isSpringSlot51_60());
memoDao.insertOnlyOne(new MemoStructure(
MyApp.getInstance().getDevSet().getClass().getSimpleName(),
systemSetByJsonObject
@ -518,48 +394,11 @@ public class MyApp extends Application {
memoDao.mapUpdate();
new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()));
memoDao.insertOnlyOne(new MemoStructure(
MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName(),
hardwareSetByJsonObject
, new JSONObject()));
memoDao.mapUpdate();
new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName()));
} catch (JSONException e) {
new Logs(MyApp.class).warning(e);
}
}
// 持久化商品名稱多語 blob不動 products schema存於 MemoDao供離線重啟 fallback
public static void saveProductNameI18n(Context context) {
MemoDao memoDao = new MemoDao(context);
try {
memoDao.insertOnlyOne(new MemoStructure(
"ProductNameI18n",
MyApp.getInstance().getProductNameI18nBlob(),
new JSONObject()));
memoDao.mapUpdate();
} catch (Exception e) {
new Logs(MyApp.class).warning(e);
}
}
// 持久化商品規格多語 blob不動 products schema存於 MemoDao供離線重啟 fallback
public static void saveProductSpecI18n(Context context) {
MemoDao memoDao = new MemoDao(context);
try {
memoDao.insertOnlyOne(new MemoStructure(
"ProductSpecI18n",
MyApp.getInstance().getProductSpecI18nBlob(),
new JSONObject()));
memoDao.mapUpdate();
} catch (Exception e) {
new Logs(MyApp.class).warning(e);
}
}
public static void updateLog(Context context) {
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(UploadLogOneTimeWorker.class)
.setInitialDelay(5, TimeUnit.SECONDS)
@ -841,7 +680,7 @@ public class MyApp extends Application {
int i = 0;
for (SlotStructure slotData: getSlotList(field)) {
i += 1;
logs.debug("slot index:" + i);
logs.info("AAAAAAAAAAAAAAAAAAAAAAAAAAAA0"+i);
if (slotData.getProduct().getProductID().equals(String.valueOf(pid))) {
if(slotData.getCount() > 0) {
return slotData;
@ -1033,68 +872,6 @@ public class MyApp extends Application {
return devSet;
}
public String getShoppingMode() {
return shoppingMode;
}
public void setShoppingMode(String shoppingMode) {
this.shoppingMode = (shoppingMode == null || shoppingMode.isEmpty()) ? "basic" : shoppingMode;
}
public String getLogLevel() {
return logLevel;
}
public void setLogLevel(String logLevel) {
this.logLevel = (logLevel == null || logLevel.isEmpty()) ? "INFO" : logLevel.toUpperCase(java.util.Locale.US);
}
public java.util.List<String> getMachineLanguages() {
return machineLanguages;
}
public void setMachineLanguages(java.util.List<String> machineLanguages) {
this.machineLanguages = (machineLanguages == null) ? new java.util.ArrayList<>() : machineLanguages;
}
public String getDefaultLanguage() {
return defaultLanguage;
}
public void setDefaultLanguage(String defaultLanguage) {
this.defaultLanguage = (defaultLanguage == null) ? "" : defaultLanguage;
}
public void setProductNameI18nBlob(JSONObject blob) {
this.productNameI18nBlob = (blob == null) ? new JSONObject() : blob;
}
public JSONObject getProductNameI18nBlob() {
return productNameI18nBlob;
}
// 取某商品的名稱多語 JSON 字串底線代碼 key無則回 null ProductStructure 離線時 fallback
public String getProductNameI18nJson(String productId) {
if (productId == null || productNameI18nBlob == null) return null;
JSONObject obj = productNameI18nBlob.optJSONObject(productId);
return (obj == null) ? null : obj.toString();
}
public void setProductSpecI18nBlob(JSONObject blob) {
this.productSpecI18nBlob = (blob == null) ? new JSONObject() : blob;
}
public JSONObject getProductSpecI18nBlob() {
return productSpecI18nBlob;
}
// 取某商品的規格多語 JSON 字串底線代碼 key無則回 null ProductStructure 離線時 fallback
public String getProductSpecI18nJson(String productId) {
if (productId == null || productSpecI18nBlob == null) return null;
JSONObject obj = productSpecI18nBlob.optJSONObject(productId);
return (obj == null) ? null : obj.toString();
}
public ReportAccessCodeStructure getReportAccessCodeStructure() {
return reportAccessCodeStructure;
@ -1108,10 +885,6 @@ public class MyApp extends Application {
return cashSetStructure;
}
public HardwareSetStructure getHardwareSetStructure() {
return hardwareSetStructure;
}
public AccessCodeStructure getAccessCodeStructure() {
return accessCodeStructure;
}
@ -1170,10 +943,9 @@ public class MyApp extends Application {
if (isShowDialog()) {
reason.append("[Dialog Open] ");
}
// 注意購物車非空不可計入忙碌否則待機/重啟會與清車死結
// resetIdleTimer 忙碌即不待機並重新計時但清空購物車的程式就寫在待機那一支
// 購物車有殘留 永遠忙碌 永遠不待機 永遠清不掉真正進行中狀態
// 已由 isShowDialog付款/出貨對話框 isNfcPaymentActive感應支付涵蓋
if (!getBuyList().isEmpty()) {
reason.append("[Cart not empty: ").append(getBuyList().size()).append(" items] ");
}
if (isNfcPaymentActive()) {
reason.append("[NFC Payment Active] ");
}
@ -1193,27 +965,6 @@ public class MyApp extends Application {
this.suAccess = suAccess;
}
public String getIdentity() {
return identity == null ? "" : identity;
}
public void setIdentity(String identity) {
this.identity = identity == null ? "" : identity;
}
/** 是否為系統方(可編輯系統設定並回寫雲端)。 */
public boolean isSystemIdentity() {
return "system".equals(getIdentity());
}
public String getLoginToken() {
return loginToken == null ? "" : loginToken;
}
public void setLoginToken(String loginToken) {
this.loginToken = loginToken == null ? "" : loginToken;
}
public boolean isDetectHost() {
return isDetectHost;
}

View File

@ -151,17 +151,30 @@ public class DevNFCPayController extends HandlerMainScheduler {
getLogs().info("電文接收:" + readData);
MyApp.getInstance().getReportFlowInfoData().setFlowRequestInfo(readData);
// 修復 付款成功卻不出貨交易結果 responseCode就在這段 404 byte 電文裡
// 直接解析本段即可先前版本誤改成再用 readListen 第二段電文才解析
// 但刷卡機只送這一段第二段永遠等不到 倒數逾時 RESPONSE 從未送出 不出貨
HashMap<String, String> readMap = devNFCPay.read(readData);
String code = readMap.get("code");
getLogs().info("交易完成,回傳代碼: " + code);
while (isRun() && MyApp.getInstance().isShowDialog()) { // 增加狀態檢查
byte[] buffer = comPort2.readListen(30, 200);
if (isRun() && !isShuttingDown && MyApp.getInstance().isShowDialog()) { // 回報前的生命週期檢查
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
} else {
getLogs().info("🚫 控制器已停止或對話框已關閉,捨棄 NFC 回傳結果");
if (buffer.length > 0) {
getLogs().info("NFC 收到資料: " + PortTools.showHex(buffer));
String rxData = new String(buffer, StandardCharsets.UTF_8);
if (rxData.contains(String.valueOf((char) 0x03))) { // ETX
HashMap<String, String> response = devNFCPay.read(rxData);
String code = response.get("code");
if (isRun() && !isShuttingDown && MyApp.getInstance().isShowDialog()) { // 最後回報前的檢查
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
} else {
getLogs().info("🚫 控制器已停止或對話框已關閉,捨棄 NFC 回傳結果");
}
break;
}
}
if (!isRun() || isShuttingDown || !MyApp.getInstance().isShowDialog()) {
getLogs().info("NFC 讀取中斷: 控制器關閉或對話框消失");
break;
}
}
} catch (LogsParseException | LogsIOException e) {
getLogs().warning(e);

View File

@ -56,7 +56,7 @@ public class DevXinYuanController extends DevController {
try {
this.rs232Dev = new Rs232Dev(MyApp.getInstance().getComPort1ByXinYuan());
devXinYuan.setRun(true);
logs.info("下位機連接");
logs.debug("下位機連接");
} catch (LogsUnsupportedOperationException | LogsIOException e) {
// getLogs().error(ErrorCode.DEV_ERROR, "@下位機連接錯誤", handlerMain);
getLogs().warning(e);
@ -123,9 +123,9 @@ public class DevXinYuanController extends DevController {
Byte key = Byte.valueOf(vmcCmd[0]);
if (!vmcMap.containsKey(key)) {
getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData));
getLogs().debug("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
getLogs().info("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
getLogs().info("XinYuan vmcData:" + PortTools.showHex(vmcData));
getLogs().info("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
getLogs().info("XinYuan vmc not found");
continue;
}
@ -133,9 +133,9 @@ public class DevXinYuanController extends DevController {
DevXinYuan.VMC vmc = vmcMap.get(key);
if (vmc != DevXinYuan.VMC.VMC_41_POLL) {
getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData));
getLogs().debug("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
getLogs().info("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
getLogs().info("XinYuan vmcData:" + PortTools.showHex(vmcData));
getLogs().info("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
getLogs().info("XinYuan vmc:" + vmc.name());
}
@ -143,12 +143,12 @@ public class DevXinYuanController extends DevController {
case VMC_41_POLL:
if (getCountBySendBuffer() > 0) {
byte[] cmdBytes = getSendBufferByFirst();
getLogs().debug("XinYuan cmd hex:" + bytesToHex(cmdBytes));
getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"+bytesToHex(cmdBytes));
if (cmdBytes.length == (4+cmdBytes[3]+1)) {
for (int sendCount=1; sendCount <2; sendCount++) {
rs232Dev.send(cmdBytes);
getLogs().debug("XinYuan Send:" + PortTools.showHex(cmdBytes));
getLogs().info("XinYuan Send:" + PortTools.showHex(cmdBytes));
byte[] vmcBuffer = rs232Dev.readListen(0, 1);
if (vmcBuffer.length > 2) {
@ -247,7 +247,7 @@ public class DevXinYuanController extends DevController {
break;
case VMC_52_CHECK_VMC_STATUS:
//溫度:vmcData[7]
getLogs().debug("下位機訊號"+PortTools.showHex(vmcData));
getLogs().info("下位機訊號"+PortTools.showHex(vmcData));
if (PortTools.showHex(vmcData).isEmpty()) {
getLogs().info("下位機失連重啟APP");

View File

@ -46,16 +46,17 @@ public enum ComPort {
}
public String getFilepath() {
// 下位機接線由硬體 flavorIPC 世代 / 安卓版本決定與購買流程 shopping_mode 正交
// S12XYU新板原晟崴 10")→ ttyS7/ttyS6
// S9XYU Android 9 原中國醫/通用 21.5")→ ttyS1/ttyS2。
// 單一 flavor 維度hardwareAGP 僅產生 BuildConfig.FLAVOR值為 S12XYU / S9XYU
boolean isS12 = "S12XYU".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR);
if (this == COM_PORT_S1) {
return isS12 ? "/dev/ttyS7" : "/dev/ttyS1";
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
return "/dev/ttyS1";
}
return "/dev/ttyS7";
}
if (this == COM_PORT_S2) {
return isS12 ? "/dev/ttyS6" : "/dev/ttyS2";
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
return "/dev/ttyS2";
}
return "/dev/ttyS6";
}
return filepath;
}

View File

@ -267,7 +267,7 @@ public class DevXinYuan {
proBytes = PortTools.concatByteArrays(proBytes, XORByte);
if (isShow) {
logs.debug("XinYuan ipcCmd:" + cmd.name() + " " + PortTools.showHex(proBytes));
logs.info("XinYuan ipcCmd:" + cmd.name() + " " + PortTools.showHex(proBytes));
}
return proBytes;
@ -371,18 +371,18 @@ public class DevXinYuan {
}
public CMD readCMD(byte[] buffer) throws LogsEmptyException {
logs.debug(PortTools.showHex(buffer));
logs.info(PortTools.showHex(buffer));
for (CMD cmd : CMD.values()) {
if (cmd.getBytes().length >= 2 && buffer.length >= 2) {
if (cmd.getBytes()[0] == buffer[0]) {
if (cmd.equals(CMD.CMD_FAFB_STX)) {
if (cmd.getBytes()[1] == buffer[1]) {
logs.debug("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return cmd;
}
} else {
logs.debug("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return cmd;
}
}
@ -393,18 +393,18 @@ public class DevXinYuan {
}
public VMC readVMC(byte[] buffer) throws LogsEmptyException {
logs.debug(PortTools.showHex(buffer));
logs.info(PortTools.showHex(buffer));
for (VMC vmc : VMC.values()) {
if (vmc.getBytes().length >= 2 && buffer.length >= 2) {
if (vmc.getBytes()[0] == buffer[0]) {
if (vmc.equals(VMC.VMC_FAFB_STX)) {
if (vmc.getBytes()[1] == buffer[1]) {
logs.debug("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return vmc;
}
} else {
logs.debug("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return vmc;
}
}
@ -416,7 +416,7 @@ public class DevXinYuan {
public byte readPackNo(byte[] buffer) {
byte packNo = buffer[0];
logs.debug("Read packNo:" + PortTools.showHex(packNo));
logs.info("Read packNo:" + PortTools.showHex(packNo));
return packNo;
}
@ -424,7 +424,7 @@ public class DevXinYuan {
for (BUY_STATUS_04 buyStatus : BUY_STATUS_04.values()) {
if (buyStatus.getBytes().length == 1 && buffer.length > 0) {
if (buyStatus.getBytes()[0] == buffer[1]) {
logs.debug(BUY_STATUS_04.class.getSimpleName()+":" + buyStatus.name() + " " + PortTools.showHex(buyStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info(BUY_STATUS_04.class.getSimpleName()+":" + buyStatus.name() + " " + PortTools.showHex(buyStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return buyStatus;
}
}
@ -437,7 +437,7 @@ public class DevXinYuan {
for (SLOT_STATUS_02 slotStatus : SLOT_STATUS_02.values()) {
if (slotStatus.getBytes().length == 1 && buffer.length > 0) {
if (slotStatus.getBytes()[0] == buffer[1]) {
logs.debug(SLOT_STATUS_02.class.getSimpleName()+":" + slotStatus.name() + " " + PortTools.showHex(slotStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info(SLOT_STATUS_02.class.getSimpleName()+":" + slotStatus.name() + " " + PortTools.showHex(slotStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return slotStatus;
}
}
@ -451,7 +451,7 @@ public class DevXinYuan {
for (GIVE_MONEY_26 giveMoney : GIVE_MONEY_26.values()) {
if (giveMoney.getBytes().length == 1 && buffer.length > 0) {
if (giveMoney.getBytes()[0] == buffer[1]) {
logs.debug(GIVE_MONEY_26.class.getSimpleName()+":" + giveMoney.name() + " " + PortTools.showHex(giveMoney.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
logs.info(GIVE_MONEY_26.class.getSimpleName()+":" + giveMoney.name() + " " + PortTools.showHex(giveMoney.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
return giveMoney;
}
}
@ -475,10 +475,10 @@ public class DevXinYuan {
byte XORByte = buffer[buffer.length -1];
proBytes = PortTools.concatByteArrays(proBytes, cmdLenData);
byte XORByte2 = getXOR(proBytes);
logs.debug("Length:"+length+" XOR1:" + PortTools.showHex(new byte[] {XORByte}) + " XOR2:" + PortTools.showHex(new byte[] {XORByte2}));
logs.info("Length:"+length+" XOR1:" + PortTools.showHex(new byte[] {XORByte}) + " XOR2:" + PortTools.showHex(new byte[] {XORByte2}));
if (buffer.length > (2+length)) {
byte[] dataBytes = PortTools.catByteArrays(buffer, 2, length);
logs.debug("data:"+PortTools.showHex(dataBytes));
logs.info("data:"+PortTools.showHex(dataBytes));
return dataBytes;
} else {
throw new LogsEmptyException(logs, "data array length insufficient");

View File

@ -18,10 +18,6 @@ import java.util.Date;
import java.util.Locale;
public class Logs {
// 單筆寫檔上限超過即截斷避免整包 JSON/payload dump debug.txt 撐爆淹沒有用事件
// 只影響寫入 debug.txt那份logcatLog.d/i/...維持完整不截斷
private static final int MAX_LINE = 1000;
private String message;
private String className;
private String methodName;
@ -50,10 +46,6 @@ public class Logs {
}
String dateTime = Tools.getTimeStampByNow("yyyy-MM-dd HH:mm:ss.SSS");
// 1超長行截斷止血整包 JSON/payload dump保留事件本身與原始長度資訊
if (data.length() > MAX_LINE) {
data = data.substring(0, MAX_LINE) + " …[truncated " + data.length() + " chars]";
}
data = dateTime + " " + data;
try (FileOutputStream fos = new FileOutputStream(txtDebug, true)) { // try-with-resources 自動 close()
@ -69,20 +61,10 @@ public class Logs {
return this.className+"."+this.methodName+"("+this.lineNumber+")";
}
// 2是否把 DEBUG/IGNORE 寫進 debug.txt預設門檻 INFO不寫過濾掉開發雜訊
// 雲端/現場把 LogLevel 設為 DEBUG 時才寫檔MyApp 尚未就緒時一律不寫維持乾淨預設
private static boolean writeDebugToFile() {
try {
return "DEBUG".equals(com.unibuy.smartdevice.MyApp.getInstance().getLogLevel());
} catch (Exception e) {
return false;
}
}
public void debug(String message) {
this.message = message;
Log.d(this.className, "DEBUG ["+getClassName()+"]"+this.message);
if (writeDebugToFile()) writeFile("DEBUG ["+getClassName()+"]"+this.message);
writeFile("DEBUG ["+getClassName()+"]"+this.message);
}
public void info(String message) {
@ -105,7 +87,7 @@ public class Logs {
public void ignore(String message) {
this.message = message;
Log.w(this.className, "IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message);
if (writeDebugToFile()) writeFile("IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message);
writeFile("IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message);
}
public void warning(Exception e) {

View File

@ -493,6 +493,7 @@ public class HttpAPI {
public void response(int responseCode, String receive, HandlerMain handlerMain) {
if (responseCode == 200) {
try {
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
MyApp.getInstance().getMachine().setMachineID("");
MyApp.getInstance().getEsunpay().setStoreID("");
@ -655,7 +656,9 @@ public class HttpAPI {
MyApp.getInstance().getGreenpayMerchant().getAtomeMerchantId()
}));
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa0");
SettingActivity.updateProgressUI(10, "下載中...");
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa10");
settingsDao.close();
} catch (JSONException e) {
getLogs().warning(e);
@ -717,7 +720,7 @@ public class HttpAPI {
* t060v06 "https://unibuy.com.tw/Unibuy/dist/img/products/53317506.png",
*/
getLogs().debug("productData:" + productData);
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAA" + productData);
if (productData.has("t060v00") &&
productData.has("t060v06") &&
@ -780,6 +783,7 @@ public class HttpAPI {
productsDao.close();
SettingActivity.updateProgressUI(10, "下載中...");
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa30");
handlerMain.start(getClass().getSimpleName(), Option.DOWNLOAD_PRODUCT_RESPONSE.getOption(),
"成功載入" + MyApp.getInstance().getProductList().size() + "筆商品資料");
@ -1049,8 +1053,9 @@ public class HttpAPI {
JSONObject receiveByJsonObject = new JSONObject(receive);
String workApi = receiveByJsonObject.getString("workApi");
int status = receiveByJsonObject.getInt("status");
getLogs().info("bbbbbbbbbbbbbbbbbbbbbb status" + status);
String message = receiveByJsonObject.getString("message");
getLogs().debug("reportFlow status:" + status + " message:" + message);
getLogs().info("bbbbbbbbbbbbbbbbbbbbbb message" + message);
if (status == 0) {
handlerMain.start(getClass().getSimpleName(), Option.REPORT_FLOW_RESPONSE.getOption(),
@ -2137,7 +2142,7 @@ public class HttpAPI {
*/
jsonObject2.put("data", jsonArray);
getLogs().debug("B009 supplementary:" + jsonObject2.toString());
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=" + jsonObject2.toString());
formPutRunMain.start(domainNameByUnibuy + "products/supplementary/B009", jsonObject2.toString(), null, null,
HttpConnect.ContentType.JSON);
@ -2223,7 +2228,7 @@ public class HttpAPI {
int slotid = Integer.parseInt(deliveryData.getString("tid"));
int slotnum = Integer.parseInt(deliveryData.getString("num"));
getLogs().info("dispatch slotid:" + slotid + " num:" + slotnum);
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + slotid + " " + slotnum);
slotid = slotid;

View File

@ -174,11 +174,11 @@ public class HttpTapPay {
int payType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
GreenpaySettingStructure greenpaySetting = MyApp.getInstance().getGreenpaySetting();
String greenpayMerchantId = MyApp.getInstance().getMachine().getMachineID();
logs.debug("merchantId line:" + MyApp.getInstance().getGreenpayMerchant().getLineMerchantId()
+ " jko:" + MyApp.getInstance().getGreenpayMerchant().getJkoMerchantId()
+ " pi:" + MyApp.getInstance().getGreenpayMerchant().getPiMerchantId()
+ " ps:" + MyApp.getInstance().getGreenpayMerchant().getPsMerchantId()
+ " easy:" + MyApp.getInstance().getGreenpayMerchant().getEasyMerchantId());
logs.info("aaaaaaaaaaaaaaaaaaaaaa1"+ MyApp.getInstance().getGreenpayMerchant().getLineMerchantId());
logs.info("aaaaaaaaaaaaaaaaaaaaaa2"+ MyApp.getInstance().getGreenpayMerchant().getJkoMerchantId());
logs.info("aaaaaaaaaaaaaaaaaaaaaa3"+ MyApp.getInstance().getGreenpayMerchant().getPiMerchantId());
logs.info("aaaaaaaaaaaaaaaaaaaaaa4"+ MyApp.getInstance().getGreenpayMerchant().getPsMerchantId());
logs.info("aaaaaaaaaaaaaaaaaaaaaa5"+ MyApp.getInstance().getGreenpayMerchant().getEasyMerchantId());
switch (payType) {
case 30:
if (MyApp.getInstance().getGreenpayMerchant() != null && !MyApp.getInstance().getGreenpayMerchant().getLineMerchantId().isEmpty()) {

View File

@ -15,8 +15,6 @@ import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.database.ProductsDao;
import com.unibuy.smartdevice.database.SettingsDao;
import com.unibuy.smartdevice.structure.CashSetStructure;
import com.unibuy.smartdevice.structure.DevSetStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.ReportSlotListStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
@ -157,9 +155,6 @@ public class StarCloudAPI {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
MyApp.getInstance().setSuAccess(res.optString("Su_Account", account));
// 身分與登入 Tokenidentity 決定系統設定是否可編輯loginToken 供回寫 B016 使用
MyApp.getInstance().setIdentity(res.optString("identity", ""));
MyApp.getInstance().setLoginToken(res.optString("token", ""));
// 移除覆蓋 ApiKey 的邏輯因為 B000 回傳的可能是 Admin Token但我們需要保留 Machine Token
Log.i(TAG, "Login success for user: " + account);
traceLogin("[" + traceId + "] b000_parse_success parseMs="
@ -199,17 +194,6 @@ public class StarCloudAPI {
* 不需要 ApiKey 即可呼叫僅需機器序號
*/
public void downloadSetting() {
downloadSetting(true);
}
/**
* B014: 重新下載系統設定
*
* @param reconnectMqtt 設定落地後是否立即重連 MQTT
* 綁定 UI 流程傳 true遠端 update_settings 指令傳 false
* 改由呼叫端在發出 ACK 之後再重連避免重連把送指令的連線斷掉導致 ACK 發不出去
*/
public void downloadSetting(final boolean reconnectMqtt) {
final String machineId = MyApp.getInstance().getMachine().getMachineID();
if (machineId.isEmpty()) {
Log.e(TAG, "Machine ID is empty, cannot bind.");
@ -271,7 +255,7 @@ public class StarCloudAPI {
if (res.optBoolean("success", false)) {
JSONObject config = res.optJSONObject("data");
if (config != null) {
saveSettings(config, reconnectMqtt);
saveSettings(config);
handlerMain.start("StarCloudAPI", 200, "綁定成功");
} else {
handlerMain.start("StarCloudAPI", 500, "設定內容為空,原始資料:" + finalReceive);
@ -609,389 +593,7 @@ public class StarCloudAPI {
void onFailure(String message);
}
/** 取貨碼驗證回呼:成功時回傳 code_id 與要出貨的貨道 slot_no。 */
public interface PickupVerificationCallback {
void onSuccess(String codeId, int slotNo);
void onFailure(String message);
}
/** 系統設定回寫回呼B016。 */
public interface SettingsUploadCallback {
void onSuccess();
void onFailure(String message);
}
/**
* 來店禮驗證回呼B690成功回傳折扣資訊
* discountTypeamount折抵金額/ percentage折扣百分比discountValue 為折扣趴數八折=20
*/
public interface WelcomeGiftCallback {
void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name);
void onFailure(String message);
}
/**
* B016系統方identity=system回寫機台系統設定到雲端 star-cloud
* 使用 B000 登入核發的使用者 Tokenauth:sanctum後端再驗一次系統方權限
* @param settings 後端鍵名的開關 JSON鍵名對照見 SystemSetActivity#buildBackendSettings
*/
public void uploadSystemSettings(final JSONObject settings, final SettingsUploadCallback callback) {
final String url = getBaseUrl() + "machine/setting/B016";
final String machineId = MyApp.getInstance().getMachine().getMachineID();
final String loginToken = MyApp.getInstance().getLoginToken();
Log.i(TAG, "Starting B016 (update settings) for machine: " + machineId);
if (loginToken == null || loginToken.isEmpty()) {
new android.os.Handler(android.os.Looper.getMainLooper()).post(
() -> callback.onFailure("尚未登入或登入逾時,請重新登入後台"));
return;
}
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = -1;
String receive = "";
try {
JSONObject json = new JSONObject();
json.put("machine", machineId);
json.put("settings", settings);
java.net.URL reqUrl = new java.net.URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
conn.setRequestMethod("PATCH");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + loginToken);
conn.setConnectTimeout(15000);
conn.setReadTimeout(15000);
conn.setDoOutput(true);
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
conn.getOutputStream().flush();
responseCode = conn.getResponseCode();
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
receive = sb.toString();
}
conn.disconnect();
Log.d(TAG, "B016 Response (" + responseCode + "): " + receive);
} catch (final Exception e) {
Log.e(TAG, "B016 Network Error", e);
new android.os.Handler(android.os.Looper.getMainLooper()).post(
() -> callback.onFailure("連線失敗: " + e.getMessage()));
return;
}
final int finalCode = responseCode;
final String finalReceive = receive;
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
callback.onSuccess();
} else {
callback.onFailure(res.optString("message", "設定儲存失敗"));
}
} catch (Exception e) {
callback.onFailure("解析錯誤 (" + finalCode + ")");
}
} else if (finalCode == 403) {
callback.onFailure("權限不足:僅系統方可修改系統設定");
} else if (finalCode == 401) {
callback.onFailure("登入逾時,請重新登入後台");
} else if (finalCode == 404) {
callback.onFailure("查無此機台");
} else {
try {
JSONObject res = new JSONObject(finalReceive);
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
} catch (Exception e) {
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
}
}
});
}
}).start();
}
/**
* B660: 取貨碼驗證邏輯比照 B680 員工卡驗證通道不同
* 成功回傳 data.code_id data.slot_no要出貨的貨道
*/
public void verifyPickupCode(final String code, final PickupVerificationCallback callback) {
final String url = getBaseUrl() + "machine/pickup/verify/B660";
Log.i(TAG, "Starting B660 request for code: " + code);
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = -1;
String receive = "";
try {
JSONObject json = new JSONObject();
json.put("code", code);
java.net.URL reqUrl = new java.net.URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
String machineToken = MyApp.getInstance().getMachine().getApiKey();
if (machineToken != null && !machineToken.isEmpty()) {
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
}
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setDoOutput(true);
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
conn.getOutputStream().flush();
responseCode = conn.getResponseCode();
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
receive = sb.toString();
}
conn.disconnect();
Log.d(TAG, "B660 Response (" + responseCode + "): " + receive);
} catch (final Exception e) {
Log.e(TAG, "B660 Network Error", e);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
return;
}
final int finalCode = responseCode;
final String finalReceive = receive;
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
JSONObject data = res.optJSONObject("data");
String codeId = data != null ? data.optString("code_id", "") : "";
int slotNo = data != null ? data.optInt("slot_no", -1) : -1;
callback.onSuccess(codeId, slotNo);
} else {
callback.onFailure(res.optString("message", "取貨碼驗證失敗"));
}
} catch (Exception e) {
callback.onFailure("解析錯誤: " + finalCode);
}
} else if (finalCode == 404) {
callback.onFailure("查無此取貨碼或已使用");
} else if (finalCode == 429) {
callback.onFailure("錯誤次數過多,請稍後再試");
} else if (finalCode == 401) {
callback.onFailure("系統認證失效,請聯繫管理員");
} else {
try {
JSONObject res = new JSONObject(finalReceive);
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
} catch (Exception e) {
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
}
}
});
}
}).start();
}
/**
* B670: 通行碼驗證技師用邏輯比照 B680驗證通道不同
* 成功回傳 data.code_id
*/
public void verifyPassCode(final String code, final VerificationCallback callback) {
final String url = getBaseUrl() + "machine/passcode/verify/B670";
Log.i(TAG, "Starting B670 request for code: " + code);
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = -1;
String receive = "";
try {
JSONObject json = new JSONObject();
json.put("code", code);
java.net.URL reqUrl = new java.net.URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
String machineToken = MyApp.getInstance().getMachine().getApiKey();
if (machineToken != null && !machineToken.isEmpty()) {
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
}
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setDoOutput(true);
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
conn.getOutputStream().flush();
responseCode = conn.getResponseCode();
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
receive = sb.toString();
}
conn.disconnect();
Log.d(TAG, "B670 Response (" + responseCode + "): " + receive);
} catch (final Exception e) {
Log.e(TAG, "B670 Network Error", e);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
return;
}
final int finalCode = responseCode;
final String finalReceive = receive;
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
String codeId = "";
if (res.has("data")) {
codeId = res.optJSONObject("data").optString("code_id", "");
} else {
codeId = res.optString("code_id", "");
}
callback.onSuccess(codeId);
} else {
callback.onFailure(res.optString("message", "通行碼驗證失敗"));
}
} catch (Exception e) {
callback.onFailure("解析錯誤: " + finalCode);
}
} else if (finalCode == 404) {
callback.onFailure("查無此通行碼或已失效");
} else if (finalCode == 429) {
callback.onFailure("錯誤次數過多,請稍後再試");
} else if (finalCode == 401) {
callback.onFailure("系統認證失效,請聯繫管理員");
} else {
try {
JSONObject res = new JSONObject(finalReceive);
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
} catch (Exception e) {
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
}
}
});
}
}).start();
}
/**
* B690: 來店禮驗證邏輯比照 B660/B670驗證通道不同
* 成功回傳 data.code_id 與折扣資訊discount_type/discount_value/discount_label/name
*/
public void verifyWelcomeGift(final String code, final WelcomeGiftCallback callback) {
final String url = getBaseUrl() + "machine/welcome-gift/verify/B690";
Log.i(TAG, "Starting B690 request for code: " + code);
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = -1;
String receive = "";
try {
JSONObject json = new JSONObject();
json.put("code", code);
java.net.URL reqUrl = new java.net.URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
String machineToken = MyApp.getInstance().getMachine().getApiKey();
if (machineToken != null && !machineToken.isEmpty()) {
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
}
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setDoOutput(true);
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
conn.getOutputStream().flush();
responseCode = conn.getResponseCode();
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
receive = sb.toString();
}
conn.disconnect();
Log.d(TAG, "B690 Response (" + responseCode + "): " + receive);
} catch (final Exception e) {
Log.e(TAG, "B690 Network Error", e);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
return;
}
final int finalCode = responseCode;
final String finalReceive = receive;
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
JSONObject data = res.optJSONObject("data");
String codeId = data != null ? data.optString("code_id", "") : "";
String discountType = data != null ? data.optString("discount_type", "") : "";
int discountValue = data != null ? data.optInt("discount_value", 0) : 0;
String discountLabel = data != null ? data.optString("discount_label", "") : "";
String name = data != null ? data.optString("name", "") : "";
callback.onSuccess(codeId, discountType, discountValue, discountLabel, name);
} else {
callback.onFailure(res.optString("message", "來店禮驗證失敗"));
}
} catch (Exception e) {
callback.onFailure("解析錯誤: " + finalCode);
}
} else if (finalCode == 404) {
callback.onFailure("查無此來店禮或已失效");
} else if (finalCode == 429) {
callback.onFailure("錯誤次數過多,請稍後再試");
} else if (finalCode == 401) {
callback.onFailure("系統認證失效,請聯繫管理員");
} else if (finalCode == 400) {
callback.onFailure("來店禮格式錯誤(需 8 碼)");
} else {
try {
JSONObject res = new JSONObject(finalReceive);
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
} catch (Exception e) {
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
}
}
});
}
}).start();
}
private void saveSettings(JSONObject config) {
saveSettings(config, true);
}
private void saveSettings(JSONObject config, boolean reconnectMqtt) {
// 使用 optString 避免欄位缺失導致 JSONException
// 1. 儲存 ApiToken 序號
@ -1021,106 +623,9 @@ public class StarCloudAPI {
MyApp.getInstance().getGreenInvoice().setGreenHashIV(config.optString("t050v36", ""));
MyApp.getInstance().getGreenInvoice().setGreenEmail(config.optString("t050v38", ""));
// 5. 解析雲端功能開關缺漏欄位 false缺漏整個物件 不動本地避免連線異常把現場全關
JSONObject devSet = config.optJSONObject("DevSet");
if (devSet != null) {
DevSetStructure d = MyApp.getInstance().getDevSet();
d.setShoppingCar(devSet.optBoolean("ShoppingCar", false));
d.setInvoice(devSet.optBoolean("Invoice", false));
d.setDevNFCPay(devSet.optBoolean("DevNFCPay", false));
d.setDevEsunPay(devSet.optBoolean("DevEsunPay", false));
d.setDevTapPay(devSet.optBoolean("DevTapPay", false));
d.setDevCash(devSet.optBoolean("DevCash", false));
d.setDevLinePay(devSet.optBoolean("DevLinePay", false));
d.setTapPay30(devSet.optBoolean("TapPay30", false));
d.setTapPay31(devSet.optBoolean("TapPay31", false));
d.setTapPay32(devSet.optBoolean("TapPay32", false));
d.setTapPay33(devSet.optBoolean("TapPay33", false));
d.setTapPay34(devSet.optBoolean("TapPay34", false));
// B014 新增支付細項刷卡機/掃碼子開關
d.setDevCreditCard(devSet.optBoolean("DevCreditCard", false));
d.setDevMobilePay(devSet.optBoolean("DevMobilePay", false));
d.setDevCardPay(devSet.optBoolean("DevCardPay", false));
d.setDevScanPay(devSet.optBoolean("DevScanPay", false));
// VMC/Electic 雲端不下發 不碰保留本地值
}
JSONObject cashSet = config.optJSONObject("CashSet");
if (cashSet != null) {
CashSetStructure c = MyApp.getInstance().getCashSetStructure();
c.setBillF1000(cashSet.optBoolean("BillF1000", false));
c.setBillE500(cashSet.optBoolean("BillE500", false));
c.setBillD100(cashSet.optBoolean("BillD100", false));
c.setCoinF50(cashSet.optBoolean("CoinF50", false));
c.setCoinE10(cashSet.optBoolean("CoinE10", false));
c.setCoinD5(cashSet.optBoolean("CoinD5", false));
c.setCoinC1(cashSet.optBoolean("CoinC1", false));
}
// 6. FunctionSet取貨模組/取貨碼/通行碼/來店禮/會員/環溫
// 取貨碼通行碼從屬取貨模組PickupModule來店禮獨立判斷
JSONObject funcSet = config.optJSONObject("FunctionSet");
if (funcSet != null) {
DevSetStructure d = MyApp.getInstance().getDevSet();
d.setPickupModule(funcSet.optBoolean("PickupModule", false));
d.setPickupCode(funcSet.optBoolean("PickupCode", false));
d.setPassCode(funcSet.optBoolean("PassCode", false));
d.setWelcomeGift(funcSet.optBoolean("WelcomeGift", false));
d.setMemberSystem(funcSet.optBoolean("MemberSystem", false));
d.setAmbientTemp(funcSet.optBoolean("AmbientTemp", false));
}
// 6c. HardwareSet 貨道型式缺整個物件不動本地比照 DevSet/CashSet 防呆
JSONObject hardwareSet = config.optJSONObject("HardwareSet");
if (hardwareSet != null) {
com.unibuy.smartdevice.structure.HardwareSetStructure h = MyApp.getInstance().getHardwareSetStructure();
h.setSpringSlot1_10(hardwareSet.optBoolean("SpringSlot1_10", false));
h.setSpringSlot11_20(hardwareSet.optBoolean("SpringSlot11_20", false));
h.setSpringSlot21_30(hardwareSet.optBoolean("SpringSlot21_30", false));
h.setSpringSlot31_40(hardwareSet.optBoolean("SpringSlot31_40", false));
h.setSpringSlot41_50(hardwareSet.optBoolean("SpringSlot41_50", false));
h.setSpringSlot51_60(hardwareSet.optBoolean("SpringSlot51_60", false));
}
// 6b. ShoppingMode購買流程模式basic/employee_card/pickup_sheet取代舊 project flavor
boolean hasShoppingMode = config.has("ShoppingMode");
if (hasShoppingMode) {
MyApp.getInstance().setShoppingMode(config.optString("ShoppingMode", "basic"));
}
// 6d. LangSet機台啟用語系最多 5 有序 0 個為預設後台單向下發機台僅顯示與切換
JSONObject langSet = config.optJSONObject("LangSet");
if (langSet != null) {
java.util.List<String> langs = new java.util.ArrayList<>();
org.json.JSONArray arr = langSet.optJSONArray("Languages");
if (arr != null) {
for (int i = 0; i < arr.length(); i++) {
String code = arr.optString(i, "");
if (code != null && !code.isEmpty()) {
langs.add(com.unibuy.smartdevice.utils.LanguageHelper.canonicalize(code));
}
}
}
MyApp.getInstance().setMachineLanguages(langs);
String def = langSet.optString("Default", "");
String canonicalDef = (def == null || def.isEmpty())
? (langs.isEmpty() ? "" : langs.get(0))
: com.unibuy.smartdevice.utils.LanguageHelper.canonicalize(def);
MyApp.getInstance().setDefaultLanguage(canonicalDef);
// 首次使用者尚未手動選過語言套用機台預設語系
android.content.SharedPreferences prefs = handlerMain.getContext()
.getSharedPreferences("app_prefs", android.content.Context.MODE_PRIVATE);
if (!canonicalDef.isEmpty() && prefs.getString("language", null) == null) {
com.unibuy.smartdevice.utils.LanguageHelper.changeLanguage(handlerMain.getContext(), canonicalDef);
}
}
if (devSet != null || cashSet != null || funcSet != null || hardwareSet != null || hasShoppingMode || langSet != null) {
MyApp.saveSystemSet(handlerMain.getContext());
}
// 7. 重要將所有設定同步回資料庫防止 App 重啟後消失
// 5. 重要將所有設定同步回資料庫防止 App 重啟後消失
persistCurrentSettings();
if (reconnectMqtt) {
MyApp.getInstance().initMqtt();
}
MyApp.getInstance().initMqtt();
}
/**
@ -1130,12 +635,7 @@ public class StarCloudAPI {
Log.i(TAG, "persistCurrentSettings() - Saving to DB: machineID=" + MyApp.getInstance().getMachine().getMachineID()
+ ", token=" + MyApp.getInstance().getMachine().getApiKey());
SettingsDao settingsDao = new SettingsDao(handlerMain.getContext());
// 只刪除本方法接下來要重寫的這 4 個設定 key避免 deleteAll() 連坐刪掉廣告(VideoEoxPlayer)等其他持久資料
// 否則遠端更新設定重啟會因廣告列被清空且啟動不會主動抓 B005導致廣告消失
settingsDao.deleteWhere(MyApp.getInstance().getMachine().getClass().getSimpleName());
settingsDao.deleteWhere(MyApp.getInstance().getEsunpay().getClass().getSimpleName());
settingsDao.deleteWhere(MyApp.getInstance().getGreenInvoice().getClass().getSimpleName());
settingsDao.deleteWhere(MyApp.getInstance().getGreenpaySetting().getClass().getSimpleName());
settingsDao.deleteAll();
settingsDao.insertOne(new SettingStructure(MyApp.getInstance().getMachine().getClass().getSimpleName(),
new String[] {
@ -1172,10 +672,6 @@ public class StarCloudAPI {
private void saveProducts(JSONArray products) throws JSONException {
MyApp.getInstance().getProductList().clear();
// 商品名稱多語 blob商品ID t060v01_i18n底線代碼 key存於 MemoDao 持久化不動 products
JSONObject nameI18nBlob = new JSONObject();
// 商品規格多語 blob商品ID t060v03_i18n純文字含換行
JSONObject specI18nBlob = new JSONObject();
for (int i = 0; i < products.length(); i++) {
JSONObject p = products.getJSONObject(i);
@ -1192,33 +688,8 @@ public class StarCloudAPI {
p.optString("t060v41", ""), // Material
p.optString("t060v01_en", ""),
p.optString("t060v01_jp", ""));
// 新增商品名稱多語 mapB012 t060v01_i18n支援後台啟用的所有語系
JSONObject nameI18n = p.optJSONObject("t060v01_i18n");
if (nameI18n != null) {
product.setNameI18nJson(nameI18n.toString());
String pid = p.optString("t060v00", "");
if (!pid.isEmpty()) {
try { nameI18nBlob.put(pid, nameI18n); } catch (JSONException ignore) {}
}
}
// 新增商品規格多語 mapB012 t060v03_i18n純文字含換行
JSONObject specI18n = p.optJSONObject("t060v03_i18n");
if (specI18n != null) {
product.setSpecI18nJson(specI18n.toString());
String pid = p.optString("t060v00", "");
if (!pid.isEmpty()) {
try { specI18nBlob.put(pid, specI18n); } catch (JSONException ignore) {}
}
}
MyApp.getInstance().getProductList().add(product);
}
// 持久化商品多語 blob供離線重啟時 fallback
MyApp.getInstance().setProductNameI18nBlob(nameI18nBlob);
MyApp.saveProductNameI18n(handlerMain.getContext());
MyApp.getInstance().setProductSpecI18nBlob(specI18nBlob);
MyApp.saveProductSpecI18n(handlerMain.getContext());
// 將商品同步至資料庫
ProductsDao productsDao = new ProductsDao(handlerMain.getContext());

View File

@ -470,51 +470,6 @@ public class MqttService extends Service {
logs.info("Failed to start remote app update: " + e.getMessage());
mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage());
}
} else if ("update_settings".equals(cmd)) {
logs.info("Executing remote settings sync command...");
// 🚩 忙碌時拒收交易/購物車/對話框進行中避免支付開關等設定中途被改動
// 後台 RemoteCommand 1 分鐘 timeout閒置後可重發
String busyReason = MyApp.getInstance().getBusyReason();
if (!busyReason.isEmpty()) {
logs.info("Remote settings sync rejected: " + busyReason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + busyReason);
} else {
// 📢 在主執行緒顯示 Toast 提示使用者
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
android.widget.Toast.makeText(getApplicationContext(),
"📢 系統設定更新中...", android.widget.Toast.LENGTH_SHORT).show());
com.unibuy.smartdevice.tools.HandlerMain settingHandler = new com.unibuy.smartdevice.tools.HandlerMain(
getApplicationContext(), logs) {
@Override
protected void execute(Context context, int code, String message) {
logs.info("Settings sync event: [" + code + "] " + message);
if (code == 200 && "綁定成功".equals(message)) {
// 1. 先發 ACK確保在 MQTT 重連之前送達後台
mqttManager.publishCommandAck(cmdId, "success",
"Settings sync completed successfully");
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
android.widget.Toast.makeText(context,
"✅ 系統設定更新完成", android.widget.Toast.LENGTH_SHORT).show());
// 2. 延後重連 MQTTtoken 可能已輪換留時間讓 ACK 先送出
new android.os.Handler(android.os.Looper.getMainLooper())
.postDelayed(() -> MyApp.getInstance().initMqtt(), 1500);
} else if (code >= 400) {
mqttManager.publishCommandAck(cmdId, "failed",
"Settings sync failed: " + message);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
android.widget.Toast.makeText(context,
"❌ 系統設定更新失敗: " + message, android.widget.Toast.LENGTH_LONG).show());
}
}
};
// 只重抓設定B014不串接商品/廣告reconnectMqtt=false改由上方 ACK 後再重連
new com.unibuy.smartdevice.external.StarCloudAPI(settingHandler, settingHandler).downloadSetting(false);
}
} else {
logs.info("Unknown MQTT command: " + cmd);
mqttManager.publishCommandAck(cmdId, "failed", "Unknown command");

View File

@ -15,25 +15,10 @@ public class DevSetStructure {
private boolean isTapPay32;
private boolean isTapPay33;
private boolean isTapPay34;
// 取貨碼/通行碼改由雲端 B014 FunctionSet 下發控制預設值來自 flavor devset_defaults.xml
private boolean isPickupCode;
private boolean isPassCode;
// 以下為 B014 新增開關預設 false由雲端下發或主控台系統方設定後覆寫
// DevSet 細項刷卡機/掃碼底下的子開關
private boolean isDevCreditCard;
private boolean isDevMobilePay;
private boolean isDevCardPay;
private boolean isDevScanPay;
// FunctionSet 細項取貨模組來店禮會員系統環境溫度監控
private boolean isPickupModule;
private boolean isWelcomeGift;
private boolean isMemberSystem;
private boolean isAmbientTemp;
public DevSetStructure(boolean isVMC, boolean isElectic, boolean isShoppingCar, boolean isInvoice,
boolean isDevNFCPay, boolean isDevEsunPay, boolean isDevTapPay, boolean isDevCash, boolean isDevLinePay,
boolean isTapPay30,boolean isTapPay31,boolean isTapPay32,boolean isTapPay33, boolean isTapPay34,
boolean isPickupCode, boolean isPassCode) {
boolean isTapPay30,boolean isTapPay31,boolean isTapPay32,boolean isTapPay33, boolean isTapPay34) {
this.isVMC = isVMC;
this.isElectic = isElectic;
this.isShoppingCar = isShoppingCar;
@ -48,8 +33,6 @@ public class DevSetStructure {
this.isTapPay32 = isTapPay32;
this.isTapPay33 = isTapPay33;
this.isTapPay34 = isTapPay34;
this.isPickupCode = isPickupCode;
this.isPassCode = isPassCode;
}
public boolean isVMC() {
@ -141,72 +124,4 @@ public class DevSetStructure {
public void setTapPay33(boolean tapPay33) {isTapPay33 = tapPay33;}
public void setTapPay34(boolean tapPay34) {isTapPay34 = tapPay34;}
public boolean isPickupCode() {
return isPickupCode;
}
public void setPickupCode(boolean pickupCode) {
isPickupCode = pickupCode;
}
public boolean isPassCode() {
return isPassCode;
}
public void setPassCode(boolean passCode) {
isPassCode = passCode;
}
public boolean isDevCreditCard() { return isDevCreditCard; }
public void setDevCreditCard(boolean devCreditCard) { isDevCreditCard = devCreditCard; }
public boolean isDevMobilePay() { return isDevMobilePay; }
public void setDevMobilePay(boolean devMobilePay) { isDevMobilePay = devMobilePay; }
public boolean isDevCardPay() { return isDevCardPay; }
public void setDevCardPay(boolean devCardPay) { isDevCardPay = devCardPay; }
public boolean isDevScanPay() { return isDevScanPay; }
public void setDevScanPay(boolean devScanPay) { isDevScanPay = devScanPay; }
public boolean isPickupModule() { return isPickupModule; }
public void setPickupModule(boolean pickupModule) { isPickupModule = pickupModule; }
public boolean isWelcomeGift() { return isWelcomeGift; }
public void setWelcomeGift(boolean welcomeGift) { isWelcomeGift = welcomeGift; }
public boolean isMemberSystem() { return isMemberSystem; }
public void setMemberSystem(boolean memberSystem) { isMemberSystem = memberSystem; }
public boolean isAmbientTemp() { return isAmbientTemp; }
public void setAmbientTemp(boolean ambientTemp) { isAmbientTemp = ambientTemp; }
// 支付方式 && 便利判斷
// 各支付實際是否可用 = 母開關開啟 && 子項開啟集中於此 UI 顯示與入口防呆共用同一份真相
// 避免在多處BuyDialog 購物車/單品/例外分支changeCheckout 入口各自拼湊而漏判
// 刷卡機 isDevNFCPay 信用卡 / 電子票證(卡片) / 手機支付
// 掃碼 isDevScanPay 玉山 EsunPay / TapPay
// Line 官方支付現金獨立大項無母開關自身即為總開關
/** flowType 1 信用卡:刷卡機開啟且信用卡子項開啟。 */
public boolean canCreditCard() { return isDevNFCPay && isDevCreditCard; }
/** flowType 2 電子票證(卡片支付):刷卡機開啟且卡片支付子項開啟。 */
public boolean canCardPay() { return isDevNFCPay && isDevCardPay; }
/** flowType 4 手機支付:刷卡機開啟且手機支付子項開啟。 */
public boolean canMobilePay() { return isDevNFCPay && isDevMobilePay; }
/** flowType 3 玉山 EsunPay掃碼開啟且玉山子項開啟。 */
public boolean canEsunPay() { return isDevScanPay && isDevEsunPay; }
/** flowType 30 TapPay掃碼開啟且 TapPay 子項開啟。 */
public boolean canTapPay() { return isDevScanPay && isDevTapPay; }
/** flowType 70 Line 官方支付:獨立大項,不從屬掃碼,只看自身開關。 */
public boolean canLinePay() { return isDevLinePay; }
/** flowType 9 現金:獨立大項,只看自身開關。 */
public boolean canCash() { return isDevCash; }
}

View File

@ -1,43 +0,0 @@
package com.unibuy.smartdevice.structure;
/**
* HardwareSet硬體貨道類型設定B014 HardwareSet
* 每個欄位代表一段貨道10 槽一組的型式true=彈簧 / false=履帶
* 由雲端 B014 下發主控台系統方可編輯後回寫雲端預設 false
*/
public class HardwareSetStructure {
private boolean springSlot1_10;
private boolean springSlot11_20;
private boolean springSlot21_30;
private boolean springSlot31_40;
private boolean springSlot41_50;
private boolean springSlot51_60;
public HardwareSetStructure(boolean springSlot1_10, boolean springSlot11_20, boolean springSlot21_30,
boolean springSlot31_40, boolean springSlot41_50, boolean springSlot51_60) {
this.springSlot1_10 = springSlot1_10;
this.springSlot11_20 = springSlot11_20;
this.springSlot21_30 = springSlot21_30;
this.springSlot31_40 = springSlot31_40;
this.springSlot41_50 = springSlot41_50;
this.springSlot51_60 = springSlot51_60;
}
public boolean isSpringSlot1_10() { return springSlot1_10; }
public void setSpringSlot1_10(boolean v) { springSlot1_10 = v; }
public boolean isSpringSlot11_20() { return springSlot11_20; }
public void setSpringSlot11_20(boolean v) { springSlot11_20 = v; }
public boolean isSpringSlot21_30() { return springSlot21_30; }
public void setSpringSlot21_30(boolean v) { springSlot21_30 = v; }
public boolean isSpringSlot31_40() { return springSlot31_40; }
public void setSpringSlot31_40(boolean v) { springSlot31_40 = v; }
public boolean isSpringSlot41_50() { return springSlot41_50; }
public void setSpringSlot41_50(boolean v) { springSlot41_50 = v; }
public boolean isSpringSlot51_60() { return springSlot51_60; }
public void setSpringSlot51_60(boolean v) { springSlot51_60 = v; }
}

View File

@ -1,9 +1,5 @@
package com.unibuy.smartdevice.structure;
import android.content.Context;
import com.unibuy.smartdevice.utils.LanguageHelper;
public class ProductStructure {
private String productID;
private String productImg;
@ -119,103 +115,6 @@ public class ProductStructure {
this.productNameJp = productNameJp;
}
// B012 t060v01_i18n 原始 JSON 字串底線代碼 key {"zh_TW":..,"en":..,"ko":..}
// B012 解析時設入離線從 DB 重新載入商品時此欄為空改向 MyApp 持久化 blob
private String nameI18nJson;
public String getNameI18nJson() {
return nameI18nJson;
}
public void setNameI18nJson(String nameI18nJson) {
this.nameI18nJson = nameI18nJson;
}
// 依目前語言取商品名稱優先順序
// 1) B012 多語 map 對應語系 2) map zh_TW 3) 舊欄位 _en/_jp向下相容 4) 中文主值
// 所有顯示商品名稱的地方都應呼叫這個方法避免各自重複判斷導致顯示不一致
public String getLocalizedName(Context context) {
String canonical = LanguageHelper.getSavedLanguage(context); // zh-TW / en / ko ...
String backendLocale = LanguageHelper.toBackendLocale(canonical); // zh_TW / en / ko ...
// 1) 商品多語 map目前語系
String fromMap = lookupNameI18n(backendLocale);
if (notBlank(fromMap)) return fromMap;
// 2) 商品多語 mapzh_TW後台保證的 fallback 主值
if (!"zh_TW".equals(backendLocale)) {
String zh = lookupNameI18n("zh_TW");
if (notBlank(zh)) return zh;
}
// 3) 舊欄位 _en/_jp線上舊資料相容 map 缺時兜底
String productLang = LanguageHelper.toProductLang(canonical); // en / ja / zh
if ("en".equals(productLang) && notBlank(productNameEn)) return productNameEn;
if ("ja".equals(productLang) && notBlank(productNameJp)) return productNameJp;
// 4) 中文主值
return productName;
}
// i18n map 取指定後台 locale 的名稱本物件欄位為空時改向 MyApp 持久化 blob 離線重啟
private String lookupNameI18n(String backendLocale) {
String json = nameI18nJson;
if (json == null || json.isEmpty()) {
json = com.unibuy.smartdevice.MyApp.getInstance().getProductNameI18nJson(productID);
}
if (json == null || json.isEmpty()) return null;
try {
return new org.json.JSONObject(json).optString(backendLocale, null);
} catch (org.json.JSONException e) {
return null;
}
}
// B012 t060v03_i18n 原始 JSON 字串底線代碼 key {"zh_TW":..,"en":..}
// 內容為純文字含換行\n顯示時以 setText 原樣呈現保留換行/空格/縮排
// B012 解析時設入離線從 DB 重新載入商品時此欄為空改向 MyApp 持久化 blob
private String specI18nJson;
public String getSpecI18nJson() {
return specI18nJson;
}
public void setSpecI18nJson(String specI18nJson) {
this.specI18nJson = specI18nJson;
}
// 依目前語言取商品規格描述順序對應語系 zh_TW 空字串無描述
public String getLocalizedSpec(Context context) {
String canonical = LanguageHelper.getSavedLanguage(context);
String backendLocale = LanguageHelper.toBackendLocale(canonical);
String fromMap = lookupSpecI18n(backendLocale);
if (notBlank(fromMap)) return fromMap;
if (!"zh_TW".equals(backendLocale)) {
String zh = lookupSpecI18n("zh_TW");
if (notBlank(zh)) return zh;
}
return "";
}
private String lookupSpecI18n(String backendLocale) {
String json = specI18nJson;
if (json == null || json.isEmpty()) {
json = com.unibuy.smartdevice.MyApp.getInstance().getProductSpecI18nJson(productID);
}
if (json == null || json.isEmpty()) return null;
try {
return new org.json.JSONObject(json).optString(backendLocale, null);
} catch (org.json.JSONException e) {
return null;
}
}
private static boolean notBlank(String s) {
return s != null && !s.trim().isEmpty();
}
public int getRealPrice() {
int machinePrice = getMachinePrice();
int sellingPrice = getSellingPrice();

View File

@ -15,9 +15,6 @@ public class ReportFlowInfoStructure {
int giveChange;
int usePoints;
String reportFlowId;
// MQTT 交易結案流水號進入付款時生成一次pending completed/failed 全程共用同一個
// 讓後台依同一 flow_id 做狀態轉移 reportFlowId取貨碼/通行碼驗證碼B600 legacy語意不同獨立一欄
String mqttFlowId;
JSONArray shoppingCartInfoByJsonArray;
public ReportFlowInfoStructure(String orderId, int flowType, String flowSendInfo, String flowRequestInfo, String productId, int totalPrice, String invoiceInfo, String flowBarCode, int flowStatusType, int giveChange, int usePoints, JSONArray shoppingCartInfoByJsonArray) {
@ -139,14 +136,6 @@ public class ReportFlowInfoStructure {
this.reportFlowId = reportFlowId;
}
public String getMqttFlowId() {
return mqttFlowId;
}
public void setMqttFlowId(String mqttFlowId) {
this.mqttFlowId = mqttFlowId;
}
@Override
public String toString() {
return "ReportFlowInfoStructure{" +

View File

@ -8,9 +8,6 @@ public class ReportInvoiceInfoStructure {
String invoiceDate;
String randomNumber;
String loveCode;
// 綠界 RelateNumber冪等鍵= machineID + flow_id DispatchDialog 開票前產生
// 開票請求與 finalize 上報共用同一值後台據此查詢/補開避免重複開立
String relateNumber = "";
public ReportInvoiceInfoStructure(String reportFlowId, String rtnCode, String rtnMsg, String invoiceNo, String invoiceDate, String randomNumber, String loveCode) {
this.reportFlowId = reportFlowId;
@ -78,14 +75,6 @@ public class ReportInvoiceInfoStructure {
this.loveCode = loveCode;
}
public String getRelateNumber() {
return relateNumber;
}
public void setRelateNumber(String relateNumber) {
this.relateNumber = relateNumber;
}
@Override
public String toString() {
return "ReportInvoiceInfoStructure{" +

View File

@ -10,7 +10,6 @@ public class SlotStructure {
private boolean lock;
private int secord;
private JSONObject otherData;
public int cartSelectedCount;// 本地欄位購物車已選數量General flavor 多品項購物車使用
/*
JSONObject jsonObject = new JSONObject();

View File

@ -35,14 +35,6 @@ public class TransactionFinalizePayload {
/** 每個貨道的出貨結果列表 */
private List<DispenseRecord> dispense;
/**
* 電子發票開立結果選填
* 機台在進入出貨流程時平行向綠界開立結果折進此物件隨 finalize 一起上報
* 若開票尚未回應則 status=pending後台依 relate_number 去綠界查詢/補開
* 無發票需求未帶發票資訊時此欄為 null後台不建立發票
*/
private InvoicePayload invoice;
// =========== Getters & Setters ===========
public String getAction() {
@ -90,74 +82,6 @@ public class TransactionFinalizePayload {
this.dispense = dispense;
}
public InvoicePayload getInvoice() {
return invoice;
}
public void setInvoice(InvoicePayload invoice) {
this.invoice = invoice;
}
// =========== 內部類別電子發票開立結果 ===========
public static class InvoicePayload {
/** 開立狀態issued已開/ pending已送出未回應/ failed綠界回失敗 */
private String status;
/** 綠界 RelateNumber= machineID + flow_id後台查詢/補開用 */
private String relate_number;
/** 發票號碼pending/failed 時為空) */
private String invoice_no;
/** 綠界回應碼1=成功) */
private String rtn_code;
/** 綠界回應訊息 */
private String rtn_msg;
/** 發票開立日期 */
private String invoice_date;
/** 隨機碼 */
private String random_number;
/** 愛心碼(捐贈時) */
private String love_code;
/** 載具號碼 */
private String carrier_id;
/** 統一編號B2B 統編,有打統編時) */
private String business_tax_id;
/** 發票金額 */
private int amount;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRelate_number() { return relate_number; }
public void setRelate_number(String relate_number) { this.relate_number = relate_number; }
public String getInvoice_no() { return invoice_no; }
public void setInvoice_no(String invoice_no) { this.invoice_no = invoice_no; }
public String getRtn_code() { return rtn_code; }
public void setRtn_code(String rtn_code) { this.rtn_code = rtn_code; }
public String getRtn_msg() { return rtn_msg; }
public void setRtn_msg(String rtn_msg) { this.rtn_msg = rtn_msg; }
public String getInvoice_date() { return invoice_date; }
public void setInvoice_date(String invoice_date) { this.invoice_date = invoice_date; }
public String getRandom_number() { return random_number; }
public void setRandom_number(String random_number) { this.random_number = random_number; }
public String getLove_code() { return love_code; }
public void setLove_code(String love_code) { this.love_code = love_code; }
public String getCarrier_id() { return carrier_id; }
public void setCarrier_id(String carrier_id) { this.carrier_id = carrier_id; }
public String getBusiness_tax_id() { return business_tax_id; }
public void setBusiness_tax_id(String business_tax_id) { this.business_tax_id = business_tax_id; }
public int getAmount() { return amount; }
public void setAmount(int amount) { this.amount = amount; }
}
// =========== 內部類別訂單資訊 ===========
public static class OrderPayload {
@ -191,17 +115,10 @@ public class TransactionFinalizePayload {
private int payment_type;
/**
* 支付狀態1 = 成功, 0 = 失敗/未付款
* 支付狀態1 = 成功
*/
private int payment_status;
/**
* 交易生命週期狀態後台 Order.status
* "pending"進付款待結果/ "completed"付款成功/ "failed"付款失敗/ "abandoned"
* 不帶null時後台預設 "completed"確保線上 main 機台行為不變
*/
private String status;
/**
* 訂單出貨狀態0 = 全部失敗, 1 = 全部成功, 2 = 部分失敗
*/
@ -298,14 +215,6 @@ public class TransactionFinalizePayload {
this.payment_status = payment_status;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public int getDelivery_status() {
return delivery_status;
}

View File

@ -346,12 +346,8 @@ public class Tools {
public void setImmOpenByFocusChange(EditText editText) {
editText.requestFocus();
editText.setSelection(editText.getText().length());
// 使用 post() 延遲執行確保 hideSystemBars() 觸發的 Window 重繪完成後才彈出鍵盤
// 若直接呼叫 showSoftInputWindow flag 變更會清除 focus導致鍵盤無法彈出
editText.post(() -> {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
});
InputMethodManager imm = (InputMethodManager) context.getSystemService(context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
public void setImmHideByFocusChange(EditText editText) {

View File

@ -4,9 +4,7 @@ import android.content.Context;
import android.util.Log;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.external.mqtt.MqttManager;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.InvoiceStructure;
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;
@ -47,73 +45,20 @@ public class TransactionFinalizeBuilder {
/** 累積的出貨結果清單 */
private final List<TransactionFinalizePayload.DispenseRecord> dispenseRecords = new ArrayList<>();
/**
* 商品明細快照buyList 仍完整時擷取例如進入出貨流程當下
* 之後 build() 會優先採用此快照不再即時讀 live buyList
* 用以修正掃碼支付(玉山)等較慢流程結案前 buyList 可能被取消/倒數路徑清空
* 導致 order.items 變成空陣列出現無商品紀錄但出貨其實成功
*/
private List<TransactionFinalizePayload.ItemRecord> itemsSnapshot = null;
private String codeId = "0";
/** 交易生命週期狀態pending/completed/failed/abandoned預設 completed 維持成功路徑原行為。 */
private String status = "completed";
/** 支付狀態1=成功、0=失敗/未付款;預設 1 維持成功路徑原行為。 */
private int paymentStatus = 1;
/** 失敗結果碼(如刷卡機 0015非空時折進 payment_response 方便後台查閱。 */
private String resultCode = null;
// 訂單層級欄位的快照全域 flowInfo 仍正確時由 captureOrderInfo() 擷取
// null 代表未快照 build() 退回讀全域即時值保留 publishPending/publishFailed 等同步路徑的原行為
// 出貨是個長流程期間全域 flowInfo 可能被別的路徑殘留加購倒數彈出 BuyDialog
// initialReportData()洗回 0用快照可避免 payment_type/total 在結案當下被誤讀成 0未知
// 也避免通行碼(5)的不上報判斷失效與既有 captureItems()商品明細快照一致
private Integer snapFlowType = null;
private Integer snapTotalAmount = null;
private Integer snapChangeAmount = null;
private Integer snapPointsUsed = null;
public TransactionFinalizeBuilder() {
this.orderMachineTime = SDF.format(new Date());
// flow_id 共用進入付款時生成的 mqttFlowId pending completed/failed 為同一筆
// 後台才能依 flow_id 做狀態轉移若尚未生成防禦則即時生成並回存 flowInfo
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
String existing = flowInfo != null ? flowInfo.getMqttFlowId() : null;
if (existing != null && !existing.isEmpty()) {
this.flowId = existing;
} else {
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
if (flowInfo != null) {
flowInfo.setMqttFlowId(this.flowId);
}
}
// 在進入出貨流程時立即生成本次交易流水號一次交易只生成一次
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
this.orderNo = this.flowId;
Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId);
}
public void setStatus(String status) {
this.status = status;
}
public void setPaymentStatus(int paymentStatus) {
this.paymentStatus = paymentStatus;
}
public void setResultCode(String resultCode) {
this.resultCode = resultCode;
}
public void setFlowId(String flowId) {
this.flowId = flowId;
}
public String getFlowId() {
return flowId;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
@ -122,133 +67,6 @@ public class TransactionFinalizeBuilder {
this.codeId = codeId;
}
// =========================================================================
// 交易生命週期上報pending / failed只在 basic 購物模式本專案 general/demo才送
// - basic進付款時生成的同一 flow_id上報後台依 flow_id pending completed/failed 轉移
// - employee_card(晟崴)/pickup_sheet(中國醫)無實際付款直接 return 不送後台資料不受影響
// shopping_mode 判斷雲端 B014 已逐機台下發不另設旗標
// 成功(completed)仍由 DispatchDialog 出貨完成後的既有流程上報不在此處
// =========================================================================
/** 僅 basic 購物模式才上報生命週期;其餘模式不送。 */
private static boolean lifecycleLoggingEnabled() {
return "basic".equals(MyApp.getInstance().getShoppingMode());
}
/** 進入付款時上報 pending進付款一律留底。非 basic 模式則不送。 */
public static void publishPending() {
if (!lifecycleLoggingEnabled()) {
return;
}
try {
TransactionFinalizeBuilder b = new TransactionFinalizeBuilder();
b.setStatus("pending");
b.setPaymentStatus(0);
MqttManager.getInstance().publishTransactionFinalize(b.build());
Log.i(TAG, "published pending finalize, flow_id=" + b.getFlowId());
} catch (Exception e) {
Log.w(TAG, "publishPending failed: " + e.getMessage());
}
}
/** 付款失敗/逾時時上報 failed救回原本默默消失的失敗單。非 basic 模式則不送。 */
public static void publishFailed(String resultCode) {
if (!lifecycleLoggingEnabled()) {
return;
}
try {
TransactionFinalizeBuilder b = new TransactionFinalizeBuilder();
b.setStatus("failed");
b.setPaymentStatus(0);
b.setResultCode(resultCode);
MqttManager.getInstance().publishTransactionFinalize(b.build());
Log.i(TAG, "published failed finalize code=" + resultCode + ", flow_id=" + b.getFlowId());
} catch (Exception e) {
Log.w(TAG, "publishFailed failed: " + e.getMessage());
}
}
/**
* 擷取目前 buyList 的商品明細快照
* 應在buyList 仍完整時呼叫例如 DispatchDialog 進入出貨流程當下
* 之後 {@link #build()} 會優先採用此快照不再即時讀 live buyList
* 避免結案前 buyList 被取消/倒數等其他執行緒清空導致 order.items 變成空陣列
* buyList 為空時不覆寫既有快照避免把有效快照洗成空
*/
public void captureItems() {
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
if (buyList == null || buyList.isEmpty()) {
return;
}
this.itemsSnapshot = buildItemRecords(buyList);
Log.i(TAG, "captureItems: snapshot " + this.itemsSnapshot.size()
+ " item(s), flow_id=" + flowId);
}
/**
* 擷取訂單層級欄位flowType / 總金額 / 找零 / 點數的快照
* 應在全域 flowInfo 已是本筆最終值時呼叫例如 DispatchDialog 進入出貨 CMUH42
* 覆寫之後之後 {@link #build()} 通行碼不上報判斷皆優先採用此快照不再即時讀全域
* 避免結案前全域被其他路徑清空導致 payment_type/total 落地成 0未知或誤報通行碼
*/
public void captureOrderInfo() {
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
if (flowInfo == null) {
return;
}
this.snapFlowType = flowInfo.getFlowType();
this.snapTotalAmount = flowInfo.getTotalPrice();
this.snapChangeAmount = flowInfo.getGiveChange();
this.snapPointsUsed = flowInfo.getUsePoints();
Log.i(TAG, "captureOrderInfo: flowType=" + snapFlowType + ", total=" + snapTotalAmount
+ ", flow_id=" + flowId);
}
/** 取得快照的 flowType尚未快照時退回全域即時值。供「通行碼(5)不上報」判斷使用。 */
public int getOrderFlowType() {
if (snapFlowType != null) {
return snapFlowType;
}
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
return flowInfo != null ? flowInfo.getFlowType() : 0;
}
// 訂單欄位取值有快照用快照否則退回全域即時值build() 共用
private int effectiveFlowType(ReportFlowInfoStructure flowInfo) {
return snapFlowType != null ? snapFlowType : flowInfo.getFlowType();
}
private int effectiveTotalAmount(ReportFlowInfoStructure flowInfo) {
return snapTotalAmount != null ? snapTotalAmount : flowInfo.getTotalPrice();
}
private int effectiveChangeAmount(ReportFlowInfoStructure flowInfo) {
return snapChangeAmount != null ? snapChangeAmount : flowInfo.getGiveChange();
}
private int effectivePointsUsed(ReportFlowInfoStructure flowInfo) {
return snapPointsUsed != null ? snapPointsUsed : flowInfo.getUsePoints();
}
/** 由 buyList 組出 items 明細captureItems 與 build() fallback 共用)。 */
private List<TransactionFinalizePayload.ItemRecord> buildItemRecords(List<BuyStructure> buyList) {
List<TransactionFinalizePayload.ItemRecord> items = new ArrayList<>();
if (buyList != null) {
for (BuyStructure buy : buyList) {
int productId = 0;
try {
productId = Integer.parseInt(buy.getProduct().getProductID());
} catch (NumberFormatException e) {
Log.w(TAG, "Cannot parse productID to int: " + buy.getProduct().getProductID());
}
int price = buy.getProduct().getRealPrice();
int quantity = buy.getCount() > 0 ? buy.getCount() : 1;
items.add(new TransactionFinalizePayload.ItemRecord(productId, price, quantity));
}
}
return items;
}
/**
* 加入一筆出貨結果記錄每個貨道出貨後呼叫一次
@ -305,7 +123,7 @@ public class TransactionFinalizeBuilder {
// --- payment_type支付類型 (Root 級別) ---
payload.setPayment_type(effectiveFlowType(flowInfo));
payload.setPayment_type(flowInfo.getFlowType());
// --- 組裝 order ---
TransactionFinalizePayload.OrderPayload order = buildOrderPayload(flowInfo);
@ -315,26 +133,6 @@ public class TransactionFinalizeBuilder {
// --- 出貨記錄 ---
payload.setDispense(new ArrayList<>(dispenseRecords));
// --- 電子發票改由後台開立---
// 機台不開票只把開立輸入載具/愛心碼/統編/金額帶上去狀態固定 pending
// 後台建 pending 發票後派 Job 去綠界開立無發票需求invoiceInfo 為空時不帶後台不建立發票
// 只有付款成功(completed)才建立發票pending/failed 不開票避免對未成交的交易開立
if ("completed".equals(status)
&& flowInfo.getInvoiceInfo() != null && !flowInfo.getInvoiceInfo().isEmpty()) {
TransactionFinalizePayload.InvoicePayload invoicePayload =
new TransactionFinalizePayload.InvoicePayload();
invoicePayload.setStatus("pending");
invoicePayload.setAmount(effectiveTotalAmount(flowInfo));
InvoiceStructure invoiceData = MyApp.getInstance().getInvoiceData();
if (invoiceData != null) {
invoicePayload.setCarrier_id(invoiceData.getCarrier());
invoicePayload.setBusiness_tax_id(invoiceData.getCustomerIdentifier());
invoicePayload.setLove_code(invoiceData.getLoveCode());
}
payload.setInvoice(invoicePayload);
}
Log.i(TAG, "TransactionFinalizePayload built: flow_id=" + flowId
+ ", order_no=" + order.getOrder_no()
+ ", items=" + order.getItems().size()
@ -372,29 +170,24 @@ public class TransactionFinalizeBuilder {
order.setOrder_no(orderNo != null && !orderNo.isEmpty() ? orderNo : flowId);
// 金額資訊優先採快照未快照時退回全域即時值
int totalAmount = effectiveTotalAmount(flowInfo);
// 金額資訊
int totalAmount = flowInfo.getTotalPrice();
order.setOriginal_amount(totalAmount);
order.setDiscount_amount(0); // TODO: 如有折扣活動需進一步填入
order.setTotal_amount(totalAmount);
order.setPay_amount(totalAmount);
order.setChange_amount(effectiveChangeAmount(flowInfo));
order.setPoints_used(effectivePointsUsed(flowInfo));
order.setChange_amount(flowInfo.getGiveChange());
order.setPoints_used(flowInfo.getUsePoints());
// 支付類型flowType 直接對應 payment_type
order.setPayment_type(effectiveFlowType(flowInfo));
// 生命週期狀態與支付狀態成功路徑沿用預設(completed/1)pending/failed setter 覆寫
order.setStatus(status);
order.setPayment_status(paymentStatus);
order.setPayment_type(flowInfo.getFlowType());
order.setPayment_status(1); // 走到出貨流程代表支付已成功
// 支付流程原始資料
order.setPayment_request(
flowInfo.getFlowSendInfo() != null ? flowInfo.getFlowSendInfo() : "");
String paymentResponse = flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "";
if (resultCode != null && !resultCode.isEmpty()) {
paymentResponse = paymentResponse + " | code=" + resultCode;
}
order.setPayment_response(paymentResponse);
order.setPayment_response(
flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "");
// 會員資訊
order.setMember_barcode(
@ -408,13 +201,20 @@ public class TransactionFinalizeBuilder {
order.setMachine_time(orderMachineTime);
// --- 組裝購買商品清單 (items) ---
// 優先採用 captureItems() 在出貨流程開始時擷取的快照無快照時才即時讀 live buyList
// 快照可避免結案前 buyList 被其他執行緒清空掃碼支付等慢流程的競態造成無商品紀錄
List<TransactionFinalizePayload.ItemRecord> items;
if (itemsSnapshot != null && !itemsSnapshot.isEmpty()) {
items = new ArrayList<>(itemsSnapshot);
} else {
items = buildItemRecords(MyApp.getInstance().getBuyList());
List<TransactionFinalizePayload.ItemRecord> items = new ArrayList<>();
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
if (buyList != null) {
for (BuyStructure buy : buyList) {
int productId = 0;
try {
productId = Integer.parseInt(buy.getProduct().getProductID());
} catch (NumberFormatException e) {
Log.w(TAG, "Cannot parse productID to int: " + buy.getProduct().getProductID());
}
int price = buy.getProduct().getRealPrice();
int quantity = buy.getCount() > 0 ? buy.getCount() : 1;
items.add(new TransactionFinalizePayload.ItemRecord(productId, price, quantity));
}
}
order.setItems(items);

View File

@ -1,70 +0,0 @@
package com.unibuy.smartdevice.ui;
import android.content.Context;
import com.blankj.utilcode.util.ToastUtils;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
import com.unibuy.smartdevice.ui.dialog.FontendPickupCodeDialog;
import com.unibuy.v2.UnibuyHelper;
import com.unibuy.v2.ui.ShoppingCartFragment;
/**
* basic 模式 General 通用版銷售流程
* 結帳時彈出標準付款 Dialog (BuyDialog)購物車浮鈕則開啟多品項購物車 Fragment
*/
public class BasicSaleFlow extends SaleFlowHandler {
/** 多品項購物車 Fragment懶建立重複開啟時沿用同一實例。 */
private ShoppingCartFragment cartFragment;
public BasicSaleFlow(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
super(context, handlerMain, callback);
}
@Override
public void onBuyRequested() {
new BuyDialog(context, handlerMain).show();
}
/**
* 點選 VMC 商品依雲端 B014 DevSet.ShoppingCar 決定流程
* - 購物車開啟加入多品項購物車並停留商品列表不做取貨口檢查出貨時才檢查
* - 購物車關閉單品直購清空後僅放入這一筆直接彈出 BuyDialog商品資料選付款
* @return true 表示已消化此次點擊
*/
@Override
public boolean handleProductSelected(SlotStructure slot) {
// 點商品一律先彈出商品詳情頁詳情頁內依購物車開關呈現
// 數量調整加入購物車直接購買或僅直接購買再走後續加購單品直購流程
new com.unibuy.smartdevice.ui.dialog.DialogProductDetail(context, handlerMain, slot).show();
return true;
}
/** basic 使用 Luzui 移植的現金強化版(卡鈔/找零保障/自癒),上報接 TaiwanStar 新 MQTT。 */
@Override
public Class<? extends com.unibuy.smartdevice.DialogAbstract> cashDialogClass() {
return com.unibuy.smartdevice.ui.dialog.CashPayDialogGeneral.class;
}
/** 購物車浮鈕:開啟多品項購物車 Fragment。 */
@Override
public void onShoppingCartRequested() {
if (!(context instanceof FontendActivity)) {
return;
}
if (cartFragment == null) {
cartFragment = new ShoppingCartFragment(handlerMain);
}
((FontendActivity) context).openFragment(cartFragment);
}
@Override
public void onPickupRequested() {
new FontendPickupCodeDialog(context, handlerMain).show();
}
}

View File

@ -1,23 +0,0 @@
package com.unibuy.smartdevice.ui;
import android.content.Context;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.ui.dialog.MemberCardPickupDialog;
/**
* employee_card 模式 Chengwai 晟崴版銷售流程
* 點選商品/購物車時直接進入員工卡刷卡取貨 Dialog (MemberCardPickupDialog, B680)
* 商品點擊不消化現金走標準 Dialog無取貨碼入口 皆沿用基底預設
*/
public class EmployeeCardSaleFlow extends SaleFlowHandler {
public EmployeeCardSaleFlow(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
super(context, handlerMain, callback);
}
@Override
public void onBuyRequested() {
new MemberCardPickupDialog(context, handlerMain).show();
}
}

View File

@ -41,7 +41,6 @@ import com.unibuy.smartdevice.MyApp;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.controller.DevController;
@ -93,13 +92,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
SET_NETWORK(6),
RESET_SLOTNO(49),
RESTART_HOST(7),
// 對話框開啟時隱藏銷售平台浮層取貨碼/購物車浮鈕上下系統資訊列關閉時還原
// 原為僅隱藏語言鈕HIDDEN/SHOW_MULTILANGUAGE_BUTTON擴大為整個浮層避免對話框
// 貼底部非滿版周圍把這些邊緣元件漏出來沿用 8/9 代碼不影響既有呼叫
HIDE_SALE_OVERLAY(8),
SHOW_SALE_OVERLAY(9),
// 純時鐘刷新每秒只更新機台資訊列時間文字不做上報/截圖/重啟判斷等重活
CLOCK_TICK(50),
HIDDEN_MULTILANGUAGE_BUTTON(8),
SHOW_MULTILANGUAGE_BUTTON(9),
;
private int option;
@ -126,7 +120,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
private Fragment vmcFragment;
private Fragment electricFragment;
private ReportShotTimeByScheduler reportOnScheduler;
private ClockTickScheduler clockTickScheduler;
private ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;
@ -266,7 +259,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
String timeByNow = sdf.format(new Date());
binding.textTime.setText(timeByNow);
// 機台資訊列的時間改由 ClockTickScheduler 每秒更新這裡不再重複設定
String timeByM = new SimpleDateFormat("HH:mm").format(new Date());
if (timeByM.equals("23:45")) {
@ -338,11 +330,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
binding.textTemperature.setText(getString(R.string.temp) + ":" + tempStr);
getLogs().update("Temperature", String.valueOf(DevXinYuanController.getTemperature()), "");
// 同步更新機台資訊列
updateMachineInfoName();
updateMachineInfoNetwork();
binding.textMachineInfoTemperature.setText(getString(R.string.temp) + ":" + tempStr);
try {
// httpAPI.detectHost();
} catch (Exception e) {
@ -396,7 +383,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
case SET_NETWORK:
getLogs().info("網路:" + message);
binding.textNetwork.setText(message);
updateMachineInfoNetwork();
break;
case RESET_SLOTNO:
new ResetSlotNo(this).start();
@ -404,21 +390,13 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
case RESTART_HOST:
new RestartHost(this).start();
break;
case HIDE_SALE_OVERLAY:
getLogs().info("對話框開啟,隱藏銷售平台浮層");
refreshSaleOverlay();
case HIDDEN_MULTILANGUAGE_BUTTON:
getLogs().info("隱藏多國語按紐");
// hiddenMultiLanguageButton();
break;
case SHOW_SALE_OVERLAY:
getLogs().info("對話框關閉,重新評估銷售平台浮層");
refreshSaleOverlay();
break;
case CLOCK_TICK:
// 輕量時鐘每秒只更新時間文字不做其他重活
if (binding != null) {
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
.format(new Date());
binding.textMachineInfoTime.setText(now);
}
case SHOW_MULTILANGUAGE_BUTTON:
getLogs().info("顯示多國語按紐");
// showMultiLanguageButton();
break;
}
}
@ -443,12 +421,12 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
settingsDao.close();
// 讀取最後設定語言的設定
LanguageHelper.applySavedLanguage(this);
// LanguageHelper.applySavedLanguage(this);
binding = ActivityFontendBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 初始化銷售流程處理器
saleFlowHandler = SaleFlowHandler.create(this, getHandlerMain(), this);
saleFlowHandler = new SaleFlowHandler(this, getHandlerMain(), this);
saleFlowHandler.onSetupUI();
httpAPI = new HttpAPI(getHandlerMain());
@ -473,9 +451,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
public void onUserInteraction() {
super.onUserInteraction();
MyApp.getInstance().updateLastInteraction();
// onUserInteraction 連對話框彈窗的觸控都會觸發dispatchTouchEvent 不會
// 故一併重設 5 分鐘待機計時器避免操作對話框時被誤判閒置踢回待機
resetIdleTimer();
}
public void ReSetAPP() {
@ -559,17 +534,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
idleRunnable = () -> {
// 🚩 全域最後互動時間為準再確認一次是否真的閒置滿 IDLE_TIMEOUT
// 對話框付款商品詳情結帳等有自己的 Window操作它時不會觸發本 Activity
// dispatchTouchEvent因此 5 分鐘計時器不會被重設 onUserInteraction 連對話框觸控
// 都會更新 lastInteractionTime若使用者其實在對話框內操作尚未真正閒置這裡就以
// 剩餘時間重新計時避免明明在操作卻被踢回待機
long idleMs = MyApp.getInstance().getIdleSeconds() * 1000L;
if (idleMs < IDLE_TIMEOUT) {
idleHandler.postDelayed(idleRunnable, IDLE_TIMEOUT - idleMs);
return;
}
// 🚩 若目前處於主控台/管理員模式則忽略閒置返回待機頁的觸發並重新計時
if (!MyApp.getInstance().getSuAccess().isEmpty()) {
getLogs().info(" 偵測到當前處於開發者/主控台模式,忽略閒置返回待機頁的觸發。");
@ -584,9 +548,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
return;
}
getLogs().info("⚠️ 無操作 5 分鐘,自動返回初始畫面(清空購物車)");
getLogs().info("⚠️ 無操作 3 分鐘,自動返回初始畫面(清空購物車)");
MyApp.getInstance().getBuyList().clear(); // 🚩 逾時返回首頁時務必清空購物車避免機台進入永久忙碌狀態
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().clearCart(); // 一併清 v2 多品項購物車避免取消結帳後走人留下殘單
Intent intent = new Intent(FontendActivity.this, AppEntry.initialActivity());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
@ -668,80 +631,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
return -1; // 無法獲取訊號強度
}
/** 取得 App 版本名稱versionName取不到時回 "0"。 */
private String getAppVersionName() {
try {
return getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
return "0";
}
}
/** 機台資訊列左側:顯示「序號 + 版本號」。 */
private void updateMachineInfoName() {
if (binding == null) {
return;
}
binding.textMachineInfoName.setText(
MyApp.getInstance().getMachine().getMachineID() + " Ver " + getAppVersionName());
}
/**
* 依系統/下位機回報的網路狀態動態更新機台資訊列的網路訊號 icon 與文字
* WiFi RSSI(dBm) 1~3 格與滿格行動網路(SIM) dBm 0~4 無網路顯示 0
*/
private void updateMachineInfoNetwork() {
if (binding == null) {
return;
}
int iconRes = R.drawable.signal_cellular_0_bar_24; // 預設無網路
String label = getString(R.string.no_internet);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network network = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = (network != null)
? connectivityManager.getNetworkCapabilities(network) : null;
if (capabilities != null
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
int rssi = 0; // 0 代表系統未提供強度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
rssi = capabilities.getSignalStrength();
}
iconRes = wifiIconByRssi(rssi);
label = getString(R.string.wireless_internet);
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
iconRes = cellularIconByDbm(getMobileSignalStrength());
label = getString(R.string.mobile_network);
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
iconRes = R.drawable.network_wifi_24; // 有線以滿格示意連線正常
label = getString(R.string.wired_internet);
}
}
}
binding.textMachineInfoNetwork.setText(label);
binding.textMachineInfoNetwork.setCompoundDrawablesRelativeWithIntrinsicBounds(iconRes, 0, 0, 0);
}
/** WiFi RSSI(dBm) → icon數字越接近 0 越強rssi==0 視為未提供強度回滿格。 */
private int wifiIconByRssi(int rssi) {
if (rssi == 0 || rssi >= -55) return R.drawable.network_wifi_24; // 滿格
if (rssi >= -66) return R.drawable.network_wifi_3_bar_24; // 等級3
if (rssi >= -77) return R.drawable.network_wifi_2_bar_24; // 等級2
return R.drawable.network_wifi_1_bar_24; // 等級1
}
/** 行動網路 dBm → icon數字越接近 0 越強,-1 代表取不到訊號。 */
private int cellularIconByDbm(int dbm) {
if (dbm == -1) return R.drawable.signal_cellular_0_bar_24; // 無網路
if (dbm >= -85) return R.drawable.signal_cellular_4_bar_24; // 滿格
if (dbm >= -95) return R.drawable.signal_cellular_3_bar_24; // 等級3
if (dbm >= -105) return R.drawable.signal_cellular_2_bar_24; // 等級2
if (dbm >= -115) return R.drawable.signal_cellular_1_bar_24; // 等級1
return R.drawable.signal_cellular_0_bar_24; // 無網路
}
private void setBottomNavigation() {
vmcFragment = new VmcFragment(getHandlerMain());
activeFragment = vmcFragment;
@ -824,21 +713,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
});
// 前台取貨碼浮鈕取貨碼從屬取貨模組(B014 FunctionSet.PickupModule)
// 取貨模組關閉時即使 PickupCode=true 也一律隱藏畫面與流程都不出現
binding.floatButtonPickupCode.setVisibility(
(MyApp.getInstance().getDevSet().isPickupModule()
&& MyApp.getInstance().getDevSet().isPickupCode()) ? VISIBLE : View.GONE);
binding.floatButtonPickupCode.setVisibility(View.GONE);
binding.floatButtonShoppingCart.setOnClickListener(view -> {
// 改由 flavor SaleFlowHandler 決定General 開多品項購物車 Fragment
// 其他 flavor 維持原行為onShoppingCartRequested 預設轉呼叫 onBuyRequested
if (saleFlowHandler != null) {
saleFlowHandler.onShoppingCartRequested();
} else {
getHandlerMain().start(getClass().getSimpleName(), Option.BUY_DIALOG_OPEN.getOption(),
getString(R.string.detect_shopping_cart));
}
getHandlerMain().start(getClass().getSimpleName(), Option.BUY_DIALOG_OPEN.getOption(),
getString(R.string.detect_shopping_cart));
});
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
@ -847,36 +726,13 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
binding.floatButtonShoppingCart.setVisibility(View.GONE);
}
// 觀察購物車內容以更新數量 Badge
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().getCartItems().observe(this, cartItems -> {
int totalCount = 0;
if (cartItems != null) {
for (com.unibuy.smartdevice.structure.SlotStructure item : cartItems) {
totalCount += item.cartSelectedCount;
}
}
if (totalCount > 0) {
binding.textCartBadge.setText(String.valueOf(totalCount));
binding.textCartBadge.setVisibility(View.VISIBLE);
} else {
binding.textCartBadge.setVisibility(View.GONE);
}
});
startHeaderReportScheduler();
// 語言切換改為單一入口開啟語言選單支援 8 無法並排
// 沿用 btnswitchlanTW 當觸發鈕XML 已改顯示 🌐隱藏另兩顆舊鈕
binding.btnswitchlanEn.setVisibility(View.GONE);
binding.btnswitchlanJa.setVisibility(View.GONE);
binding.btnswitchlanTW.setOnClickListener(v -> showLanguagePicker());
// 新增機台資訊列的語言按鈕
binding.textMachineInfoLang.setOnClickListener(v -> showLanguagePicker());
// 機台資訊列顯示序號 + 版本號
updateMachineInfoName();
updateMachineInfoNetwork();
String currentLang = LanguageHelper.getSavedLanguage(this);
binding.textMachineInfoLangText.setText(LanguageHelper.getDisplayName(currentLang));
// binding.btnswitchlanTW.setOnClickListener(v -> switchLanguage("tw"));
//
// binding.btnswitchlanEn.setOnClickListener(v -> switchLanguage("en"));
//
// binding.btnswitchlanJa.setOnClickListener(v -> switchLanguage("ja"));
// 🛠 隱藏後門長按網路狀態文字 2 秒進入主控台
final Handler networkHandler = new Handler();
@ -886,8 +742,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
finish();
};
// 後門已遷至機台資訊列的序號 + 版本號元素舊的 textNetwork 整條系統列已隱藏不再接收觸控
binding.textMachineInfoName.setOnTouchListener((v, event) -> {
binding.textNetwork.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case android.view.MotionEvent.ACTION_DOWN:
networkHandler.postDelayed(networkRunnable, 2000); // 2秒
@ -908,29 +763,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
// 顯示語言選單8 種語言以各自文字呈現選擇後即套用並重繪畫面
private void showLanguagePicker() {
// 語系清單以後台 B014 LangSet 下發的機台啟用語系為準未取得時退回全部支援語系
final String[] codes = LanguageHelper.getMachineLanguages();
String[] names = new String[codes.length];
int current = 0;
String saved = LanguageHelper.getSavedLanguage(this);
for (int i = 0; i < codes.length; i++) {
names[i] = LanguageHelper.getDisplayName(codes[i]);
if (codes[i].equals(saved)) {
current = i;
}
}
new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle(R.string.select_language)
.setSingleChoiceItems(names, current, (dialog, which) -> {
dialog.dismiss();
switchLanguage(codes[which]);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void switchLanguage(String languageCode) {
LanguageHelper.changeLanguage(this, languageCode);
binding = ActivityFontendBinding.inflate(getLayoutInflater());
@ -940,15 +772,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
setBottomNavigation();
startOnSwitchFragment();
// 重新綁定語言選單入口重繪後 binding 已換新
binding.btnswitchlanEn.setVisibility(View.GONE);
binding.btnswitchlanJa.setVisibility(View.GONE);
binding.btnswitchlanTW.setOnClickListener(v -> showLanguagePicker());
binding.textMachineInfoLang.setOnClickListener(v -> showLanguagePicker());
updateMachineInfoName();
updateMachineInfoNetwork();
String currentLang = LanguageHelper.getSavedLanguage(this);
binding.textMachineInfoLangText.setText(LanguageHelper.getDisplayName(currentLang));
}
private void startHeaderReportScheduler() {
@ -956,10 +779,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
getHandlerMain().start(getClass().getSimpleName(), Option.REPORT_AND_SHOT_TIME.getOption(), "start");
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
// 時鐘獨立每秒刷新與上面的 10 秒重活迴圈分開避免把上報/重啟判斷拉到每秒
clockTickScheduler = new ClockTickScheduler(getHandlerMain());
clockTickScheduler.start(0L, 1L, TimeUnit.SECONDS);
}
private void stopHeaderReportScheduler() {
@ -967,38 +786,18 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
if (clockTickScheduler != null) {
clockTickScheduler.shutdown();
clockTickScheduler = null;
}
}
/**
* 統一控制銷售平台浮層在對話框期間的顯示取貨碼/購物車浮鈕上方系統列時間/溫度/網路
* 下方系統列版號/語言/執行緒對話框貼底部非滿版只蓋住中間這些邊緣元件若不隱藏會漏出來
* visible=false 全部隱藏對話框開啟時
* visible=true 還原浮鈕依各自 B014 gating 重新判斷不可無腦設 VISIBLE否則會把已停用的
* 取貨碼/購物車漏出來系統列則直接還原
*/
private void applySaleOverlayVisibility(boolean visible) {
if (binding == null) return;
if (visible) {
// 取貨碼浮鈕從屬取貨模組PickupModule && PickupCode 才顯示
binding.floatButtonPickupCode.setVisibility(
(MyApp.getInstance().getDevSet().isPickupModule()
&& MyApp.getInstance().getDevSet().isPickupCode()) ? VISIBLE : View.GONE);
// 購物車浮鈕依購物車開關
binding.floatButtonShoppingCart.setVisibility(
MyApp.getInstance().getDevSet().isShoppingCar() ? VISIBLE : View.GONE);
// /下灰色系統列已停用銷售時不再顯示機台資訊改由 machine_info_bar 呈現
binding.linearLayout5.setVisibility(View.GONE);
binding.bottomBar.setVisibility(View.GONE);
} else {
binding.floatButtonPickupCode.setVisibility(View.GONE);
binding.floatButtonShoppingCart.setVisibility(View.GONE);
binding.linearLayout5.setVisibility(View.GONE);
binding.bottomBar.setVisibility(View.GONE);
}
private void hiddenMultiLanguageButton() {
binding.btnswitchlanTW.setVisibility(View.GONE);
binding.btnswitchlanEn.setVisibility(View.GONE);
binding.btnswitchlanJa.setVisibility(View.GONE);
}
private void showMultiLanguageButton() {
binding.btnswitchlanTW.setVisibility(VISIBLE);
binding.btnswitchlanEn.setVisibility(VISIBLE);
binding.btnswitchlanJa.setVisibility(VISIBLE);
}
private class RestartHost extends HandlerMainScheduler {
@ -1288,28 +1087,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
}
/** 每秒只刷新機台資訊列時間文字的輕量排程,與 10 秒上報迴圈分離。 */
public class ClockTickScheduler extends HandlerMainScheduler {
public ClockTickScheduler(HandlerMain handlerMain) {
super(handlerMain);
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void execute(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.CLOCK_TICK.getOption(), "tick");
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
public class AddProductOnCountdown extends HandlerMainCountdown {
public AddProductOnCountdown(HandlerMain handlerMain) {
super(handlerMain);
@ -1340,74 +1117,24 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
private void switchFragment(Fragment from, Fragment to) {
if (from != to) {
// 防呆 Activity onSaveInstanceState非前景commit 會丟
// IllegalStateException購物車 180 秒倒數 onFinish 在背景觸發返回時即屬此情況
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
getLogs().info("switchFragment skipped: activity not started");
return;
}
if (from instanceof FragmentAbstract) {
getLogs().info("change pay stop");
((FragmentAbstract) from).stopOnSwitchFragment();
}
// 此切換僅為 Fragment 顯示切換資料在 ViewModel允許 state loss 不致實害
getSupportFragmentManager().beginTransaction()
.hide(from)
.show(to)
.commitAllowingStateLoss();
.commit();
activeFragment = to;
if (to instanceof FragmentAbstract) {
getLogs().info("change pay start");
((FragmentAbstract) to).startOnSwitchFragment();
}
// 浮層連動購物車General 多品項是開在 fragment_container Fragment不走 DialogAbstract
// 故不會送 HIDE_SALE_OVERLAY在此依目標 fragment 補上連動activeFragment 已於上方更新
refreshSaleOverlay();
}
}
/**
* 重新評估銷售平台浮層取貨碼/購物車浮鈕上下系統列是否該顯示
* 唯一真相僅當處於商品瀏覽頁VMC/Electric沒有任何對話框開著時才顯示否則隱藏
* 涵蓋兩條會遮住銷售平台的路徑對話框DialogAbstract貼底部非滿版與購物車 Fragment
*/
private void refreshSaleOverlay() {
boolean inProductBrowsing = (activeFragment == vmcFragment || activeFragment == electricFragment);
boolean anyDialog = MyApp.getInstance().isShowDialog();
applySaleOverlayVisibility(inProductBrowsing && !anyDialog);
}
/**
* 將指定 Fragment 加入容器若尚未加入並切換顯示
* flavor 端注入專屬 Fragment General 多品項購物車使用本身不依賴任何 flavor 類別
*/
public void openFragment(Fragment to) {
if (to == null) {
return;
}
// switchFragment背景狀態下 commitNow 亦會丟 IllegalStateException
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
getLogs().info("openFragment skipped: activity not started");
return;
}
if (!to.isAdded()) {
getSupportFragmentManager().beginTransaction()
.add(binding.fragmentContainer.getId(), to)
.hide(to)
.commitNow();
}
switchFragment(activeFragment, to);
}
/** 切回 VMC 商品列表(供購物車「加購/取消/逾時」返回使用)。 */
public void backToVmc() {
switchFragment(activeFragment, vmcFragment);
}
public void callbackOnBackPressed() {
FragmentManager fragmentManager = getSupportFragmentManager();
int backStackCount = fragmentManager.getBackStackEntryCount();

View File

@ -12,7 +12,6 @@ import android.widget.ImageView;
import android.widget.SearchView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.GridLayoutManager;
import com.unibuy.smartdevice.AppCompatActivityAbstract;
import com.unibuy.smartdevice.MyApp;
@ -95,44 +94,34 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
if (size > 0) recyclerSearchTextAdpter.notifyItemRangeRemoved(0, size);
if (message.isEmpty()) {
binding.recyclerSearchText.setVisibility(View.GONE);
binding.textSearchStatus.setVisibility(View.VISIBLE);
binding.textSearchStatus.setText("請輸入您要搜尋的文字");
for (ProductStructure product: MyApp.getInstance().getProductList()) {
searchProductList.add(product);
}
if (searchProductList.size() > 0) {
recyclerProductSearchAdpter.notifyDataSetChanged();
}
recyclerProductSearchAdpter.notifyItemInserted(searchProductList.size() -1);
} else {
for (ProductStructure product: MyApp.getInstance().getProductList()) {
if (product.getProductName().contains(message)) {
searchProductList.add(product);
String name = product.getProductName();
// Log.i("product name", name);
int index = name.indexOf(message);
if (index != -1 && index < name.length() - 1) {
String result = name.substring(index + message.length());
if (!searchTextList.contains(name)) {
searchTextList.add(name);
if (!result.isEmpty()) {
String oneText = String.valueOf(result.charAt(0));
if (!searchTextList.contains(oneText)) {
searchTextList.add(oneText);
}
}
}
}
}
if (searchTextList.size() > 2) Collections.sort(searchTextList);
if (searchTextList.isEmpty()) {
binding.recyclerSearchText.setVisibility(View.GONE);
binding.textSearchStatus.setVisibility(View.VISIBLE);
binding.textSearchStatus.setText("查無相關的候選字");
} else {
binding.recyclerSearchText.setVisibility(View.VISIBLE);
binding.textSearchStatus.setVisibility(View.GONE);
recyclerSearchTextAdpter.notifyDataSetChanged();
}
if (searchProductList.size() > 0) {
recyclerProductSearchAdpter.notifyDataSetChanged();
}
recyclerProductSearchAdpter.notifyItemInserted(searchProductList.size() -1);
recyclerSearchTextAdpter.notifyItemInserted(searchTextList.size() -1);
}
break;
}
@ -151,7 +140,7 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
recyclerProductSearchAdpter = new RecyclerProductSearchAdpter(getCtx(), position, searchProductList);
binding.recyclerProductSearch.setAdapter(recyclerProductSearchAdpter);
binding.recyclerProductSearch.setLayoutManager(new GridLayoutManager(getCtx(), 3));
binding.recyclerProductSearch.setLayoutManager(new LinearLayoutManager(getCtx()));
recyclerSearchTextAdpter = new RecyclerSearchTextAdpter(binding, searchTextList);
binding.recyclerSearchText.setAdapter(recyclerSearchTextAdpter);
@ -160,30 +149,8 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
// SearchView searchView = binding.searchProductList;
// enlargeSearchView(searchView, 32, 20, R.drawable.search); // icon 32dp文字 20sp自訂 drawable
replaceSearchViewIcon(binding.searchProductList);
binding.searchProductList.setOnQueryTextListener(new SearchProductListOnQueryText(getHandlerMain()) {
@Override
public boolean onQueryTextSubmit(String query) {
binding.searchProductList.clearFocus();
return super.onQueryTextSubmit(query);
}
});
// 點擊列表時自動關閉鍵盤
binding.recyclerProductSearch.setOnTouchListener((v, event) -> {
binding.searchProductList.clearFocus();
return false;
});
binding.recyclerSearchText.setOnTouchListener((v, event) -> {
binding.searchProductList.clearFocus();
return false;
});
binding.searchProductList.setOnQueryTextListener(new SearchProductListOnQueryText(getHandlerMain()));
binding.buttonBack.setOnClickListener(v -> finish());
binding.buttonClear.setOnClickListener(v -> {
binding.searchProductList.setQuery("", false);
binding.searchProductList.clearFocus();
});
if (MyApp.getInstance().getProductList().isEmpty()) {
ProductsDao productsDao = new ProductsDao(getCtx());
@ -205,6 +172,9 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
private void replaceSearchViewIcon(SearchView searchView) {
if (searchView == null) return;
// 設定 hint
searchView.setQueryHint("輸入搜尋關鍵字");
// 找到內部的 EditText修改字體大小 & 顏色
int searchEditTextId = searchView.getContext()
.getResources()
@ -224,24 +194,7 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
ImageView searchIcon = searchView.findViewById(searchIconId);
if (searchIcon != null) {
searchIcon.setImageDrawable(null);
searchIcon.setVisibility(View.GONE);
}
// 隱藏內部的清除 icon
int closeIconId = searchView.getContext()
.getResources()
.getIdentifier("android:id/search_close_btn", null, null);
ImageView closeIcon = searchView.findViewById(closeIconId);
if (closeIcon != null) {
closeIcon.setImageDrawable(null);
closeIcon.setEnabled(false);
ViewGroup.LayoutParams params = closeIcon.getLayoutParams();
if (params != null) {
params.width = 0;
params.height = 0;
closeIcon.setLayoutParams(params);
}
searchIcon.setImageResource(R.drawable.searchopen);
}
}

View File

@ -1,79 +0,0 @@
package com.unibuy.smartdevice.ui;
import android.content.Context;
import android.view.View;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
/**
* 銷售流程處理器抽象基底
*
* 三種購買流程改由雲端 B014 {@code shopping_mode} 執行期切換取代舊 project flavor 分家
* <ul>
* <li>{@code basic} {@link BasicSaleFlow}多品項購物車 General</li>
* <li>{@code employee_card} {@link EmployeeCardSaleFlow}員工卡 B680 Chengwai</li>
* <li>{@code pickup_sheet} {@link PickupSheetSaleFlow}掃碼醫令自動結帳 Cmuh</li>
* </ul>
* {@link #create} {@link MyApp#getShoppingMode()} 建立對應策略
*/
public abstract class SaleFlowHandler {
protected final Context context;
protected final SaleFlowCallback callback;
protected final HandlerMain handlerMain;
protected SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
this.context = context;
this.handlerMain = handlerMain;
this.callback = callback;
}
/** 依雲端 shopping_mode 建立對應的銷售流程策略(預設 basic。 */
public static SaleFlowHandler create(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
String mode = MyApp.getInstance().getShoppingMode();
if ("employee_card".equals(mode)) {
return new EmployeeCardSaleFlow(context, handlerMain, callback);
}
if ("pickup_sheet".equals(mode)) {
return new PickupSheetSaleFlow(context, handlerMain, callback);
}
return new BasicSaleFlow(context, handlerMain, callback);
}
public void onSetupUI() {
onSetupUI(null);
}
public void onSetupUI(View rootView) {
// 預設無動作
}
public void onBuyRequested() {
// 預設無動作
}
public void onShoppingCartRequested() {
// 預設購物車浮鈕等同進入購買流程
onBuyRequested();
}
public boolean handleProductSelected(SlotStructure slot) {
// 預設不消化商品點擊交回 adapter 既有流程
return false;
}
public Class<? extends DialogAbstract> cashDialogClass() {
return com.unibuy.smartdevice.ui.dialog.CashPayDialog.class;
}
public void onBarcodeReceived(String rawData) {
// 預設無動作
}
public void onPickupRequested() {
// 預設無動作
}
}

View File

@ -2,28 +2,30 @@ package com.unibuy.smartdevice.ui;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TableRow;
import android.widget.TextView;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.unibuy.smartdevice.AppCompatActivityAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.database.MemoDao;
import com.unibuy.smartdevice.database.SettingsDao;
import com.unibuy.smartdevice.databinding.ActivitySettingTestBinding;
import com.unibuy.smartdevice.databinding.ActivitySystemSetBinding;
import com.unibuy.smartdevice.external.StarCloudAPI;
import com.unibuy.smartdevice.structure.DevSetStructure;
import com.unibuy.smartdevice.structure.MemoStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.recycler.RecyclerSettingListAdpter;
import org.json.JSONException;
import org.json.JSONObject;
/**
* 主控台系統設定呈現雲端 B014 下發的開關設定
* 系統方identity=system可編輯並按儲存回寫雲端B016其他身分唯讀
*/
import java.util.ArrayList;
import java.util.List;
public class SystemSetActivity extends AppCompatActivityAbstract {
private ActivitySystemSetBinding binding;
private boolean isSystem;
@Override
protected Context setCtx() {
@ -47,249 +49,57 @@ public class SystemSetActivity extends AppCompatActivityAbstract {
binding = ActivitySystemSetBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
isSystem = MyApp.getInstance().isSystemIdentity();
applyShoppingModeView();
updateCheckBoxes();
applyRolePermission();
binding.buttonBack.setOnClickListener(v -> finish());
binding.buttonSave.setOnClickListener(v -> onSaveClicked());
binding.buttonBack.setOnClickListener(v -> {
updateSystemSet();
finish();
});
// IC 卡讀取功能已移除
}
/**
* 購物方式雲端 B014 ShoppingMode機台唯讀顯示中文名稱並只在 basic基礎版
* 時顯示其下的可設定項員工卡取貨單模式整段隱藏流程與硬體由雲端固定現場不可調
*/
private void applyShoppingModeView() {
String mode = MyApp.getInstance().getShoppingMode();
String label;
switch (mode == null ? "" : mode) {
case "employee_card":
label = "員工卡";
break;
case "pickup_sheet":
label = "取貨單";
break;
case "basic":
default:
label = "基礎版";
break;
}
binding.textViewShoppingMode.setText("購物方式:" + label);
binding.layoutBasicOnlySettings.setVisibility("basic".equals(mode) ? View.VISIBLE : View.GONE);
}
/** 將記憶體中的設定值套到各 CheckBox。 */
private void updateCheckBoxes() {
binding.checkboxVMC.setChecked(MyApp.getInstance().getDevSet().isVMC());
binding.checkboxElectic.setChecked(MyApp.getInstance().getDevSet().isElectic());
binding.checkboxShoppingCar.setChecked(MyApp.getInstance().getDevSet().isShoppingCar());
binding.checkboxInvoice.setChecked(MyApp.getInstance().getDevSet().isInvoice());
binding.cardDevShoppingCar.setVisibility(MyApp.getInstance().getDevSet().isShoppingCar() ? View.VISIBLE : View.GONE);
binding.cardDevInvoice.setVisibility(MyApp.getInstance().getDevSet().isInvoice() ? View.VISIBLE : View.GONE);
boolean isNfcPayEnabled = MyApp.getInstance().getDevSet().isDevNFCPay();
if (isNfcPayEnabled) {
binding.textViewNFCStatusHint.setText("啟用中");
binding.textViewNFCStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
binding.gridLayoutNFCPayOptions.setVisibility(View.VISIBLE);
} else {
binding.textViewNFCStatusHint.setText("功能未啟用");
binding.textViewNFCStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
binding.gridLayoutNFCPayOptions.setVisibility(View.GONE);
}
binding.cardDevCreditCard.setVisibility(MyApp.getInstance().getDevSet().isDevCreditCard() ? View.VISIBLE : View.GONE);
binding.cardDevMobilePay.setVisibility(MyApp.getInstance().getDevSet().isDevMobilePay() ? View.VISIBLE : View.GONE);
binding.cardDevCardPay.setVisibility(MyApp.getInstance().getDevSet().isDevCardPay() ? View.VISIBLE : View.GONE);
boolean isScanPayEnabled = MyApp.getInstance().getDevSet().isDevScanPay();
binding.checkboxDevScanPay.setChecked(isScanPayEnabled);
if (isScanPayEnabled) {
binding.textViewScanStatusHint.setText("啟用中");
binding.textViewScanStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
binding.layoutScanPayOptions.setVisibility(View.VISIBLE);
} else {
binding.textViewScanStatusHint.setText("功能未啟用");
binding.textViewScanStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
binding.layoutScanPayOptions.setVisibility(View.GONE);
}
boolean isEsunEnabled = MyApp.getInstance().getDevSet().isDevEsunPay();
binding.checkboxDevEsunPay.setChecked(isEsunEnabled);
int esunVisibility = isEsunEnabled ? View.VISIBLE : View.GONE;
binding.cardDevEsunPay.setVisibility(esunVisibility);
binding.cardDevEsunSubTaiwanPay.setVisibility(esunVisibility);
boolean isTapPayEnabled = MyApp.getInstance().getDevSet().isDevTapPay();
binding.checkboxDevTapPay.setChecked(isTapPayEnabled);
binding.cardDevTapPay.setVisibility(isTapPayEnabled ? View.VISIBLE : View.GONE);
binding.checkboxDevNFCPay.setChecked(MyApp.getInstance().getDevSet().isDevNFCPay());
binding.checkboxDevEsunPay.setChecked(MyApp.getInstance().getDevSet().isDevEsunPay());
binding.checkboxDevTapPay.setChecked(MyApp.getInstance().getDevSet().isDevTapPay());
binding.checkboxDevCash.setChecked(MyApp.getInstance().getDevSet().isDevCash());
binding.checkboxDevLinePay.setChecked(MyApp.getInstance().getDevSet().isDevLinePay());
binding.checkboxDevTapPay30.setChecked(MyApp.getInstance().getDevSet().isTapPay30());
binding.checkboxDevTapPay31.setChecked(MyApp.getInstance().getDevSet().isTapPay31());
binding.checkboxDevTapPay32.setChecked(MyApp.getInstance().getDevSet().isTapPay32());
binding.checkboxDevTapPay33.setChecked(MyApp.getInstance().getDevSet().isTapPay33());
binding.checkboxDevTapPay34.setChecked(MyApp.getInstance().getDevSet().isTapPay34());
binding.cardDevTapPay30.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay30() ? View.VISIBLE : View.GONE);
binding.cardDevTapPay31.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay31() ? View.VISIBLE : View.GONE);
binding.cardDevTapPay32.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay32() ? View.VISIBLE : View.GONE);
binding.cardDevTapPay33.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay33() ? View.VISIBLE : View.GONE);
binding.cardDevTapPay34.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay34() ? View.VISIBLE : View.GONE);
boolean isLinePayEnabled = MyApp.getInstance().getDevSet().isDevLinePay();
binding.checkboxDevLinePay.setChecked(isLinePayEnabled);
binding.cardDevLinePay.setVisibility(isLinePayEnabled ? View.VISIBLE : View.GONE);
binding.cardDevLineSubLinePay.setVisibility(isLinePayEnabled ? View.VISIBLE : View.GONE);
boolean isCashEnabled = MyApp.getInstance().getDevSet().isDevCash();
binding.checkboxDevCash.setChecked(isCashEnabled);
if (isCashEnabled) {
binding.textViewCashStatusHint.setText("啟用中");
binding.textViewCashStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
binding.layoutCashPayOptions.setVisibility(View.VISIBLE);
} else {
binding.textViewCashStatusHint.setText("功能未啟用");
binding.textViewCashStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
binding.layoutCashPayOptions.setVisibility(View.GONE);
}
binding.checkboxDevBillF.setChecked(MyApp.getInstance().getCashSetStructure().isBillF1000());
binding.checkboxDevBillE.setChecked(MyApp.getInstance().getCashSetStructure().isBillE500());
binding.checkboxDevBillD.setChecked(MyApp.getInstance().getCashSetStructure().isBillD100());
binding.checkboxDevCoinF.setChecked(MyApp.getInstance().getCashSetStructure().isCoinF50());
binding.checkboxDevCoinE.setChecked(MyApp.getInstance().getCashSetStructure().isCoinE10());
binding.checkboxDevCoinD.setChecked(MyApp.getInstance().getCashSetStructure().isCoinD5());
binding.checkboxDevCoinC.setChecked(MyApp.getInstance().getCashSetStructure().isCoinC1());
binding.cardDevBillF.setVisibility(MyApp.getInstance().getCashSetStructure().isBillF1000() ? View.VISIBLE : View.GONE);
binding.cardDevBillE.setVisibility(MyApp.getInstance().getCashSetStructure().isBillE500() ? View.VISIBLE : View.GONE);
binding.cardDevBillD.setVisibility(MyApp.getInstance().getCashSetStructure().isBillD100() ? View.VISIBLE : View.GONE);
binding.cardDevCoinF.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinF50() ? View.VISIBLE : View.GONE);
binding.cardDevCoinE.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinE10() ? View.VISIBLE : View.GONE);
binding.cardDevCoinD.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinD5() ? View.VISIBLE : View.GONE);
binding.cardDevCoinC.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinC1() ? View.VISIBLE : View.GONE);
boolean isPickupEnabled = MyApp.getInstance().getDevSet().isPickupModule();
binding.checkboxPickupModule.setChecked(isPickupEnabled);
if (isPickupEnabled) {
binding.textViewPickupStatusHint.setText("啟用中");
binding.textViewPickupStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
binding.chipGroupPickup.setVisibility(View.VISIBLE);
} else {
binding.textViewPickupStatusHint.setText("功能未啟用");
binding.textViewPickupStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
binding.chipGroupPickup.setVisibility(View.GONE);
}
binding.checkboxPickupCode.setChecked(MyApp.getInstance().getDevSet().isPickupCode());
binding.checkboxPassCode.setChecked(MyApp.getInstance().getDevSet().isPassCode());
binding.cardDevPickupCode.setVisibility(isPickupEnabled && MyApp.getInstance().getDevSet().isPickupCode() ? View.VISIBLE : View.GONE);
binding.cardDevPassCode.setVisibility(isPickupEnabled && MyApp.getInstance().getDevSet().isPassCode() ? View.VISIBLE : View.GONE);
binding.checkboxWelcomeGift.setChecked(MyApp.getInstance().getDevSet().isWelcomeGift());
binding.checkboxMemberSystem.setChecked(MyApp.getInstance().getDevSet().isMemberSystem());
binding.checkboxAmbientTemp.setChecked(MyApp.getInstance().getDevSet().isAmbientTemp());
binding.cardDevWelcomeGift.setVisibility(MyApp.getInstance().getDevSet().isWelcomeGift() ? View.VISIBLE : View.GONE);
binding.cardDevMemberSystem.setVisibility(MyApp.getInstance().getDevSet().isMemberSystem() ? View.VISIBLE : View.GONE);
binding.cardDevAmbientTemp.setVisibility(MyApp.getInstance().getDevSet().isAmbientTemp() ? View.VISIBLE : View.GONE);
setupSpringSlotRow(binding.rowSpringSlot110, binding.textSpringSlot110, MyApp.getInstance().getHardwareSetStructure().isSpringSlot1_10());
setupSpringSlotRow(binding.rowSpringSlot1120, binding.textSpringSlot1120, MyApp.getInstance().getHardwareSetStructure().isSpringSlot11_20());
setupSpringSlotRow(binding.rowSpringSlot2130, binding.textSpringSlot2130, MyApp.getInstance().getHardwareSetStructure().isSpringSlot21_30());
setupSpringSlotRow(binding.rowSpringSlot3140, binding.textSpringSlot3140, MyApp.getInstance().getHardwareSetStructure().isSpringSlot31_40());
setupSpringSlotRow(binding.rowSpringSlot4150, binding.textSpringSlot4150, MyApp.getInstance().getHardwareSetStructure().isSpringSlot41_50());
setupSpringSlotRow(binding.rowSpringSlot5160, binding.textSpringSlot5160, MyApp.getInstance().getHardwareSetStructure().isSpringSlot51_60());
}
private void setupSpringSlotRow(TableRow row, TextView textMode, boolean isChecked) {
textMode.setTag(isChecked);
textMode.setText(isChecked ? "彈簧模式" : "履帶模式");
}
/** 依登入身分決定可否編輯:系統方僅可編輯硬體貨道並顯示儲存鈕;其餘全唯讀。 */
private void applyRolePermission() {
binding.buttonSave.setVisibility(View.GONE);
refreshChildStates();
}
/**
* 統一刷新所有 CheckBox enabled 狀態機台不可編輯系統設定
* - 所有系統設定項購物車/發票/各支付方式/刷卡機/掃碼/現金總開關與面額/取貨模組/
* 來店禮/會員/環溫 一律唯讀真相在雲端後台即使系統方也不可改
* - 唯一例外硬體貨道彈簧/履帶 僅系統方可改並寫回雲端因只有現場技師最清楚
* 各貨道的實際機械型式
*/
private void refreshChildStates() {
// 系統設定項恆唯讀與身分主開關都無關
setEnabled(false,
binding.checkboxShoppingCar, binding.checkboxInvoice,
binding.checkboxDevScanPay, binding.checkboxDevEsunPay, binding.checkboxDevTapPay,
binding.checkboxDevLinePay,
binding.checkboxDevTapPay30, binding.checkboxDevTapPay31, binding.checkboxDevTapPay32,
binding.checkboxDevTapPay33, binding.checkboxDevTapPay34,
binding.checkboxDevCash,
binding.checkboxDevBillF, binding.checkboxDevBillE, binding.checkboxDevBillD,
binding.checkboxDevCoinF, binding.checkboxDevCoinE, binding.checkboxDevCoinD,
binding.checkboxDevCoinC,
binding.checkboxPickupModule, binding.checkboxPickupCode, binding.checkboxPassCode,
binding.checkboxWelcomeGift, binding.checkboxMemberSystem, binding.checkboxAmbientTemp);
// 硬體貨道已改為全唯讀
}
private void setEnabled(boolean enabled, View... views) {
for (View v : views) {
v.setEnabled(enabled);
}
}
/** 系統方按下儲存:先回寫雲端,成功後才更新本地並關閉。 */
private void onSaveClicked() {
if (!isSystem) return;
binding.buttonSave.setEnabled(false);
try {
JSONObject settings = buildBackendSettings();
new StarCloudAPI(getHandlerMain(), getHandlerMain()).uploadSystemSettings(settings,
new StarCloudAPI.SettingsUploadCallback() {
@Override
public void onSuccess() {
commitToLocal();
android.widget.Toast.makeText(getCtx(), "設定已儲存並回寫雲端", android.widget.Toast.LENGTH_SHORT).show();
finish();
}
@Override
public void onFailure(String message) {
binding.buttonSave.setEnabled(true);
android.widget.Toast.makeText(getCtx(), "儲存失敗:" + message, android.widget.Toast.LENGTH_LONG).show();
}
});
} catch (JSONException e) {
binding.buttonSave.setEnabled(true);
android.widget.Toast.makeText(getCtx(), "設定組裝失敗:" + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
}
}
/** 將目前 CheckBox 狀態寫回記憶體並持久化(本地)。 */
private void commitToLocal() {
private void updateSystemSet() {
MyApp.getInstance().getDevSet().setVMC(binding.checkboxVMC.isChecked());
MyApp.getInstance().getDevSet().setElectic(binding.checkboxElectic.isChecked());
MyApp.getInstance().getDevSet().setShoppingCar(binding.checkboxShoppingCar.isChecked());
MyApp.getInstance().getDevSet().setInvoice(binding.checkboxInvoice.isChecked());
MyApp.getInstance().getDevSet().setDevScanPay(binding.checkboxDevScanPay.isChecked());
MyApp.getInstance().getDevSet().setDevNFCPay(binding.checkboxDevNFCPay.isChecked());
MyApp.getInstance().getDevSet().setDevEsunPay(binding.checkboxDevEsunPay.isChecked());
MyApp.getInstance().getDevSet().setDevTapPay(binding.checkboxDevTapPay.isChecked());
MyApp.getInstance().getDevSet().setDevCash(binding.checkboxDevCash.isChecked());
MyApp.getInstance().getDevSet().setDevLinePay(binding.checkboxDevLinePay.isChecked());
MyApp.getInstance().getDevSet().setTapPay30(binding.checkboxDevTapPay30.isChecked());
MyApp.getInstance().getDevSet().setTapPay31(binding.checkboxDevTapPay31.isChecked());
MyApp.getInstance().getDevSet().setTapPay32(binding.checkboxDevTapPay32.isChecked());
MyApp.getInstance().getDevSet().setTapPay33(binding.checkboxDevTapPay33.isChecked());
MyApp.getInstance().getDevSet().setTapPay34(binding.checkboxDevTapPay34.isChecked());
MyApp.getInstance().getDevSet().setDevLinePay(binding.checkboxDevLinePay.isChecked());
MyApp.getInstance().getDevSet().setDevCash(binding.checkboxDevCash.isChecked());
MyApp.getInstance().getCashSetStructure().setBillF1000(binding.checkboxDevBillF.isChecked());
MyApp.getInstance().getCashSetStructure().setBillE500(binding.checkboxDevBillE.isChecked());
@ -299,36 +109,6 @@ public class SystemSetActivity extends AppCompatActivityAbstract {
MyApp.getInstance().getCashSetStructure().setCoinD5(binding.checkboxDevCoinD.isChecked());
MyApp.getInstance().getCashSetStructure().setCoinC1(binding.checkboxDevCoinC.isChecked());
MyApp.getInstance().getDevSet().setPickupModule(binding.checkboxPickupModule.isChecked());
MyApp.getInstance().getDevSet().setPickupCode(binding.checkboxPickupCode.isChecked());
MyApp.getInstance().getDevSet().setPassCode(binding.checkboxPassCode.isChecked());
MyApp.getInstance().getDevSet().setWelcomeGift(binding.checkboxWelcomeGift.isChecked());
MyApp.getInstance().getDevSet().setMemberSystem(binding.checkboxMemberSystem.isChecked());
MyApp.getInstance().getDevSet().setAmbientTemp(binding.checkboxAmbientTemp.isChecked());
MyApp.getInstance().getHardwareSetStructure().setSpringSlot1_10((Boolean) binding.textSpringSlot110.getTag());
MyApp.getInstance().getHardwareSetStructure().setSpringSlot11_20((Boolean) binding.textSpringSlot1120.getTag());
MyApp.getInstance().getHardwareSetStructure().setSpringSlot21_30((Boolean) binding.textSpringSlot2130.getTag());
MyApp.getInstance().getHardwareSetStructure().setSpringSlot31_40((Boolean) binding.textSpringSlot3140.getTag());
MyApp.getInstance().getHardwareSetStructure().setSpringSlot41_50((Boolean) binding.textSpringSlot4150.getTag());
MyApp.getInstance().getHardwareSetStructure().setSpringSlot51_60((Boolean) binding.textSpringSlot5160.getTag());
MyApp.saveSystemSet(getCtx());
}
/**
* 組出回寫雲端用的 settings JSON機台僅可編輯硬體貨道故只送這六個 is_spring_slot_*
* 欄位B016 後端以 array_key_exists 逐欄位守門未送的欄位保持原值避免用本地舊值
* 覆寫雲端後台可能已更新的其他設定
*/
private JSONObject buildBackendSettings() throws JSONException {
JSONObject s = new JSONObject();
s.put("is_spring_slot_1_10", (Boolean) binding.textSpringSlot110.getTag());
s.put("is_spring_slot_11_20", (Boolean) binding.textSpringSlot1120.getTag());
s.put("is_spring_slot_21_30", (Boolean) binding.textSpringSlot2130.getTag());
s.put("is_spring_slot_31_40", (Boolean) binding.textSpringSlot3140.getTag());
s.put("is_spring_slot_41_50", (Boolean) binding.textSpringSlot4150.getTag());
s.put("is_spring_slot_51_60", (Boolean) binding.textSpringSlot5160.getTag());
return s;
}
}
}

View File

@ -79,9 +79,21 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
} catch (LogsEmptyException e) {
this.logs.warning(e);
}
holder.binding.textSlot.setText("貨道" + slot.getSlot());
// 多國語的判斷統一由 ProductStructure 處理/日未翻譯自動 fallback 中文
holder.binding.textProductName.setText(slot.getProduct().getLocalizedName(context));
holder.binding.textSlot.setText(String.format("%03d", slot.getSlot()));
// 多國語的判斷
String currentLang = LanguageHelper.getSavedLanguage(context);
holder.binding.textProductName.setText(slot.getProduct().getProductName());
switch (currentLang) {
case "tw":
holder.binding.textProductName.setText(slot.getProduct().getProductName());
break;
case "en":
holder.binding.textProductName.setText(slot.getProduct().getProductNameEn());
break;
case "ja":
holder.binding.textProductName.setText(slot.getProduct().getProductNameJp());
break;
}
int machinePrice = slot.getProduct().getMachinePrice();
int sellingPrice = slot.getProduct().getSellingPrice();
@ -91,18 +103,19 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
boolean isLock = slot.isLock();
int price = machinePrice > 0 ? machinePrice : sellingPrice;
holder.binding.textPrice.setVisibility(View.VISIBLE);
holder.binding.textPrice.setText("NT$ " + price);
// 綁定庫存數量顯示
holder.binding.textStock.setText("庫存" + count);
if (price > 0) {
holder.binding.textPrice.setVisibility(View.VISIBLE);
holder.binding.textPrice.setText("NT$ " + price);
} else {
holder.binding.textPrice.setVisibility(View.GONE);
}
holder.binding.mask.setVisibility(View.GONE);
holder.binding.textMask.setText("");
if (count <= 0) {
holder.binding.mask.setVisibility(View.VISIBLE);
holder.binding.textMask.setText("售完");
holder.binding.textMask.setText("補貨中");// 已經售完
}
if (isLock) {
@ -114,25 +127,8 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
holder.binding.imageProductPicture.setFocusable(false);
holder.binding.imageProductPicture.setFocusableInTouchMode(false);
holder.binding.imageProductPicture.setOnClickListener(new imageProductPictureOnClickListener(position));
// 整張卡片皆可點開商品詳情不限圖片避免點到名稱/價格區沒反應
holder.binding.getRoot().setOnClickListener(new imageProductPictureOnClickListener(position));
} else {
holder.binding.imageProductPicture.setOnClickListener(null);
holder.binding.getRoot().setOnClickListener(null);
}
// 商品卡右下角+快速加購僅購物車功能開啟時顯示無購物車時隱藏只能點商品開詳情頁
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
if (cartOn && count > 0 && !isLock) {
holder.binding.buttonQuickAdd.setVisibility(View.VISIBLE);
holder.binding.buttonQuickAdd.setOnClickListener(v -> {
if (MyApp.getInstance().isMachineBusy()) return;
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
com.blankj.utilcode.util.ToastUtils.showShort(R.string.cart_added);
});
} else {
holder.binding.buttonQuickAdd.setVisibility(View.GONE);
holder.binding.buttonQuickAdd.setOnClickListener(null);
}
// devRunVMC(0);
@ -181,16 +177,6 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
// 查詢失敗時不阻擋讓原流程繼續
}
// flavor 路由若該 flavor 自行消化此次點擊 General 多品項購物車加入購物車
// 則直接結束否則走以下既有流程晟崴員工卡取貨等行為不變
if (fragment.getActivity() instanceof FontendActivity) {
com.unibuy.smartdevice.ui.SaleFlowHandler handler =
((FontendActivity) fragment.getActivity()).getSaleFlowHandler();
if (handler != null && handler.handleProductSelected(slotProduct)) {
return;
}
}
// 點選時即時檢查出貨口狀態上鎖防止重複點擊
isCheckingSlot = true;
checkSlotHaveProduct(slotProduct.getSlot(), 1, () -> {

View File

@ -19,12 +19,10 @@ import androidx.viewpager.widget.ViewPager;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.RecyclerVmcSlotListBinding;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.ProductSearchActivity;
import com.unibuy.smartdevice.ui.tools.ImageGlide;
import java.util.List;
@ -64,7 +62,7 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
SlotStructure slot = slotList.get(position);
holder.binding.textSlot.setText("貨道" + slot.getSlot());
holder.binding.textSlot.setText(String.valueOf(slot.getSlot()));
String productID = slot.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
holder.binding.textProductName.setText(slot.getProduct().getProductName());
@ -73,54 +71,17 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
}
holder.binding.textCount.setText(String.valueOf(slot.getCount()));
holder.binding.editCount.setText(String.valueOf(slot.getCount()));
// numberPassword 預設會遮罩輸入關閉遮罩以顯示明碼數字
holder.binding.editCount.setTransformationMethod(null);
if (slot.isLock()) {
holder.binding.textLock.setChecked(false);
holder.binding.textLockStatus.setText("停售中");
holder.binding.textLockStatus.setTextColor(androidx.core.content.ContextCompat.getColor(context, R.color.warning));
holder.binding.textLock.setThumbTintList(android.content.res.ColorStateList.valueOf(androidx.core.content.ContextCompat.getColor(context, R.color.warning)));
holder.binding.textLock.setTrackTintList(android.content.res.ColorStateList.valueOf(androidx.core.graphics.ColorUtils.setAlphaComponent(androidx.core.content.ContextCompat.getColor(context, R.color.warning), 77)));
holder.binding.viewProductMask.setVisibility(View.VISIBLE);
holder.binding.textLock.setText(context.getString(R.string.lock));
holder.binding.textLock.setTextColor(Color.BLACK);
} else {
holder.binding.textLock.setChecked(true);
holder.binding.textLockStatus.setText("販售中");
holder.binding.textLockStatus.setTextColor(androidx.core.content.ContextCompat.getColor(context, R.color.success));
holder.binding.textLock.setThumbTintList(android.content.res.ColorStateList.valueOf(androidx.core.content.ContextCompat.getColor(context, R.color.success)));
holder.binding.textLock.setTrackTintList(android.content.res.ColorStateList.valueOf(androidx.core.graphics.ColorUtils.setAlphaComponent(androidx.core.content.ContextCompat.getColor(context, R.color.success), 77)));
holder.binding.viewProductMask.setVisibility(View.GONE);
holder.binding.textLock.setText(context.getString(R.string.unlocked));
holder.binding.textLock.setTextColor(Color.GRAY);
}
holder.binding.textLock.setOnClickListener(new textLockOnClickListener(position));
String productImg = slot.getProduct().getProductImg();
if (productImg != null && !productImg.isEmpty() && !productImg.equals("?")) {
try {
new ImageGlide(context).fileload(productImg, holder.binding.imageProduct);
} catch (LogsEmptyException e) {
holder.binding.imageProduct.setImageResource(R.drawable.slot_no_product);
}
} else {
holder.binding.imageProduct.setImageResource(R.drawable.slot_no_product);
}
holder.binding.buttonCountMinus.setOnClickListener(v -> {
int current = slot.getCount();
if (current > 0) {
slot.setCount(current - 1);
notifyItemChanged(position);
}
});
holder.binding.buttonCountPlus.setOnClickListener(v -> {
int current = slot.getCount();
slot.setCount(current + 1);
notifyItemChanged(position);
});
holder.binding.imageProduct.setOnClickListener(new selectPositionOnClickListener(position));
holder.binding.textProductName.setOnClickListener(null);
holder.binding.textProductName.setOnClickListener(new selectPositionOnClickListener(position));
holder.binding.textCount.setOnClickListener(new textCountOnClickListener(holder.binding, slot.getCount()));
holder.binding.textLock.setOnClickListener(new textLockOnClickListener(position));
holder.binding.textDelete.setOnClickListener(new textDeleteOnClickListener(position));
holder.binding.editCount.setOnEditorActionListener(new editCountOnEditorActionListener(holder.binding, position));
holder.binding.editCount.setOnClickListener(new editCountOnClickListener(holder.binding, slot.getCount()));
@ -202,9 +163,7 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
@Override
public void onClick(View v) {
// 改用 INVISIBLE 而非 GONE避免 buttonCountPlus constraint textCount 折疊
// 導致 editCount 可用寬度被壓縮成 0看不到輸入框IME 無法接受輸入
binding.textCount.setVisibility(View.INVISIBLE);
binding.textCount.setVisibility(View.GONE);
binding.editCount.setVisibility(View.VISIBLE);
if (this.count == 0) {
binding.editCount.setText("");

View File

@ -163,11 +163,6 @@ public class VmcFragment extends FragmentAbstract {
binding.recyclerBuy.setAdapter(recyclerVmcBuyListAdpter);
binding.recyclerBuy.setLayoutManager(new GridLayoutManager(getContext(), 3));
// pickup_sheet Cmuh模式顯示大掃描單據按鈕隱藏商品清單其餘模式相反
boolean isPickupSheet = "pickup_sheet".equals(com.unibuy.smartdevice.MyApp.getInstance().getShoppingMode());
binding.layoutScanBarcode.setVisibility(isPickupSheet ? View.VISIBLE : View.GONE);
binding.recyclerBuy.setVisibility(isPickupSheet ? View.GONE : View.VISIBLE);
// for (int i = 0; i < MyApp.getInstance().getSlotList(SlotField.VMC.getField()).size(); i++) {
// SlotStructure slot = MyApp.getInstance().getSlotList(SlotField.VMC.getField()).get(i);
// if (!slot.getProduct().getProductID().isEmpty() && !slot.getProduct().getProductName().isEmpty() && slot.getProduct().getSellingPrice() > 0) {

View File

@ -9,7 +9,7 @@ import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.unibuy.smartdevice.AppCompatActivityAbstract;
import com.unibuy.smartdevice.MyApp;
@ -127,7 +127,7 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
recyclerVmcSlotListAdpter = new RecyclerVmcSlotListAdpter(this, resultLauncher, MyApp.getInstance().getSlotList(SlotField.VMC.getField()));
binding.recyclerSlotList.setAdapter(recyclerVmcSlotListAdpter);
binding.recyclerSlotList.setLayoutManager(new GridLayoutManager(this, 3));
binding.recyclerSlotList.setLayoutManager(new LinearLayoutManager(this));
// dataShow();
@ -281,9 +281,6 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
}
private String validateMaterialCodeConsistency() {
if (!com.unibuy.smartdevice.AppEntry.isCmuh()) {
return "";
}
for (SlotStructure slot : MyApp.getInstance().getSlotList(SlotField.VMC.getField())) {
if (slot == null || slot.getProduct() == null) continue;
ProductStructure product = slot.getProduct();

View File

@ -10,7 +10,6 @@ import com.unibuy.smartdevice.databinding.DialogBuyBinding;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.DevSetStructure;
import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
@ -18,6 +17,7 @@ import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.tools.Tools;
import com.unibuy.smartdevice.ui.FontendActivity;
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogBuyListAdpter;
import com.unibuy.smartdevice.utils.LanguageHelper;
import java.util.HashMap;
@ -83,7 +83,21 @@ public class BuyDialog extends DialogAbstract {
setButtonCancelText(Integer.valueOf(message));
break;
case SET_PRICE:
refreshAmountBar();
String status = "";
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
case 0:
break;
case 1:
status = "("+context.getString(R.string.discounted)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountAmount()+")";
break;
case 2:
status = "("+context.getString(R.string.discounted1)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountPercentage()+")";
break;
}
}
binding.textPrice.setText(getTotalPrice()+status);
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM:
@ -103,6 +117,7 @@ public class BuyDialog extends DialogAbstract {
}
private DialogBuyBinding binding;
private RecyclerDialogBuyListAdpter recyclerDialogBuyListAdpter;
private CloseDialogOnThreadHandler closeDialogCountdown;
public BuyDialog(Context context, HandlerMain srcHandlerMain) {
@ -117,12 +132,35 @@ public class BuyDialog extends DialogAbstract {
// 讀取最後設定語言的設定
// LanguageHelper.applySavedLanguage(context);
this.recyclerDialogBuyListAdpter = new RecyclerDialogBuyListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogBuyListAdpter);
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
MyApp.getInstance().initialReportData();
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
this.binding.layoutPaymentTransactions.setVisibility(View.VISIBLE);
this.binding.layoutVerificationPickup.setVisibility(View.GONE);
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
this.binding.buttonPaymentTransactions.setOnClickListener(view -> {
this.binding.layoutPaymentTransactions.setVisibility(View.VISIBLE);
this.binding.layoutVerificationPickup.setVisibility(View.GONE);
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
closeDialogCountdown.setCountdown(40);
});
this.binding.buttonVerificationPickup.setOnClickListener(view -> {
this.binding.layoutPaymentTransactions.setVisibility(View.GONE);
this.binding.layoutVerificationPickup.setVisibility(View.VISIBLE);
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
closeDialogCountdown.setCountdown(40);
});
this.binding.buttonMemberTransactions.setOnClickListener(view -> {
});
this.binding.buttonNfcCard.setOnClickListener(v -> {
MyApp.getInstance().getReportFlowInfoData().setFlowType(1);
@ -169,121 +207,171 @@ public class BuyDialog extends DialogAbstract {
gotoDialog();
});
// 來店禮入口獨立判斷由雲端 B014 FunctionSet.WelcomeGift 下發控制DevSet.isWelcomeGift
// 與取貨模組無關關閉時隱藏來店禮按鈕避免進入 flowType 7
// 防呆本筆已套用過來店禮freeGiftId 非空即隱藏入口禁止重複掃描覆蓋疊加折扣
boolean welcomeGiftApplied =
!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty();
this.binding.buttonFreeGift.setVisibility(
(MyApp.getInstance().getDevSet().isWelcomeGift() && !welcomeGiftApplied)
? View.VISIBLE : View.GONE);
// 通行碼入口付款格內 buttonAccessCodeflowType 5權威判斷涵蓋購物車/單品所有模式
// 從屬取貨模組PickupModule PassCode 任一關閉即隱藏購物車模式下方不會再覆寫此值
this.binding.buttonAccessCode.setVisibility(
(MyApp.getInstance().getDevSet().isPickupModule()
&& MyApp.getInstance().getDevSet().isPassCode()) ? View.VISIBLE : View.GONE);
this.binding.buttonAddProduct.setOnClickListener(v -> gotoAddProduct());
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
refreshAmountBar();
String status = "";
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
case 0:
break;
case 1:
status = "("+context.getString(R.string.discounted)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountAmount()+")";
break;
case 2:
status = "("+context.getString(R.string.discounted1)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountPercentage()+")";
break;
}
}
binding.textPrice.setText(getTotalPrice()+status);
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
// 刷卡機 isDevNFCPay三子項各自 && 判斷母關則三者全隱藏
// 信用卡=NfcCard(canCreditCard)電子票證/卡片=NfcRecharge(canCardPay)手機=NfcPhone(canMobilePay)
binding.buttonNfcCard.setVisibility(MyApp.getInstance().getDevSet().canCreditCard() ? View.VISIBLE : View.GONE);
binding.buttonNfcRecharge.setVisibility(MyApp.getInstance().getDevSet().canCardPay() ? View.VISIBLE : View.GONE);
binding.buttonNfcPhone.setVisibility(MyApp.getInstance().getDevSet().canMobilePay() ? View.VISIBLE : View.GONE);
binding.buttonAddProduct.setVisibility(View.VISIBLE);
} else {
binding.buttonAddProduct.setVisibility(View.GONE);
}
// 掃碼 isDevScanPay子項玉山 / TapPay母關則隱藏
binding.buttonEsunPay.setVisibility(MyApp.getInstance().getDevSet().canEsunPay() ? View.VISIBLE : View.GONE);
binding.buttonTapPay.setVisibility(MyApp.getInstance().getDevSet().canTapPay() ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
if (MyApp.getInstance().getDevSet().isDevNFCPay()) {
// binding.layoutNfcPay.setVisibility(View.VISIBLE);
} else {
// binding.layoutNfcPay.setVisibility(View.GONE);
}
// Line 官方支付獨立大項不從屬掃碼
binding.buttonLinePay.setVisibility(MyApp.getInstance().getDevSet().canLinePay() ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isDevEsunPay()) {
binding.buttonEsunPay.setVisibility(View.VISIBLE);
} else {
binding.buttonEsunPay.setVisibility(View.GONE);
}
// 現金獨立大項
binding.buttonCash.setVisibility(MyApp.getInstance().getDevSet().canCash() ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isDevLinePay()) {
binding.buttonLinePay.setVisibility(View.VISIBLE);
} else {
binding.buttonLinePay.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevCash()) {
binding.buttonCash.setVisibility(View.VISIBLE);
} else {
binding.buttonCash.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevTapPay()) {
binding.buttonTapPay.setVisibility(View.VISIBLE);
} else {
binding.buttonTapPay.setVisibility(View.GONE);
}
} else {
BuyStructure buyData = MyApp.getInstance().getBuyList().get(0);
try {
SlotField slotField = SlotField.getSlotField(buyData.getField());
// 貨道層級是否支援 NFCVMC/ELECTRIC 不同三個刷卡機子項共用這個貨道旗標
boolean slotNfc = slotField.isDevNFCPay();
if (MyApp.getInstance().getDevSet().isDevNFCPay()) {
// 刷卡機開啟三子項各自 (&&) 且貨道支援 NFC 才顯示
binding.buttonNfcCard.setVisibility(
(MyApp.getInstance().getDevSet().canCreditCard() && slotNfc) ? View.VISIBLE : View.GONE);
binding.buttonNfcRecharge.setVisibility(
(MyApp.getInstance().getDevSet().canCardPay() && slotNfc) ? View.VISIBLE : View.GONE);
binding.buttonNfcPhone.setVisibility(
(MyApp.getInstance().getDevSet().canMobilePay() && slotNfc) ? View.VISIBLE : View.GONE);
if (slotField.isDevNFCPay()) {
getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
// binding.layoutNfcPay.setVisibility(View.VISIBLE);
} else {
// binding.layoutNfcPay.setVisibility(View.GONE);
}
} else {
// 刷卡機關閉三子項全隱藏並啟用通行碼備援入口仍須從屬取貨模組且通行碼開啟
// 否則 NFC 不支援時會把已關閉的通行碼漏出來
getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB2");
binding.buttonPaymentTransactions.setVisibility(View.GONE);
binding.buttonNfcCard.setVisibility(View.GONE);
binding.buttonNfcPhone.setVisibility(View.GONE);
binding.buttonNfcRecharge.setVisibility(View.GONE);
boolean passCodeOn = MyApp.getInstance().getDevSet().isPickupModule()
&& MyApp.getInstance().getDevSet().isPassCode();
binding.buttonAccessCode.setVisibility(passCodeOn ? View.VISIBLE : View.GONE);
binding.buttonAccessCode.setVisibility(View.VISIBLE);
binding.buttonVerificationPickup.setVisibility(View.GONE);
binding.layoutVerificationPickup.setVisibility(View.VISIBLE);
// binding.layoutNfcPay.setVisibility(View.GONE);
}
// 掃碼子項(isDevScanPay)&& 且貨道支援
binding.buttonEsunPay.setVisibility(
(MyApp.getInstance().getDevSet().canEsunPay() && slotField.isDevEsunpay()) ? View.VISIBLE : View.GONE);
binding.buttonTapPay.setVisibility(
(MyApp.getInstance().getDevSet().canTapPay() && slotField.isDevTapPay()) ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isDevEsunPay()) {
if (slotField.isDevEsunpay()) {
binding.buttonEsunPay.setVisibility(View.VISIBLE);
} else {
binding.buttonEsunPay.setVisibility(View.GONE);
}
} else {
binding.buttonEsunPay.setVisibility(View.GONE);
}
// Line 官方支付獨立大項僅貨道支援與自身開關
binding.buttonLinePay.setVisibility(
(MyApp.getInstance().getDevSet().canLinePay() && slotField.isDevLinePay()) ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isDevLinePay()) {
if (slotField.isDevLinePay()) {
binding.buttonLinePay.setVisibility(View.VISIBLE);
} else {
binding.buttonLinePay.setVisibility(View.GONE);
}
} else {
binding.buttonLinePay.setVisibility(View.GONE);
}
// 現金獨立大項
binding.buttonCash.setVisibility(
(MyApp.getInstance().getDevSet().canCash() && slotField.isDevCash()) ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isDevTapPay()) {
if (slotField.isDevTapPay()) {
binding.buttonTapPay.setVisibility(View.VISIBLE);
} else {
binding.buttonTapPay.setVisibility(View.GONE);
}
} else {
binding.buttonTapPay.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevCash()) {
if (slotField.isDevCash()) {
binding.buttonCash.setVisibility(View.VISIBLE);
} else {
binding.buttonCash.setVisibility(View.GONE);
}
} else {
binding.buttonCash.setVisibility(View.GONE);
}
} catch (LogsEmptyException e) {
// 無貨道資訊略過貨道層級限制只依&&開關判斷
binding.buttonNfcCard.setVisibility(MyApp.getInstance().getDevSet().canCreditCard() ? View.VISIBLE : View.GONE);
binding.buttonNfcRecharge.setVisibility(MyApp.getInstance().getDevSet().canCardPay() ? View.VISIBLE : View.GONE);
binding.buttonNfcPhone.setVisibility(MyApp.getInstance().getDevSet().canMobilePay() ? View.VISIBLE : View.GONE);
binding.buttonEsunPay.setVisibility(MyApp.getInstance().getDevSet().canEsunPay() ? View.VISIBLE : View.GONE);
binding.buttonTapPay.setVisibility(MyApp.getInstance().getDevSet().canTapPay() ? View.VISIBLE : View.GONE);
binding.buttonLinePay.setVisibility(MyApp.getInstance().getDevSet().canLinePay() ? View.VISIBLE : View.GONE);
binding.buttonCash.setVisibility(MyApp.getInstance().getDevSet().canCash() ? View.VISIBLE : View.GONE);
if (MyApp.getInstance().getDevSet().isDevNFCPay()) {
// binding.layoutNfcPay.setVisibility(View.VISIBLE);
} else {
// binding.layoutNfcPay.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevEsunPay()) {
binding.buttonEsunPay.setVisibility(View.VISIBLE);
} else {
binding.buttonEsunPay.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevLinePay()) {
binding.buttonLinePay.setVisibility(View.VISIBLE);
} else {
binding.buttonLinePay.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevTapPay()) {
binding.buttonTapPay.setVisibility(View.VISIBLE);
} else {
binding.buttonTapPay.setVisibility(View.GONE);
}
if (MyApp.getInstance().getDevSet().isDevCash()) {
binding.buttonCash.setVisibility(View.VISIBLE);
} else {
binding.buttonCash.setVisibility(View.GONE);
}
}
}
}
/** 商品原始總額(未套用來店禮折扣),不含免費商品。 */
public int getOriginalPrice() {
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().isEmpty()) {
return totalPrice;
}
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
if (!buy.isFreeGift()) totalPrice += buy.getFullPrice();
}
return totalPrice;
}
/** 商品總件數(不含免費商品),用於支付頁顯示「共 X 件」。 */
public int getTotalCount() {
int count = 0;
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
if (!buy.isFreeGift()) count += buy.getCount();
}
return count;
}
public int getTotalPrice() {
if (MyApp.getInstance().getBuyList().isEmpty()) {
return 0;
}
int totalPrice = getOriginalPrice();
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
case 0:
@ -296,75 +384,55 @@ public class BuyDialog extends DialogAbstract {
break;
}
}
if (totalPrice < 1) totalPrice = 1; // 折抵後至少 1 避免 0 元交易
if (totalPrice == 0) totalPrice = 1;
return totalPrice;
}
/**
* 刷新支付頁上方件數 / 應付金額
* 金額已含來店禮折抵有套用來店禮時於後方標示實際折抵金額折抵/打折統一以省下的錢顯示
*/
private void refreshAmountBar() {
binding.textCount.setText(getContext().getString(R.string.pay_item_count, getTotalCount()));
int payable = getTotalPrice();
String priceText = "NT$" + payable;
android.text.SpannableStringBuilder spannable = new android.text.SpannableStringBuilder(priceText);
spannable.setSpan(
new android.text.style.ForegroundColorSpan(androidx.core.content.ContextCompat.getColor(getContext(), R.color.primary)),
0,
priceText.length(),
android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
int saved = getOriginalPrice() - payable;
if (saved > 0) {
String discountPart = " (" + getContext().getString(R.string.pay_discounted_format, saved) + ")";
int start = spannable.length();
spannable.append(discountPart);
spannable.setSpan(
new android.text.style.ForegroundColorSpan(androidx.core.content.ContextCompat.getColor(getContext(), R.color.success)),
start,
spannable.length(),
android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
binding.textPrice.setText(spannable);
public void gotoAddProduct() {
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.ADD_PRODUCT.getOption(), "@請40秒內完成加購動作");
closeDialogCountdown.shutdown();
cancel();
}
public void dialogCancel() {
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
closeDialogCountdown.shutdown();
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
cancel();
}
public void cancelOnThreadCountdown() {
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
getTools().dialogClose();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText(getContext().getString(R.string.cancel) + "" + countdown + "秒)");//取消
binding.buttonCancel.setText(getContext().getString(R.string.cancel)+"("+countdown+")");//取消
}
public void gotoDialog() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
getLogs().debug("buy flowType:" + flowType);
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaa"+flowType);
// if (flowType == 5 || flowType == 7 || flowType == 30) {
if (flowType == 5 || flowType == 7) {
changeCheckout();
} else {
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
if (MyApp.getInstance().getDevSet().isInvoice()) {
changeDialog(InvoiceTypeDialog.class);
changeDialog(InvoiceBarcodeDialog.class);
} else {
changeCheckout();
}
@ -374,12 +442,12 @@ public class BuyDialog extends DialogAbstract {
try {
SlotField slotField = SlotField.getSlotField(buyData.getField());
if (slotField.isInvoice()) {
changeDialog(InvoiceTypeDialog.class);
changeDialog(InvoiceBarcodeDialog.class);
} else {
changeCheckout();
}
} catch (LogsEmptyException e) {
changeDialog(InvoiceTypeDialog.class);
changeDialog(InvoiceBarcodeDialog.class);
}
} else {
changeCheckout();
@ -391,28 +459,6 @@ public class BuyDialog extends DialogAbstract {
public void changeCheckout() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
// 入口防呆即使按鈕被繞過殘留事件/外部觸發進入支付 dialog 前再以同一份&&
// 開關權威判斷一次母開關或子項任一關閉即擋下避免已停用的支付方式漏出
if (!isPaymentFlowAllowed(flowType)) {
getLogs().info("⛔ 支付方式已停用,封堵入口 flowType=" + flowType);
dialogCancel();
return;
}
// 進入付款先清掉上一筆殘留的 MQTT 交易流水號避免被本筆誤用flow_id 撞單
com.unibuy.smartdevice.structure.ReportFlowInfoStructure flowInfo =
MyApp.getInstance().getReportFlowInfoData();
flowInfo.setMqttFlowId("");
// basic 購物模式(本專案 general/demo) 且為實際付款方式(1/2/3/4/9/30/70)
// 為本筆生成新流水號並上報 pending進付款一律留底pending completed/failed 全程共用此 flow_id
// 通行碼(5)/來店禮(7) 非付款不在此處理employee_card(晟崴)/pickup_sheet(中國醫) 無付款不送後台資料不受影響
boolean realPayment = (flowType == 1 || flowType == 2 || flowType == 3 || flowType == 4
|| flowType == 9 || flowType == 30 || flowType == 70);
if ("basic".equals(MyApp.getInstance().getShoppingMode()) && realPayment) {
flowInfo.setMqttFlowId(com.unibuy.smartdevice.tools.FlowIdGenerator.next(MyApp.getInstance()));
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishPending();
}
switch (flowType) {
case 1:
changeDialog(CheckoutNfcCardDialog.class);
@ -433,7 +479,7 @@ public class BuyDialog extends DialogAbstract {
changeDialog(CheckoutFreeGiftCodeDialog.class);
break;
case 9:
changeDialog(cashDialogClass());
changeDialog(CashPayDialog.class);
break;
case 30:
changeDialog(CheckoutTapPayDialog.class);
@ -444,24 +490,6 @@ public class BuyDialog extends DialogAbstract {
}
}
/**
* 入口防呆 flowType 對應的支付方式判斷其&&開關是否允許
* flowType 5通行碼7來店禮非支付方式由取貨模組/來店禮各自把關此處放行
*/
private boolean isPaymentFlowAllowed(int flowType) {
DevSetStructure d = MyApp.getInstance().getDevSet();
switch (flowType) {
case 1: return d.canCreditCard(); // 信用卡刷卡機
case 2: return d.canCardPay(); // 電子票證/卡片支付刷卡機
case 4: return d.canMobilePay(); // 手機支付刷卡機
case 3: return d.canEsunPay(); // 玉山掃碼
case 30: return d.canTapPay(); // TapPay掃碼
case 70: return d.canLinePay(); // Line 官方支付獨立
case 9: return d.canCash(); // 現金獨立
default: return true; // 5/7 等非支付流程放行
}
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);

View File

@ -46,7 +46,6 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
SET_STATUS_TEXT(8),
SET_SCAN_INPUT(9),
;
private int option;
@ -114,11 +113,6 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
case SET_STATUS_TEXT:
setStatusText(message);
break;
case SET_SCAN_INPUT:
// 掃碼即時回饋把掃到的內容顯示在輸入框讓使用者確認有掃到
binding.editTextNumber.setText(message);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
break;
}
}
}
@ -144,18 +138,6 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
openBarcode = true;
httpAPI = new HttpAPI(getHandlerMain());
setInputMode(true);
binding.btnModeKeypad.setOnClickListener(v -> {
setInputMode(true);
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
});
binding.btnModeScan.setOnClickListener(v -> {
setInputMode(false);
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
});
binding.buttonFinish.setOnClickListener(v -> changeOk());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
@ -259,8 +241,7 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
closeDialogCountdown.setCountdown(40);
});
// 立即開始聽掃描器原本延遲 5 導致對話框剛彈出時掃碼被整個丟掉 時好時壞無回饋
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
@ -284,14 +265,19 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<9; i++) { // 45 秒讀取窗口覆蓋 40 秒對話框存活時間
for (int i=0; i<7; i++) {
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {
String barCodeString = new String(buffer, 0, size).trim();
// 掃碼即時回饋先把掃到的內容顯示在輸入框再送出驗證
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
submitPassCode(barCodeString);
String barCodeString = new String(buffer, 0, size);
getHandlerMain().start(getLogs().getClassName(), getContext().getString(R.string.receive_barcode)+""+ Tools.randomReplace(barCodeString, 0.3)+""+getContext().getString(R.string.send_transaction));//收到條碼送出交易
try {
MyApp.getInstance().getAccessCodeStructure().setPassCode(barCodeString);
httpAPI.checkAccessCode(MyApp.getInstance().getAccessCodeStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
openBarcode = false;
}
@ -309,14 +295,6 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
}
}
private void setInputMode(boolean isKeyboard) {
binding.btnModeKeypad.setSelected(isKeyboard);
binding.btnModeScan.setSelected(!isKeyboard);
binding.linearNumpad.setVisibility(isKeyboard ? android.view.View.VISIBLE : android.view.View.GONE);
binding.imageViewScanHint.setVisibility(isKeyboard ? android.view.View.GONE : android.view.View.VISIBLE);
}
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
@ -329,70 +307,26 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
public void changeDispatch() {
if (isChangeDispatch) {
closeDialogCountdown.shutdown();
// 進入出貨前務必關閉 FontendActivity 加購倒數(AddProductOnCountdown)
// 否則該 40 秒倒數會在出貨中途到期 自動發 BUY_DIALOG_OPEN 支付畫面疊在出貨畫面上跳出
getSrcHandlerMain().start(getClass().getSimpleName(),
FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
isChangeDispatch = false;
changeDialog(DispatchDialog.class);
}
}
private long lastChangeOkClickTime = 0;
public void changeOk() {
try {
// 防連點3 秒內重複點擊忽略 Luzui 移植
long now = System.currentTimeMillis();
if (now - lastChangeOkClickTime < 3000) {
return;
}
lastChangeOkClickTime = now;
String number = binding.editTextNumber.getText().toString().trim();
String number = binding.editTextNumber.getText().toString();
// 長度檢查
if (number.isEmpty() || number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), "通行碼長度不能少於 8 碼");
if (number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
return;
}
submitPassCode(number);
} catch (Exception e) {
MyApp.getInstance().getAccessCodeStructure().setPassCode(number);
httpAPI.checkAccessCode(MyApp.getInstance().getAccessCodeStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
}
/**
* 通行碼驗證改走 StarCloudAPI B670邏輯比照員工卡 B680
* 成功後保存 code_id MQTT finalizepayment_type=5 通行碼再進入出貨
*/
private void submitPassCode(String code) {
// 防護通行碼從屬取貨模組任一關閉即不開放呼叫 B670即使 UI 漏出入口也擋下
if (!(MyApp.getInstance().getDevSet().isPickupModule() && MyApp.getInstance().getDevSet().isPassCode())) {
openBarcode = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "通行碼功能未開放");
return;
}
// 送出前顯示驗證中避免使用者以為沒反應
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
.verifyPassCode(code, new com.unibuy.smartdevice.external.StarCloudAPI.VerificationCallback() {
@Override
public void onSuccess(String codeId) {
MyApp.getInstance().getReportFlowInfoData().setFlowType(5); // 通行碼 payment_type 5
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
MyApp.getInstance().getReportAccessCodeStructure().setAccessCodeId(codeId);
getHandlerMain().send(getClass().getSimpleName(), Option.CHANGE_DISPATCH.getOption(), "change dispatch");
}
@Override
public void onFailure(String message) {
openBarcode = true; // 允許重新掃碼/輸入
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), message);
}
});
}
public void changeBuy() {
closeDialogCountdown.shutdown();
changeDialog(BuyDialog.class);

View File

@ -100,22 +100,25 @@ public class CheckoutEsunPayDialog extends DialogAbstract {
case TRANSACTION_SUCCESSFUL:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=3掃碼支付已於 BuyDialog 設定
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
isTransaction = false;
changeDispatch();
}
break;
case TRANSACTION_FAILED:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
// [交易生命週期] 掃碼支付失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case CHANGE_DISPATCH:

View File

@ -39,7 +39,6 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
SET_STATUS_TEXT(8),
SET_SCAN_INPUT(9),
;
private int option;
@ -107,11 +106,6 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
case SET_STATUS_TEXT:
setStatusText(message);
break;
case SET_SCAN_INPUT:
// 掃碼即時回饋把掃到的內容顯示在輸入框讓使用者確認有掃到
binding.editTextNumber.setText(message);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
break;
}
}
}
@ -137,18 +131,6 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
openBarcode = true;
httpAPI = new HttpAPI(getHandlerMain());
setInputMode(true);
binding.btnModeKeypad.setOnClickListener(v -> {
setInputMode(true);
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
});
binding.btnModeScan.setOnClickListener(v -> {
setInputMode(false);
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
});
binding.buttonFinish.setOnClickListener(v -> changeOk());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
@ -250,8 +232,7 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
closeDialogCountdown.setCountdown(40);
});
// 立即開始聽掃描器原本延遲 5 導致對話框剛彈出時掃碼被整個丟掉 時好時壞無回饋
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
@ -275,14 +256,20 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<9; i++) { // 45 秒讀取窗口覆蓋 40 秒對話框存活時間
for (int i=0; i<7; i++) {
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {
String barCodeString = new String(buffer, 0, size).trim();
// 掃碼即時回饋先把掃到的內容顯示在輸入框再送出驗證
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
submitWelcomeGift(barCodeString);
String barCodeString = new String(buffer, 0, size);
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+""+ Tools.randomReplace(barCodeString, 0.3)+""+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
try {
MyApp.getInstance().getFreeGiftStructure().setPassCode(barCodeString);
httpAPI.freeGiftCode(MyApp.getInstance().getFreeGiftStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
openBarcode = false;
}
}
@ -299,14 +286,6 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
}
}
private void setInputMode(boolean isKeyboard) {
binding.btnModeKeypad.setSelected(isKeyboard);
binding.btnModeScan.setSelected(!isKeyboard);
binding.linearNumpad.setVisibility(isKeyboard ? android.view.View.VISIBLE : android.view.View.GONE);
binding.imageViewScanHint.setVisibility(isKeyboard ? android.view.View.GONE : android.view.View.VISIBLE);
}
public int getTotalPrice() {
int totalPrice = 0;
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
@ -325,57 +304,15 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
}
public void changeOk() {
String number = binding.editTextNumber.getText().toString().trim();
if (number.isEmpty() || number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "來店禮代碼需 8 碼");
return;
try {
String number = binding.editTextNumber.getText().toString();
MyApp.getInstance().getFreeGiftStructure().setPassCode(number);
httpAPI.freeGiftCode(MyApp.getInstance().getFreeGiftStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
}
submitWelcomeGift(number);
}
/**
* 來店禮驗證改走 StarCloudAPI B690取代舊 HttpAPI.freeGiftCode
* B690 只回折扣amount/percentage免費商品成功即套折扣並返回 BuyDialog 結帳
*/
private void submitWelcomeGift(String code) {
// 防護來店禮關閉即不開放呼叫 B690即使 UI 漏出入口也擋下
if (!MyApp.getInstance().getDevSet().isWelcomeGift()) {
openBarcode = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "來店禮功能未開放");
return;
}
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
.verifyWelcomeGift(code, new com.unibuy.smartdevice.external.StarCloudAPI.WelcomeGiftCallback() {
@Override
public void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name) {
// 映射到既有折扣機制mode 1=折抵金額mode 2=折扣百分比乘數
MyApp.getInstance().getReportFreeGiftCodeStructure().setFreeGiftId(codeId);
MyApp.getInstance().getReportFreeGiftCodeStructure().setPassCode(code);
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
if ("amount".equals(discountType)) {
MyApp.getInstance().getReportFreeGiftCodeStructure().setMode(1);
MyApp.getInstance().getReportFreeGiftCodeStructure().setDiscountAmount(discountValue);
} else if ("percentage".equals(discountType)) {
MyApp.getInstance().getReportFreeGiftCodeStructure().setMode(2);
// discountValue 為折扣趴數八折=20 換算成總價乘數 0.8
MyApp.getInstance().getReportFreeGiftCodeStructure()
.setDiscountPercentage((100 - discountValue) / 100f);
} else {
openBarcode = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "不支援的折扣類型");
return;
}
getHandlerMain().start(getClass().getSimpleName(), Option.CHANGE_BUY.getOption(), "change buy");
}
@Override
public void onFailure(String message) {
openBarcode = true; // 允許重新掃碼/輸入
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), message);
}
});
}
public void changeBuy() {

View File

@ -101,22 +101,25 @@ public class CheckoutLinePayDialog extends DialogAbstract {
case TRANSACTION_SUCCESSFUL:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=70LINE Pay 官方已於 BuyDialog 設定
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
isTransaction = false;
changeDispatch();
}
break;
case TRANSACTION_FAILED:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
// [交易生命週期] LINE Pay 失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case CHANGE_DISPATCH:

View File

@ -109,21 +109,18 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
if (message.equals("0000")) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=1信用卡已於 BuyDialog 設定
changeDispatch();
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回非 0000上報 failed(帶代碼)救回原本默默消失的失敗單flag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -108,24 +108,18 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
if (message.equals("0000")) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// NfcPhone(手機支付)刷卡機 NCCC 電文無法區分手機錢包/實體卡
// 依使用者所選付款鈕歸類為 payment_type=10手機支付
// 路由鍵 flowType=4 僅用於 BuyDialog 開啟本 Dialog這裡覆寫為實際上報值
MyApp.getInstance().getReportFlowInfoData().setFlowType(10);
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
changeDispatch();
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回非 0000上報 failed(帶代碼)救回原本默默消失的失敗單flag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -108,21 +108,18 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
if (message.equals("0000")) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=2悠遊卡/一卡通已於 BuyDialog 設定
changeDispatch();
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回非 0000上報 failed(帶代碼)救回原本默默消失的失敗單flag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -123,21 +123,22 @@ public class CheckoutTapPayDialog extends DialogAbstract {
if (isTransaction) {
isTransaction = false; // 🚨 馬上關閉避免送出第二筆
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=30~34TapPay 子項已於本 Dialog 選擇時設定
changeDispatch();
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
}
break;
case TRANSACTION_FAILED:
if (isTransaction) {
isTransaction = false; // 🚨 馬上關閉
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
// [交易生命週期] 掃碼支付失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
}
break;
case TRANSACTION_AGAIN:

View File

@ -1,220 +0,0 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import com.blankj.utilcode.util.ToastUtils;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.DialogProductDetailBinding;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.ui.FontendActivity;
import com.unibuy.smartdevice.ui.SaleFlowHandler;
import com.unibuy.smartdevice.ui.tools.ImageGlide;
import com.unibuy.v2.UnibuyHelper;
/**
* 商品詳情對話框
* 點選商品後一律先彈出此頁依雲端 B014 DevSet.ShoppingCar 決定按鈕
* - 購物車開啟顯示數量調整 + 加入購物車 + 直接購買
* - 購物車關閉一般版隱藏數量調整 + 加入購物車只留直接購買數量固定 1
* 直接購買沿用既有單品直購流程清空 buyList BuyDialog 選付款
*/
public class DialogProductDetail extends DialogAbstract {
private DialogProductDetailBinding binding;
private final SlotStructure slot;
// 建構子傳入的原始 Context真正的 FontendActivity
// 不可用 onCreate getContext()那是 AlertDialog 包裝過的 ContextThemeWrapper
// instanceof FontendActivity 會是 false導致購物車頁/結帳入口取不到 SaleFlowHandler
private final Context hostContext;
private int quantity = 1;
public DialogProductDetail(Context context, HandlerMain srcHandlerMain, SlotStructure slot) {
super(context, srcHandlerMain);
this.hostContext = context;
if (slot == null || slot.getProduct() == null) {
this.slot = createMockSlot();
} else {
this.slot = slot;
}
}
private SlotStructure createMockSlot() {
com.unibuy.smartdevice.structure.ProductStructure mockProduct = new com.unibuy.smartdevice.structure.ProductStructure(
"mock_001",
"",
"模擬商品名稱 (Mock)",
30,
30,
30,
10,
"",
"MOCK_CODE",
"Mock Product Name",
"模擬の商品"
);
mockProduct.setSpecI18nJson("{\"zh_TW\":\"此為模擬商品描述,用於當系統尚未寫入真實商品資料時,可供開發者與測試人員模擬查看詳細頁面的版面配置與樣式呈現。\\n支援換行與多種排版。\",\"en\":\"This is a mock product description used for simulating layout and styling before real data is populated.\"}");
return new SlotStructure(1, 99, mockProduct, 5);
}
private boolean isMock() {
return slot != null && slot.getProduct() != null && "mock_001".equals(slot.getProduct().getProductID());
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getContext(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
// 本對話框不需處理回呼指令
}
};
}
@Override
protected void onCreate(Context context) {
binding = DialogProductDetailBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 商品圖片
try {
new ImageGlide(context).fileload(slot.getProduct().getProductImg(), binding.imageProduct);
} catch (LogsEmptyException e) {
getLogs().warning(e);
}
// 商品名稱多語貨道庫存
binding.textProductName.setText(slot.getProduct().getLocalizedName(context));
binding.slotNumber.setText("貨道" + slot.getSlot());
binding.inventoryNumber.setText("庫存" + slot.getCount());
// 商品規格描述多語純文字含換行後台空白則隱藏整塊
// setText 原樣呈現換行與空格縮排版面已設 gravity=start故縮排不會被置中打亂
String spec = slot.getProduct().getLocalizedSpec(context);
if (spec == null || spec.trim().isEmpty()) {
binding.textProductDescription.setVisibility(android.view.View.GONE);
} else {
binding.textProductDescription.setText(spec);
}
// 依購物車開關決定按鈕呈現
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
if (!cartOn) {
// 一般版隱藏數量調整與加入購物車只留直接購買數量固定 1
binding.layoutQuantityControl.setVisibility(android.view.View.GONE);
binding.btnAddToCart.setVisibility(android.view.View.GONE);
quantity = 1;
}
refreshQuantity();
binding.ivCancel.setOnClickListener(v -> cancel());
binding.buttonMinus.setOnClickListener(v -> {
if (quantity > 1) {
quantity--;
refreshQuantity();
}
});
binding.buttonPlus.setOnClickListener(v -> {
// 上限貨道庫存與購物車總量上限8
if (quantity >= slot.getCount()) {
ToastUtils.showShort(R.string.cart_out_of_stock);
return;
}
if (quantity >= 8) {
ToastUtils.showShort(R.string.cart_max_items);
return;
}
quantity++;
refreshQuantity();
});
// 加入購物車以選定數量加入購物車後停留商品列表CartViewModel 內建庫存8 件上限防呆
binding.btnAddToCart.setOnClickListener(v -> {
if (isMock()) {
ToastUtils.showShort("此為模擬商品,無法加入購物車");
cancel();
return;
}
addSelectedQuantityToCart();
ToastUtils.showShort(R.string.cart_added);
cancel();
});
// 直接購買
// - 購物車開啟以選定數量加入購物車後前往購物車列表頁數量帶入不直接結帳
// - 購物車關閉一般版單品直購清空 buyList 放入本商品後彈 BuyDialog 選付款
binding.btnBuyNow.setOnClickListener(v -> {
if (isMock()) {
ToastUtils.showShort("此為模擬商品,無法購買");
cancel();
return;
}
if (cartOn) {
addSelectedQuantityToCart();
// 先切到購物車品項頁於詳情頁視窗後方再關閉詳情頁顯示出來
// 避免關閉對話框fragment 切換時序互相干擾導致沒跳頁
openCartList();
cancel();
} else {
MyApp.getInstance().getBuyList().clear();
MyApp.getInstance().getBuyList().add(
new BuyStructure(slot.getField(), slot.getSlot(), slot.getProduct(), quantity));
cancel();
requestBuy();
}
});
}
/** 以詳情頁選定數量加入購物車逐件累加數量確實帶入受庫存8 件上限限制)。 */
private void addSelectedQuantityToCart() {
for (int i = 0; i < quantity; i++) {
UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
}
}
/** 前往購物車列表頁(沿用各 flavor 的購物車入口)。 */
private void openCartList() {
if (hostContext instanceof FontendActivity) {
SaleFlowHandler handler = ((FontendActivity) hostContext).getSaleFlowHandler();
if (handler != null) {
handler.onShoppingCartRequested();
}
}
}
private void refreshQuantity() {
binding.textQuantity.setText(String.valueOf(quantity));
int total = slot.getProduct().getRealPrice() * quantity;
binding.textPrice.setText("$" + total);
}
/** 沿用各 flavor 的結帳入口basic→BuyDialog非 FontendActivity 時直接開 BuyDialog。 */
private void requestBuy() {
if (hostContext instanceof FontendActivity) {
SaleFlowHandler handler = ((FontendActivity) hostContext).getSaleFlowHandler();
if (handler != null) {
handler.onBuyRequested();
return;
}
}
new BuyDialog(hostContext, getSrcHandlerMain()).show();
}
}

View File

@ -253,34 +253,14 @@ public class DispatchDialog extends DialogAbstract {
protected void onCreate(Context context) {
binding = DialogDispatchBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 進入出貨統一保險關閉 FontendActivity 加購倒數(AddProductOnCountdown)
// 任一支付方式只要走到出貨就不該再被 40 秒加購倒數到期觸發 BUY_DIALOG_OPEN
// 避免支付畫面在出貨/取貨倒數中途疊出通行碼TapPay 等成功路徑原本未送 STOP_COUNTDOWN
getSrcHandlerMain().start(getClass().getSimpleName(),
FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
isReportDispatch = true;
tvCountdown = binding.tvCountdown;
httpAPI = new HttpAPI(getHandlerMain());
// 初始化 Finalize Builder在支付完成進入出貨流程時建立
finalizeBuilder = new TransactionFinalizeBuilder();
// 進入出貨流程當下 buyList 必為完整下方即用它組 dispatchList先擷取商品明細快照
// 之後結案 build() 會優先採用此快照避免掃碼支付(玉山)等慢流程在結案前 buyList
// 被取消/倒數路徑清空導致 order.items 變空無商品紀錄但出貨其實成功
finalizeBuilder.captureItems();
if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
MyApp.getInstance().getReportFlowInfoData().setFlowType(42);
}
// 快照訂單層級欄位flowType / 金額 / 找零 / 點數須在上方 isCmuh42 覆寫之後
// 且趁全域 flowInfo 仍正確時擷取結案 build() 通行碼(5)不上報判斷都改用此快照
// 不再讀結案當下的全域即時值避免出貨中途任何路徑如殘留加購倒數彈出 BuyDialog
// initialReportData()把全域洗回 0導致通行碼誤報payment_type/total 落地成 0未知
finalizeBuilder.captureOrderInfo();
// [MQTT Finalize 重構] 電子發票改由後台開立機台不再直連綠界
// 僅把發票輸入載具/愛心碼/統編/金額 finalize invoice 物件上報TransactionFinalizeBuilder 組裝
// 後台收到 finalize 後建 pending 發票並派 Job 去綠界開立避免機台網路不確定性與等待
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
@ -331,7 +311,7 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("DispatchList size:" + this.dispatchList.size());
if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
try {
String patName = "未知患者";
String sn = "";
@ -463,15 +443,6 @@ public class DispatchDialog extends DialogAbstract {
}
public void dialogCancel() {
// 安全網出貨流程已開始dispatchList 非空但尚未結案既非成功 finalize也非錯誤 finalize
// 在關閉前補送一次 MQTT Finalize部分出貨/未完成避免走倒數結束 / dispatchIndex 越界 / 中止
// 等非錯誤關閉路徑時整筆交易完全不上報isFinalizing 擋住正常成功/錯誤路徑不會重複上報
// 注意須在下方 buyList.clear() 之前執行 finalizeBuilder.build() 會讀 buyList 組出商品明細
if (!isFinalizing && finalizeBuilder != null && dispatchList != null && !dispatchList.isEmpty()) {
getLogs().info("dialogCancel偵測到尚未結案的出貨流程補送 MQTT Finalize部分/未完成)");
finalizeFailedDispatch("出貨流程關閉前補送結案");
}
closeDialogCountdown.shutdown();
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
@ -514,7 +485,7 @@ public class DispatchDialog extends DialogAbstract {
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
if (isCheckSlotByVMC)
return;
getLogs().debug("dispatch flow 006");
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA006");
isCheckSlotByVMC = true; // 進來立即設定true可以判斷掉重複出貨
boolean isCMD = false;
@ -558,7 +529,7 @@ public class DispatchDialog extends DialogAbstract {
R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));// 請檢查商品有無取出再確認取貨
}
} else {
boolean isCmuh = com.unibuy.smartdevice.AppEntry.isCmuh();
boolean isCmuh = "Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project);
int maxRetries = isCmuh ? 10 : 3;
if (checkSlotRunCount >= maxRetries) {
buttonControlCountdown.shutdown();
@ -689,7 +660,7 @@ public class DispatchDialog extends DialogAbstract {
getSrcHandlerMain().send(getClass().getSimpleName(), Option.NEXT_PROCESS_GOODS.getOption(),
"next process goods");
} else {
getLogs().debug("dispatch flow 002");
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA002");
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L);
}
@ -702,7 +673,7 @@ public class DispatchDialog extends DialogAbstract {
getCtx().getString(R.string.product_delivery_issues_contact_the_counter_window));
}
} else {
getLogs().debug("dispatch flow 003");
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA003");
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L);
MyApp.getInstance().getDevXinYuanController()
@ -969,13 +940,10 @@ public class DispatchDialog extends DialogAbstract {
isFinalizing = true;
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
// 整筆交易出貨成功結案清空 v2 多品項購物車單例避免下一位客人看到上一筆殘留商品
// 僅在成功結案取消結帳不清保留購物車供客人重試
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().clearCart();
if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
insertDrgnoToMemo();
}
publishFinalizeUnlessPassCode();
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(),
"update data");
saveData();
@ -1084,33 +1052,19 @@ public class DispatchDialog extends DialogAbstract {
addFailedDispenseRecordsFromCurrent();
if (com.unibuy.smartdevice.AppEntry.isCmuh() && hasAnySuccessfulDispatch()) {
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project) && hasAnySuccessfulDispatch()) {
insertDrgnoToMemo();
} else if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
} else if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
getLogs().info("CMUH 整單未出貨成功,不寫入 SN/DRGNO Memo允許重新掃碼領藥");
}
getLogs().info("出貨失敗,發送 MQTT Finalize 結案訊息: " + message);
publishFinalizeUnlessPassCode();
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(),
"update data");
saveData();
}
/**
* 發送 MQTT 交易結案 flowType 5通行碼為工程師/內部硬體測試不上報交易
* B670 驗證已於後台記 log避免污染銷售/營收且不扣減雲端庫存
*/
private void publishFinalizeUnlessPassCode() {
// finalizeBuilder 在進入出貨當下擷取的快照判斷而非結案當下的全域即時值
// 防止出貨中途全域 flowType 被洗回 0如殘留加購倒數彈出 BuyDialog而導致通行碼被誤報
if (finalizeBuilder != null && finalizeBuilder.getOrderFlowType() == 5) {
getLogs().info("通行碼(payment_type=5) 為內部測試出貨,略過 MQTT 交易結案上報。");
return;
}
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
}
private void addFailedDispenseRecordsFromCurrent() {
if (dispatchList == null || dispatchList.isEmpty()) {
return;

View File

@ -179,7 +179,7 @@ public class InvoiceBarcodeDialog extends DialogAbstract {
binding.buttonCustomerIdentifier.setOnClickListener(v -> changeCustomerIdentifier());
// binding.buttonDonation.setOnClickListener(v -> changeDonation());
binding.buttonDonation.setOnClickListener(v -> changeDialog(InvoiceDonationDialog.class));
binding.buttonDonation.setOnClickListener(v -> showDonationDialog());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
binding.buttonInputVehicle.setOnClickListener(v -> showInputVehicleDialog());
@ -295,6 +295,116 @@ public class InvoiceBarcodeDialog extends DialogAbstract {
changeCheckout();
}
private void showDonationDialog() {
closeDialogCountdown.shutdown();
AlertDialog.Builder builder = new AlertDialog.Builder(getCtx());
LayoutInflater inflater = getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_donation, null);
builder.setView(dialogView);
CheckBox checkboxDefault = dialogView.findViewById(R.id.checkboxDefault);
Spinner spinnerCharity = dialogView.findViewById(R.id.spinnerCharity);
TextView tvlovecode = dialogView.findViewById(R.id.tvlovecode);
// 1. 建立捐贈機構對應表
// 假設這是你的捐贈機構清單
List<String> organizationNames = new ArrayList<>(donationMap.keySet());
ArrayAdapter<String> adapter = new ArrayAdapter<>( getCtx(),
R.layout.spinner_item, // 改成自訂 layout
R.id.spinnerText,
organizationNames);
adapter.setDropDownViewResource(R.layout.spinner_item); // 下拉選單也套用
spinnerCharity.setAdapter(adapter);
// 2. 預設勾選 checkbox並禁用 spinner
checkboxDefault.setChecked(true);
spinnerCharity.setEnabled(false);
// 3. 隨機一組愛心碼並立刻寫入
Map.Entry<String, String> randomEntry = getRandomDonationEntry(donationMap);
if (randomEntry != null) {
MyApp.getInstance().getInvoiceData().setLoveCode(randomEntry.getValue());
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("loveCode:" + MyApp.getInstance().getInvoiceData().getLoveCode());
// Toast.makeText(getCtx(), "預設捐贈:" + randomEntry.getKey(), Toast.LENGTH_SHORT).show();
tvlovecode.setText("捐贈機構:"+randomEntry.getKey());
}
// 自訂 Title
TextView title = new TextView(getCtx());
title.setText("選擇捐贈方式");
title.setTextSize(24); // 放大字體
title.setPadding(40, 40, 40, 40);
title.setGravity(Gravity.CENTER);
donationDialog = new AlertDialog.Builder(getCtx())
.setCustomTitle(title) // 使用自訂 Title
.setView(dialogView)
.setPositiveButton("確定", (d, which) -> {
if (!checkboxDefault.isChecked()) {
String selectedName = (String) spinnerCharity.getSelectedItem();
String loveCode = donationMap.get(selectedName);
MyApp.getInstance().getInvoiceData().setLoveCode(loveCode);
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("loveCode:" + MyApp.getInstance().getInvoiceData().getLoveCode());
Toast.makeText(getCtx(), "選擇:" + selectedName, Toast.LENGTH_SHORT).show();
}
// 若是勾選預設之前就寫入過不需處理
closeDialogCountdown.start(40); // 再次開始倒數
changeCheckout();
})
.setNegativeButton("取消", (d, which) -> {
// 使用者取消後重新開始倒數 40
closeDialogCountdown.start(40);
})
.create();
donationDialog.show();
Button positive = donationDialog.getButton(AlertDialog.BUTTON_POSITIVE);
Button negative = donationDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
if (positive != null) {
positive.setTextSize(24);
positive.setPadding(30, 20, 130, 20);
}
if (negative != null) {
negative.setTextSize(24);
negative.setPadding(30, 20, 30, 20);
}
// 放大框架 (寬度 90%)
Window window = donationDialog.getWindow();
if (window != null) {
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.copyFrom(window.getAttributes());
params.width = (int) (getCtx().getResources().getDisplayMetrics().widthPixels * 0.9);
params.height = (int) (getCtx().getResources().getDisplayMetrics().heightPixels * 0.6); // 高度 60%
window.setAttributes(params);
}
// 加入20秒後自動關閉 Dialog
Handler handler = new Handler();
Runnable timeoutRunnable = () -> {
if (donationDialog.isShowing()) {
donationDialog.dismiss();
Toast.makeText(getCtx(), "已逾時取消", Toast.LENGTH_SHORT).show();
}
};
handler.postDelayed(timeoutRunnable, 10000); // 20秒
// 4. Checkbox 切換時控制 Spinner
checkboxDefault.setOnCheckedChangeListener((buttonView, isChecked) -> {
spinnerCharity.setEnabled(!isChecked);
handler.removeCallbacks(timeoutRunnable); // 取消 timeout
});
spinnerCharity.setOnTouchListener((v, event) -> {
handler.removeCallbacks(timeoutRunnable); // 取消 timeout
return false;
});
}
private void showInputVehicleDialog() {
closeDialogCountdown.shutdown(); // 停止倒數避免中途跳掉
@ -379,7 +489,7 @@ public class InvoiceBarcodeDialog extends DialogAbstract {
changeDialog(CheckoutNfcPhoneDialog.class);
break;
case 9:
changeDialog(cashDialogClass());
changeDialog(CashPayDialog.class);
break;
case 30:
changeDialog(CheckoutTapPayDialog.class);

View File

@ -1,7 +1,6 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import android.view.View;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
@ -118,7 +117,6 @@ public class InvoiceCustomerIdentifierDialog extends DialogAbstract {
setContentView(binding.getRoot());
binding.buttonFinish.setOnClickListener(v -> changeOk());
binding.buttonNextStep.setOnClickListener(v -> changeNextStep());
binding.buttonDonation.setOnClickListener(v -> changeDonation());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
@ -325,25 +323,6 @@ public class InvoiceCustomerIdentifierDialog extends DialogAbstract {
cancel();
}
/**
* 步驟1輸入統編 步驟2輸入電話
* 先驗證統編長度通過才切換畫面隱藏統編區與下一步顯示電話區與完成輸入
* 原本此按鈕未綁 listener導致統編流程卡死統編永遠送不到後台
*/
public void changeNextStep() {
String number = binding.editTextNumber.getText().toString();
if (number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
return;
}
binding.linearStep1.setVisibility(View.GONE);
binding.linearStep2.setVisibility(View.VISIBLE);
binding.buttonNextStep.setVisibility(View.GONE);
binding.buttonFinish.setVisibility(View.VISIBLE);
binding.textInvoiceCarrier.setText("輸入電話號碼");
closeDialogCountdown.setCountdown(40);
}
public void changeOk() {
//----- 發票統編 ------
String number = binding.editTextNumber.getText().toString();
@ -405,7 +384,7 @@ public class InvoiceCustomerIdentifierDialog extends DialogAbstract {
changeDialog(CheckoutNfcPhoneDialog.class);
break;
case 9:
changeDialog(cashDialogClass());
changeDialog(CashPayDialog.class);
break;
case 30:
changeDialog(CheckoutTapPayDialog.class);

View File

@ -1,334 +0,0 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.DialogDonationBinding;
import com.unibuy.smartdevice.ui.recycler.RecyclerDonationListAdpter;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class InvoiceDonationDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
INVOICE_SUCCESSFUL(4),
INVOICE_FAILED(5),
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
CHANGE_CHECKOUT(8),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
if (MyApp.getInstance().getBuyList().size() <= 0) {
dialogCancel();
} else {
Option option = optionMap.get(commandCode);
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonCancelText(Integer.valueOf(message));
break;
case INVOICE_SUCCESSFUL:
break;
case INVOICE_FAILED:
break;
case CHANGE_DISPATCH:
changeDispatch();
break;
case CHANGE_BUY:
changeBuy();
break;
case CHANGE_CHECKOUT:
changeCheckout();
break;
}
}
}
};
}
private DialogDonationBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private LinkedHashMap<String, String> donationMap;
private List<String> organizationNames;
private RecyclerDonationListAdpter donationAdapter;
public InvoiceDonationDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
}
@Override
protected void onCreate(Context context) {
binding = DialogDonationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
donationMap = new LinkedHashMap<>();
intiDonationMap();
binding.buttonFinish.setOnClickListener(v -> changeOk());
binding.buttonCancel.setOnClickListener(v -> changeBuy());
// 隱藏切換 Tab 按鈕 InvoiceTypeDialog 統一管理入口
binding.buttonCustomerIdentifier.setVisibility(View.GONE);
binding.buttonDonation.setVisibility(View.GONE);
binding.buttonInputVehicle.setVisibility(View.GONE);
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
setupDonationSpinner();
}
private void updateButtonState(com.google.android.material.button.MaterialButton selectedButton) {
binding.buttonInputVehicle.setChecked(selectedButton.getId() == R.id.buttonInputVehicle);
binding.buttonDonation.setChecked(selectedButton.getId() == R.id.buttonDonation);
binding.buttonCustomerIdentifier.setChecked(selectedButton.getId() == R.id.buttonCustomerIdentifier);
}
public void intiDonationMap(){
donationMap.put("社團法人嘉義市腦性麻痺協會", "2855669");
donationMap.put("社團法人桃園市唐氏症家長協會", "520321");
donationMap.put("社團法人中華民國自閉症總會", "1577");
donationMap.put("社團法人臺北市蒲公英聽語協會", "71521");
donationMap.put("社團法人桃園市失智症關懷協會", "8185");
donationMap.put("財團法人台灣痲瘋救濟基金會", "1954");
donationMap.put("社團法人中華小腦萎縮症病友協會", "9972");
donationMap.put("社團法人高雄市唐氏症歡喜協會", "7717299");
donationMap.put("社團法人屏東縣腦性麻痺服務協會", "115");
donationMap.put("社團法人台南市智障者福利家長協進會", "2070");
donationMap.put("財團法人台灣肯納自閉症基金會", "1018");
donationMap.put("社團法人高雄市腦性麻痺服務協會", "29636");
donationMap.put("社團法人高雄市自閉症協進會", "8585885");
donationMap.put("社團法人臺灣自閉兒家庭關懷協會", "338518");
donationMap.put("社團法人臺東縣自閉症協進會", "3685");
donationMap.put("社團法人中華民國外傷性腦損傷照顧者關懷協會", "5841");
donationMap.put("財團法人天主教靈醫會私立聖嘉民啟智中心", "57880");
donationMap.put("社團法人新竹市智障福利協進會", "6684");
donationMap.put("社團法人屏東縣自閉症協進會", "7351024");
donationMap.put("社團法人台北市自閉症家長協會", "772");
donationMap.put("社團法人屏東縣啟智協進會", "159");
donationMap.put("社團法人花蓮縣自閉症協會", "8588");
donationMap.put("澎湖縣私立財團法人天主教澎湖教區附設惠民啟智中心", "9261790");
donationMap.put("財團法人新竹市私立天主教仁愛啟智中心", "227");
donationMap.put("社團法人中華民國唐氏症關愛者協會", "88321");
donationMap.put("財團法人天主教會台中教區附設南投縣私立復活啟智中心", "7717");
donationMap.put("財團法人喜憨兒社會福利基金會", "88432");
donationMap.put("社團法人花蓮縣智障福利協進會", "7756");
donationMap.put("社團法人雲林縣啟智協會", "1311");
donationMap.put("社團法人新竹縣智障福利協進會", "3170");
donationMap.put("社團法人台南市腦性麻痺之友協會", "815");
donationMap.put("社團法人台南市癲癇之友協會", "2350253");
donationMap.put("新北市自閉症服務協進會", "7688");
donationMap.put("社團法人台中市?智協進會", "450");
donationMap.put("社團法人台中市自閉症教育協進會", "1127");
donationMap.put("財團法人中華民國自閉症基金會", "402");
donationMap.put("社團法人台灣流浪貓狗關懷協會", "9958");
donationMap.put("社團法人台灣愛貓協會", "921314");
donationMap.put("社團法人台灣幸褔狗流浪中途協會", "856203");
donationMap.put("社團法人新北市流浪貓狗再生保護協會", "8866");
donationMap.put("社團法人台灣流浪動物希望協會", "119");
donationMap.put("社團法人高雄市幸福狗尾巴友善動物協會", "6848");
donationMap.put("社團法人臺南市貓狗流浪終點協會", "5866");
donationMap.put("社團法人台灣黑糖流浪動物救援協會", "958");
donationMap.put("社團法人台灣動物輔助活動及治療協會", "916");
donationMap.put("社團法人台灣友善動物協會", "9305");
donationMap.put("社團法人中華民國反棄養寵物協會", "11289");
donationMap.put("社團法人台灣流浪貓關懷協會", "71706");
donationMap.put("社團法人台灣浪愛不流浪關懷動物協會", "9458");
donationMap.put("台灣米樂流浪動物中途協會", "9925");
donationMap.put("社團法人中華民國寵物友善協會", "52982");
donationMap.put("社團法人台灣愛兔協會", "5822");
donationMap.put("社團法人台灣拾貓生命教育創新協會", "880222");
donationMap.put("社團法人台灣防止虐待動物協會", "1772");
donationMap.put("社團法人好好善待動物協會", "6666666");
donationMap.put("社團法人臺北市毛小孩幸福聯盟協會", "99891");
donationMap.put("社團法人台南市流浪動物愛護協會", "17966");
donationMap.put("中華民國流浪動物花園協會", "528");
donationMap.put("台灣流浪兔保護協會", "17922");
donationMap.put("社團法人台北市愛兔協會", "5222");
donationMap.put("社團法人臺灣照顧生命協會", "589");
donationMap.put("新竹市保護動物協會", "860713");
donationMap.put("社團法人中華民國動物福利環保協進會", "529529");
donationMap.put("財團法人中華民國兒童癌症基金會", "88888");
donationMap.put("中華民國癌病腫瘤患者扶助協會", "6088");
donationMap.put("財團法人臺灣癌症基金會", "1799");
}
private void setupDonationSpinner() {
organizationNames = new ArrayList<>(donationMap.keySet());
donationAdapter = new RecyclerDonationListAdpter(organizationNames, (name, position) -> {
closeDialogCountdown.setCountdown(40);
});
binding.recyclerViewCharity.setAdapter(donationAdapter);
// 隨機預選一個捐贈機構
int randomIndex = new Random().nextInt(organizationNames.size());
donationAdapter.setSelectedPosition(randomIndex);
binding.recyclerViewCharity.scrollToPosition(randomIndex);
}
public void changeInvoiceBarcode() {
closeDialogCountdown.shutdown();
new InvoiceBarcodeDialog(getCtx(), getSrcHandlerMain()).show();
cancel();
}
public void changeCustomerIdentifier() {
closeDialogCountdown.shutdown();
new InvoiceCustomerIdentifierDialog(getCtx(), getSrcHandlerMain()).show();
cancel();
}
public void changeDispatch() {
closeDialogCountdown.shutdown();
DispatchDialog dispatchDialog = new DispatchDialog(getContext(), getSrcHandlerMain());
dispatchDialog.show();
cancel();
}
public void changeOk() {
int selectedPosition = donationAdapter.getSelectedPosition();
if (selectedPosition < 0) {
Toast.makeText(getCtx(), "請選擇捐贈機構", Toast.LENGTH_SHORT).show();
return;
}
String selectedName = organizationNames.get(selectedPosition);
String loveCode = donationMap.get(selectedName);
MyApp.getInstance().getInvoiceData().setLoveCode(loveCode);
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("loveCode:" + loveCode);
Toast.makeText(getCtx(), "選擇:" + selectedName, Toast.LENGTH_SHORT).show();
changeCheckout();
}
public void changeBuy() {
closeDialogCountdown.shutdown();
new InvoiceTypeDialog(getContext(), getSrcHandlerMain()).show();
cancel();
}
public void changeCheckout() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
switch (flowType) {
case 1:
changeDialog(CheckoutNfcCardDialog.class);
break;
case 2:
changeDialog(CheckoutNfcRechargeDialog.class);
break;
case 3:
changeDialog(CheckoutEsunPayDialog.class);
break;
case 4:
changeDialog(CheckoutNfcPhoneDialog.class);
break;
case 9:
changeDialog(CashPayDialog.class);
break;
case 30:
changeDialog(CheckoutTapPayDialog.class);
break;
case 70:
changeDialog(CheckoutLinePayDialog.class);
break;
}
cancel();
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void cancelOnThreadCountdown() {
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("返回發票選擇(" + countdown + ")");
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
handlerMain.start(getClass().getSimpleName(), CashPayDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
}

View File

@ -1,178 +0,0 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.databinding.DialogInvoiceTypeBinding;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import java.util.HashMap;
import java.util.Map;
/**
* 選擇發票開立方式介面
* 提供三個選項輸入載具 / 捐贈發票 / 公司統編
* 位於 BuyDialog選擇支付方式與各發票設定頁面之間
*/
public class InvoiceTypeDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
if (MyApp.getInstance().getBuyList().size() <= 0) {
dialogCancel();
} else {
Option option = optionMap.get(commandCode);
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonCancelText(Integer.valueOf(message));
break;
}
}
}
};
}
private DialogInvoiceTypeBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
public InvoiceTypeDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
}
@Override
protected void onCreate(Context context) {
binding = DialogInvoiceTypeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
android.view.Window window = getWindow();
if (window != null) {
android.view.WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = android.view.Gravity.CENTER;
window.setAttributes(layoutParams);
}
// 選擇輸入載具手機條碼
binding.cardVehicle.setOnClickListener(v -> {
closeDialogCountdown.shutdown();
changeDialog(InvoiceBarcodeDialog.class);
});
// 選擇捐贈發票
binding.cardDonation.setOnClickListener(v -> {
closeDialogCountdown.shutdown();
changeDialog(InvoiceDonationDialog.class);
});
// 選擇公司統編
binding.cardCustomerIdentifier.setOnClickListener(v -> {
closeDialogCountdown.shutdown();
changeDialog(InvoiceCustomerIdentifierDialog.class);
});
// 返回支付方式
binding.buttonCancel.setOnClickListener(v -> changeBuy());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
}
/**
* 返回 BuyDialog選擇支付方式
*/
public void changeBuy() {
closeDialogCountdown.shutdown();
new BuyDialog(getContext(), getSrcHandlerMain()).show();
cancel();
}
/**
* 倒數逾時時清空購物車並關閉
*/
public void cancelOnThreadCountdown() {
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("返回" + countdown + "");
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
}

View File

@ -1,148 +0,0 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.databinding.DialogPickupSuccessBinding;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import java.util.HashMap;
import java.util.Map;
public class PickupSuccessDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
RESET_COUNTDOWN(3),
;
private int option;
public int getOption() {
return option;
}
Option(int option) {
this.option = option;
}
}
public PickupSuccessDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
getLogs().info("PickupSuccessDialog:" + getClass().getSimpleName());
}
public static final Map<Integer, Option> optionMap = new HashMap<>();
static {
for (Option option : Option.values()) {
optionMap.put(option.getOption(), option);
}
}
@Override
protected Context setCtx() {
return this.getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getContext(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
Option option = optionMap.get(commandCode);
if (option != null) {
switch (option) {
case TOAST:
super.execute(context, commandCode, message);
break;
case COUNTDOWN_CANCEL:
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
setButtonConfirmText(Integer.valueOf(message));
break;
case RESET_COUNTDOWN:
closeDialogCountdown.setCountdown(15);
break;
}
}
}
};
}
private DialogPickupSuccessBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private String laneNumber = "99"; // 預設貨道號碼
public void setLaneNumber(String laneNumber) {
this.laneNumber = laneNumber;
if (binding != null) {
binding.tvLaneNumber.setText(laneNumber);
binding.tvSubtitle.setText("請至" + laneNumber + "號櫃門拿取您的商品");
}
}
@Override
protected void onCreate(Context context) {
binding = DialogPickupSuccessBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 初始化貨道數字與副標文字
setLaneNumber(laneNumber);
binding.btnConfirm.setOnClickListener(v -> dialogCancel());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(15);
}
public void dialogCancel() {
if (closeDialogCountdown != null) {
closeDialogCountdown.shutdown();
}
cancel();
}
public void cancelOnThreadCountdown() {
getTools().dialogClose();
cancel();
}
public void setButtonConfirmText(long countdown) {
binding.btnConfirm.setText("確認已取貨(" + countdown + "秒)");
}
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
super(handlerMain);
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void close(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
}
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
}

View File

@ -1,44 +0,0 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.databinding.DialogSystemInspectionBinding;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
public class SystemInspectionDialog extends DialogAbstract {
public SystemInspectionDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
getLogs().info("SystemInspectionDialog:" + getClass().getSimpleName());
}
@Override
protected Context setCtx() {
return this.getContext();
}
@Override
protected Class<? extends DialogAbstract> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getContext(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
super.execute(context, commandCode, message);
}
};
}
private DialogSystemInspectionBinding binding;
@Override
protected void onCreate(Context context) {
binding = DialogSystemInspectionBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
}
}

View File

@ -76,8 +76,20 @@ public class RecyclerDialogBuyListAdpter extends RecyclerView.Adapter<RecyclerDi
holder.binding.textPosition.setText(String.valueOf(position));
String productID = buyProduct.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
// 多國語的判斷統一由 ProductStructure 處理/日未翻譯自動 fallback 中文
holder.binding.textProductName.setText(buyProduct.getProduct().getLocalizedName(context));
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
//多國語的判斷
// String currentLang = LanguageHelper.getSavedLanguage(context);
// switch (currentLang) {
// case "tw":
// holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
// break;
// case "en":
// holder.binding.textProductName.setText(buyProduct.getProduct().getProductNameEn());
// break;
// case "ja":
// holder.binding.textProductName.setText(buyProduct.getProduct().getProductNameJp());
// break;
// }
} else {
holder.binding.textProductName.setText(context.getString(R.string.click_select_product));//點我選擇商品
}

View File

@ -1,13 +1,12 @@
package com.unibuy.smartdevice.ui.recycler;
import android.content.Context;
import android.graphics.drawable.GradientDrawable;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.unibuy.smartdevice.R;
@ -84,31 +83,29 @@ public class RecyclerDialogDispatchListAdpter extends RecyclerView.Adapter<Recyc
int price = machinePrice > 0? machinePrice: sellingPrice;
holder.binding.textCount.setText(String.format("已出貨數量(%d / %d", count, quantity));
holder.binding.textCount.setText(String.valueOf(count));
holder.binding.textQuantity.setText(String.valueOf(quantity));
logs.info("isShipped:" + dispatchProduct.isShipped());
if (dispatchProduct.getCount() == dispatchProduct.getQuantity()) {
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipped));//已出貨
holder.binding.textDispatchStatus.setTextColor(ContextCompat.getColor(context, R.color.onSuccess));
holder.binding.textDispatchStatus.setBackground(createTagBackground(R.color.success));
holder.binding.textDispatchStatus.setTextColor(Color.RED);
} else if (dispatchProduct.isShipped()) {
try {
if (SlotField.getSlotField(dispatchProduct.getField()).isCanOneShipped()) {
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+""+(count+1)+"件)");//出貨中
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"("+(count+1)+")");//出貨中
} else {
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"(All)");//出貨中
}
} catch (LogsEmptyException e) {
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+""+(count+1)+"件)");//出貨中
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"("+(count+1)+")");//出貨中
}
holder.binding.textDispatchStatus.setTextColor(ContextCompat.getColor(context, R.color.onPrimary));
holder.binding.textDispatchStatus.setBackground(createTagBackground(R.color.primary));
holder.binding.textDispatchStatus.setTextColor(Color.RED);
} else {
holder.binding.textDispatchStatus.setText(context.getString(R.string.waiting));//等待中
holder.binding.textDispatchStatus.setTextColor(ContextCompat.getColor(context, R.color.black));
holder.binding.textDispatchStatus.setBackground(createTagBackground(R.color.gray));
holder.binding.textDispatchStatus.setTextColor(Color.BLACK);
}
}
@ -124,14 +121,6 @@ public class RecyclerDialogDispatchListAdpter extends RecyclerView.Adapter<Recyc
delProductData(position);
}
}
/** 建立圓角標籤背景 */
private GradientDrawable createTagBackground(int colorResId) {
GradientDrawable drawable = new GradientDrawable();
drawable.setColor(ContextCompat.getColor(context, colorResId));
drawable.setCornerRadius(context.getResources().getDisplayMetrics().density * 20);
return drawable;
}
@Override
public int getItemCount() {
return dispatchList.size();

View File

@ -1,84 +0,0 @@
package com.unibuy.smartdevice.ui.recycler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.unibuy.smartdevice.R;
import java.util.List;
public class RecyclerDonationListAdpter extends RecyclerView.Adapter<RecyclerDonationListAdpter.ViewHolder> {
private List<String> organizationNames;
private int selectedPosition = -1;
private OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(String name, int position);
}
public RecyclerDonationListAdpter(List<String> organizationNames, OnItemClickListener listener) {
this.organizationNames = organizationNames;
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.spinner_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String name = organizationNames.get(position);
holder.textView.setText(name);
// 高亮選中項
if (position == selectedPosition) {
holder.textView.setBackgroundResource(R.drawable.bg_selected_item);
holder.textView.setTextColor(holder.itemView.getContext().getResources().getColor(R.color.white));
} else {
holder.textView.setBackgroundResource(android.R.color.transparent);
holder.textView.setTextColor(holder.itemView.getContext().getResources().getColor(R.color.black));
}
holder.itemView.setOnClickListener(v -> {
int previousPosition = selectedPosition;
selectedPosition = position;
notifyItemChanged(previousPosition);
notifyItemChanged(selectedPosition);
if (listener != null) {
listener.onItemClick(name, position);
}
});
}
@Override
public int getItemCount() {
return organizationNames.size();
}
public void setSelectedPosition(int position) {
this.selectedPosition = position;
notifyDataSetChanged();
}
public int getSelectedPosition() {
return selectedPosition;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.spinnerText);
}
}
}

View File

@ -56,7 +56,7 @@ public class RecyclerProductListAdpter extends RecyclerView.Adapter<RecyclerProd
}
holder.binding.textProductId.setText(product.getProductID());
holder.binding.textProductName.setText(product.getLocalizedName(context));
holder.binding.textProductName.setText(product.getProductName());
holder.binding.textPrice.setText(String.valueOf(product.getMachinePrice() > 0? product.getMachinePrice(): product.getSellingPrice()));
}

View File

@ -68,8 +68,8 @@ public class RecyclerProductSearchAdpter extends RecyclerView.Adapter<RecyclerPr
}
holder.binding.textProductId.setText(product.getProductID());
holder.binding.textProductName.setText(product.getLocalizedName(context));
holder.binding.cardProduct.setOnClickListener(new View.OnClickListener() {
holder.binding.textProductName.setText(product.getProductName());
holder.binding.buttonSlotProductSelect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("select product", "點選商品:"+product.getProductName());

View File

@ -58,9 +58,9 @@ public class RecyclerSearchTextAdpter extends RecyclerView.Adapter<RecyclerSearc
@Override
public void onClick(View v) {
Log.i("SearchTextOnClick", "Selected full name: " + oneText);
activityProductSearchBinding.searchProductList.setQuery(oneText, false);
activityProductSearchBinding.searchProductList.clearFocus();
String nowText = activityProductSearchBinding.searchProductList.getQuery().toString();
Log.i("SearchTextOnClick", nowText + oneText);
activityProductSearchBinding.searchProductList.setQuery(nowText+oneText, false);
}
}

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.view.View;
import java.util.Locale;
@ -12,130 +13,9 @@ public class LanguageHelper {
private static final String PREFS_NAME = "app_prefs";
private static final String KEY_LANGUAGE = "language";
// App 介面支援的語系顯示順序對應 res/values-* 資料夾
// zh-TW=values(預設,繁中)zh-CN=values-zh-rCNenjakovithid(=values-in)
public static final String[] SUPPORTED_LANGUAGES = {
"zh-TW", "zh-CN", "en", "ja", "ko", "vi", "th", "id", "fr", "de", "es"
};
// 各語系以該語言自己的文字顯示的名稱給語言選單用
public static String getDisplayName(String code) {
switch (canonicalize(code)) {
case "zh-TW": return "中文(繁體)";
case "zh-CN": return "中文(简体)";
case "en": return "English";
case "ja": return "日本語";
case "ko": return "한국어";
case "vi": return "Tiếng Việt";
case "th": return "ภาษาไทย";
case "id": return "Bahasa Indonesia";
case "fr": return "Français";
case "de": return "Deutsch";
case "es": return "Español";
default: return code;
}
}
// 將各種歷史/縮寫代碼正規化成標準 UI 語系標籤 region
// 舊資料相容twzh-TWjpja單獨 zhzh-TW過去預設為繁中
public static String canonicalize(String code) {
if (code == null || code.trim().isEmpty()) {
return fromLocale(Locale.getDefault());
}
String c = code.trim().toLowerCase(Locale.ROOT).replace('_', '-');
switch (c) {
case "zh-tw":
case "zh-hant":
case "zh-hant-tw":
case "tw":
case "zh": // 過去單獨 zh 一律視為繁中
return "zh-TW";
case "zh-cn":
case "zh-hans":
case "zh-hans-cn":
case "cn":
return "zh-CN";
case "ja":
case "jp":
return "ja";
case "ko":
case "kr":
return "ko";
case "vi":
case "vn":
return "vi";
case "th":
return "th";
case "id":
case "in": // Android/Java 對印尼文的舊代碼
return "id";
case "fr":
return "fr";
case "de":
return "de";
case "es":
return "es";
default:
if (c.startsWith("en")) return "en";
if (c.startsWith("zh")) return "zh-TW";
return "zh-TW";
}
}
private static String fromLocale(Locale locale) {
String lang = locale.getLanguage();
if ("zh".equals(lang)) {
String region = locale.getCountry();
return ("CN".equalsIgnoreCase(region) || "SG".equalsIgnoreCase(region)) ? "zh-CN" : "zh-TW";
}
// in = 印尼文舊代碼統一回 id
if ("in".equals(lang)) return "id";
return canonicalize(lang);
}
// UI 語系標籤建立對應的 Locale處理 region 與印尼文資源代碼
private static Locale buildLocale(String canonical) {
switch (canonical) {
case "zh-TW": return new Locale("zh", "TW");
case "zh-CN": return new Locale("zh", "CN");
case "id": return new Locale("id"); // 系統會對應到 res/values-in
default: return new Locale(canonical);
}
}
// App 標準語系標籤(zh-TW) 後台 locale 代碼(zh_TW)後台一律用底線格式
public static String toBackendLocale(String code) {
return canonicalize(code).replace('-', '_');
}
// 語言選單的語系來源優先用後台 B014 LangSet 下發的機台啟用語系
// 尚未取得離線/未綁定時退回全部 SUPPORTED_LANGUAGES回傳標準標籤
public static String[] getMachineLanguages() {
java.util.List<String> machine = com.unibuy.smartdevice.MyApp.getInstance().getMachineLanguages();
if (machine != null && !machine.isEmpty()) {
return machine.toArray(new String[0]);
}
return SUPPORTED_LANGUAGES;
}
// 供商品名稱等資料層使用後台目前只有 // 三語
// 其餘語言簡中////印尼一律 fallback 回中文主值
public static String toProductLang(String code) {
String c = canonicalize(code);
if (c.startsWith("en")) return "en";
if (c.equals("ja")) return "ja";
return "zh";
}
// 取得目前已儲存且正規化為商品資料用的語言代碼zh/en/ja
public static String normalizedLang(Context context) {
return toProductLang(getSavedLanguage(context));
}
// 切換語言
public static void changeLanguage(Context context, String languageCode) {
String canonical = canonicalize(languageCode);
Locale locale = buildLocale(canonical);
Locale locale = new Locale(languageCode);
Locale.setDefault(locale);
Resources resources = context.getResources();
@ -149,8 +29,8 @@ public class LanguageHelper {
resources.updateConfiguration(config, resources.getDisplayMetrics());
// 保存語言設定一律存正規化後的標準標籤
saveLanguagePreference(context, canonical);
// 保存語言設定
saveLanguagePreference(context, languageCode);
}
// 保存語言到 SharedPreferences
@ -159,11 +39,10 @@ public class LanguageHelper {
prefs.edit().putString(KEY_LANGUAGE, languageCode).apply();
}
// 讀取語言設定回傳正規化後的標準標籤
// 讀取語言設定
public static String getSavedLanguage(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
String saved = prefs.getString(KEY_LANGUAGE, null);
return canonicalize(saved);
return prefs.getString(KEY_LANGUAGE, Locale.getDefault().getLanguage());
}
// 初始化語言 App 啟動時呼叫

View File

@ -1,35 +0,0 @@
package com.unibuy.v2
import com.blankj.utilcode.util.ActivityUtils
import com.blankj.utilcode.util.AppUtils
import com.unibuy.smartdevice.ui.FontendActivity
import com.unibuy.v2.vm.CartViewModel
/**
* v2 購物車的橋接 helper Luzui 移植並裁剪
*
* Luzui 差異
* - getCartViewModel() 改為自持 singleton不綁 FontendActivity ViewModelProvider
* 避免修改 main 共用的 FontendActivity其無 cartViewModel 欄位
* - 移除 dismissAllAlertDialogs()/getSPStrNotNull()前者依賴 DialogAbstract.dialogInstances
* TaiwanStar private static 無法存取後者購物車未使用
*/
object UnibuyHelper {
private val cartViewModel: CartViewModel = CartViewModel()
fun getCartViewModel(): CartViewModel = cartViewModel
fun getFontendActivity(): FontendActivity {
if (ActivityUtils.getTopActivity() is FontendActivity) {
return ActivityUtils.getTopActivity() as FontendActivity
} else if (ActivityUtils.isActivityExists(AppUtils.getAppPackageName(), FontendActivity::class.java.simpleName)) {
ActivityUtils.getActivityList().forEach { activity ->
if (activity is FontendActivity) {
return activity
}
}
}
return FontendActivity()
}
}

View File

@ -1,93 +0,0 @@
package com.unibuy.v2.adapter
import android.graphics.PorterDuff
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.unibuy.smartdevice.R
import com.unibuy.smartdevice.exception.LogsEmptyException
import com.unibuy.smartdevice.structure.SlotStructure
import com.unibuy.smartdevice.ui.tools.ImageGlide
import com.unibuy.v2.UnibuyHelper
/**
* 購物車品項清單 Adapter Luzui v2 移植
* 依賴 BaseRecyclerViewAdapterHelper BaseQuickAdapter
*
* Disabled 按鈕規則
* - buttonMinus cartSelectedCount == 1 disabled最低 1 不允許繼續減少
* - buttonPlus cartSelectedCount >= slot.count庫存上限或全車總件數 >= 8 disabled
* Disabled 外觀背景換 bg_quantity_button_disabled圖示染灰#ADADADclickable = false
*/
class CartOperationAdapter(layoutResId: Int, data: MutableList<SlotStructure>) :
BaseQuickAdapter<SlotStructure, BaseViewHolder>(layoutResId, data) {
init {
addChildClickViewIds(R.id.buttonMinus, R.id.buttonPlus, R.id.buttonWaste, R.id.layout_product, R.id.image_product)
}
override fun convert(holder: BaseViewHolder, item: SlotStructure) {
val result = UnibuyHelper.getCartViewModel().createResult(item)
val ctx = holder.itemView.context
// 多國語商品名稱(統一由 ProductStructure 處理i18n map → zh_TW → 舊 _en/_jp → 中文主值)
val name = item.product.getLocalizedName(ctx)
holder.setText(R.id.textProductName, name)
.setText(R.id.totalPriceTextView, "NT$ ${result.currentItemPrice}")
.setText(R.id.textStock, String.format("庫存%02d件", item.count))
.setText(R.id.textQuantity, "${result.currentItemCount}")
.setText(R.id.textPrice, "NT$ ${item.product.realPrice}")
// ── 商品圖片 ────────────────────────────────────────────────────
val imageProductPicture = holder.getView<ImageView>(R.id.image_product)
try {
ImageGlide(imageProductPicture.context).fileload(item.product.productImg, imageProductPicture)
} catch (e: LogsEmptyException) {
e.printStackTrace()
}
// ── 按鈕 Disabled 狀態判斷 ──────────────────────────────────────
val buttonMinus = holder.getView<ImageView>(R.id.buttonMinus)
val buttonPlus = holder.getView<ImageView>(R.id.buttonPlus)
val isAtMinCount = item.cartSelectedCount <= 1
val isAtStockLimit = item.cartSelectedCount >= item.count
val isCartFull = UnibuyHelper.getCartViewModel().hasMinimumCartItems()
val isPlusDisabled = isAtStockLimit || isCartFull
// ── 減少按鈕 ────────────────────────────────────────────────────
applyButtonState(buttonMinus, disabled = isAtMinCount, ctx)
// ── 增加按鈕 ────────────────────────────────────────────────────
applyButtonState(buttonPlus, disabled = isPlusDisabled, ctx)
}
/**
* 套用啟用 / 停用外觀到 ImageView 按鈕
*
* @param button 目標按鈕
* @param disabled true = 停用light_gray 背景 + 灰色圖示 + 不可點擊
* @param ctx Context取色用
*/
private fun applyButtonState(button: ImageView, disabled: Boolean, ctx: android.content.Context) {
if (disabled) {
button.setBackgroundResource(R.drawable.bg_quantity_button_disabled)
// setBackgroundResource 會重置 stateListAnimator需立刻清除以防止陰影跳動
button.stateListAnimator = null
button.setColorFilter(
ContextCompat.getColor(ctx, R.color.gray),
PorterDuff.Mode.SRC_IN
)
button.isClickable = false
} else {
button.setBackgroundResource(R.drawable.bg_quantity_outline_button)
// setBackgroundResource 會重置 stateListAnimator需立刻清除以防止陰影跳動
button.stateListAnimator = null
button.clearColorFilter()
button.isClickable = true
}
}
}

View File

@ -1,9 +0,0 @@
package com.unibuy.v2.bean
// 購物車操作結果資料類
data class CartOperationResult(
val totalPrice: Int, // 購物車總價
val totalCount: Int, // 購物車總數量
val currentItemCount: Int, // 當前商品數量
val currentItemPrice: Int // 當前商品價格
)

View File

@ -1,254 +0,0 @@
package com.unibuy.v2.ui;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import com.blankj.utilcode.util.ClickUtils;
import com.blankj.utilcode.util.ToastUtils;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.unibuy.smartdevice.FragmentAbstract;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.FragmentShoppingCartBinding;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.ui.FontendActivity;
import com.unibuy.v2.UnibuyHelper;
import com.unibuy.v2.adapter.CartOperationAdapter;
import com.unibuy.v2.bean.CartOperationResult;
import com.unibuy.v2.utils.SafeCountdownUtil;
import java.util.List;
/**
* General flavor 多品項購物車 Fragment Luzui v2 移植並適配
*
* Luzui 差異
* - 移除 HttpAPI / DialogXManager / FlowerFragment / 格子機相關邏輯
* - 加購 VMC 商品列表結帳改為 convertToBuyStructure() + SaleFlowHandler.onBuyRequested()
* 交棒給 TaiwanStar 既有結帳鏈BuyDialog 付款 DispatchDialog MQTT 結案
* - 移除 MyApp.setShoppingCartFragmentInstanceTaiwanStar 無此 API
*/
public class ShoppingCartFragment extends FragmentAbstract {
private FragmentShoppingCartBinding binding;
private CartOperationAdapter cartAdapter;
private CartOperationResult allCartItems;
private SafeCountdownUtil countdownUtil;
private static final int DEBOUNCE_DURATION = 500;
private static final int COUNTDOWN_DURATION = 180;
/** 回傳當前倒數秒數(未初始化回 -1。 */
public int getCurrentCountdownSeconds() {
if (countdownUtil != null) {
return countdownUtil.getCurrentSeconds();
}
return -1;
}
@Override
protected String setName() {
return getString(R.string.shopping_cart_title);
}
@Override
protected Context setCtx() {
return getContext();
}
@Override
protected Class<?> setCls() {
return getClass();
}
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@Override
protected void execute(Context context, int commandCode, String message) {
// 購物車本身不需處理額外指令
}
};
}
public ShoppingCartFragment() {
super(null);
}
public ShoppingCartFragment(HandlerMain srcHandlerMain) {
super(srcHandlerMain);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentShoppingCartBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupUI();
}
private void setupUI() {
setupCartRecyclerView();
setupClickListeners();
refreshTitleView();
initCountdownUtil();
}
private void initCountdownUtil() {
if (countdownUtil == null) {
countdownUtil = new SafeCountdownUtil(getViewLifecycleOwner().getLifecycle());
countdownUtil.setCountdownListener(new SafeCountdownUtil.CountdownListener() {
@Override
public void onTick(int seconds) {
updateCountdownText(seconds);
}
@Override
public void onFinish() {
// isAdded() onSaveInstanceState 後仍為 true無法擋住背景 commit
// 改以 lifecycle 是否達 STARTED 為準最終 commit 防呆在 FontendActivity
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)
&& getActivity() != null) {
getActivity().runOnUiThread(() -> clearCartAndBack());
}
}
});
}
}
private void updateCountdownText(int seconds) {
if (!isAdded() || getActivity() == null) {
return;
}
getActivity().runOnUiThread(() -> {
if (binding != null && binding.cancelText != null) {
binding.cancelText.setText(getString(R.string.cart_cancel_button_format, seconds));
}
});
}
private void handleCartItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
try {
SlotStructure slot = (SlotStructure) adapter.getData().get(position);
int viewId = view.getId();
if (viewId == R.id.buttonMinus) {
UnibuyHelper.INSTANCE.getCartViewModel().removeFromCart(slot);
adapter.notifyItemChanged(position);
countdownReset(); // 使用者調整數量有互動續命 180 秒倒數
} else if (viewId == R.id.buttonPlus) {
UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
adapter.notifyItemChanged(position);
countdownReset(); // 使用者調整數量有互動續命 180 秒倒數
} else if (viewId == R.id.buttonWaste) {
UnibuyHelper.INSTANCE.getCartViewModel().removeItemFromCart(slot);
adapter.remove(position);
// 購物車清空後返回 VMC 商品列表
if (UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot().isEmpty()) {
backToVmc();
} else {
countdownReset(); // 還有商品視為有互動續命 180 秒倒數
}
}
// layout_product / image_product 的商品詳情 popup Luzui DialogXManagerGeneral 不採用
refreshTitleView();
} catch (Exception e) {
ToastUtils.showLong(getString(R.string.data_error));
e.printStackTrace();
}
}
private void setupClickListeners() {
// 加購商品回到 VMC 商品列表繼續挑選
ClickUtils.applySingleDebouncing(binding.addMoreItemsButton, DEBOUNCE_DURATION, v -> backToVmc());
// 前往結帳寫入 buyList 後交棒給既有結帳鏈
ClickUtils.applySingleDebouncing(binding.confirmButton, DEBOUNCE_DURATION, v -> {
if (UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot().isEmpty()) {
ToastUtils.showLong(getString(R.string.cart_cannot_decrease));
return;
}
UnibuyHelper.INSTANCE.getCartViewModel().convertToBuyStructure();
FontendActivity activity = getFontendActivity();
if (activity != null && activity.getSaleFlowHandler() != null) {
activity.getSaleFlowHandler().onBuyRequested();
}
});
// 取消清空購物車並返回 VMC
ClickUtils.applySingleDebouncing(binding.cancelButton, DEBOUNCE_DURATION, v -> clearCartAndBack());
}
private void setupCartRecyclerView() {
List<SlotStructure> cartItems = UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot();
cartAdapter = new CartOperationAdapter(R.layout.recycler_cart_operation, cartItems);
cartAdapter.setOnItemChildClickListener(this::handleCartItemClick);
binding.shoppingCartRecyclerView.setAdapter(cartAdapter);
}
private void refreshTitleView() {
allCartItems = UnibuyHelper.INSTANCE.getCartViewModel().getAllCartItems();
binding.totalItemsTextView.setText(getString(R.string.total_items_format, allCartItems.getTotalCount()));
binding.totalAmountTextView.setText(getString(R.string.total_amount_format, allCartItems.getTotalPrice()));
if (UnibuyHelper.INSTANCE.getCartViewModel().hasMinimumCartItems()) {
binding.maxItemsWarningTextView.setVisibility(View.VISIBLE);
} else {
binding.maxItemsWarningTextView.setVisibility(View.GONE);
}
}
/** 清空購物車並切回 VMC 商品列表。 */
private void clearCartAndBack() {
UnibuyHelper.INSTANCE.getCartViewModel().clearCart();
backToVmc();
}
private void backToVmc() {
FontendActivity activity = getFontendActivity();
if (activity != null) {
activity.backToVmc();
}
}
@Nullable
private FontendActivity getFontendActivity() {
if (getActivity() instanceof FontendActivity) {
return (FontendActivity) getActivity();
}
return null;
}
@Override
public void startOnSwitchFragment() {
setupUI();
countdownUtil.startOrReset(COUNTDOWN_DURATION);
}
@Override
public void stopOnSwitchFragment() {
if (countdownUtil != null) {
countdownUtil.stop();
}
}
public void countdownReset() {
countdownUtil.startOrReset(COUNTDOWN_DURATION);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@ -1,76 +0,0 @@
package com.unibuy.v2.utils
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import java.util.Timer
import java.util.TimerTask
/**
* Fragment 生命週期綁定的安全倒數計時器 Luzui v2 移植
*/
class SafeCountdownUtil(private val lifecycle: Lifecycle) : DefaultLifecycleObserver {
private var timer: Timer? = null
private var currentTime = 0
private var listener: CountdownListener? = null
private var lastCallTime: Long = 0
fun getCurrentSeconds(): Int {
return currentTime
}
init {
lifecycle.addObserver(this)
}
interface CountdownListener {
fun onTick(seconds: Int)
fun onFinish()
}
fun setCountdownListener(listener: CountdownListener) {
this.listener = listener
}
// 安全啟動/重置方法(帶防抖)
fun startOrReset(seconds: Int) {
synchronized(this) {
val now = System.currentTimeMillis()
if (now - lastCallTime < 200) {
return
}
lastCallTime = now
timer?.cancel()
currentTime = seconds
listener?.onTick(currentTime)
timer = Timer().apply {
schedule(object : TimerTask() {
override fun run() {
if (currentTime > 0) {
currentTime--
listener?.onTick(currentTime)
} else {
stop()
listener?.onFinish()
}
}
}, 1000, 1000)
}
}
}
fun stop() {
synchronized(this) {
timer?.cancel()
timer = null
}
}
override fun onDestroy(owner: LifecycleOwner) {
stop()
lifecycle.removeObserver(this)
listener = null
}
}

View File

@ -1,148 +0,0 @@
package com.unibuy.v2.vm
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.blankj.utilcode.util.ToastUtils
import com.unibuy.smartdevice.MyApp
import com.unibuy.smartdevice.R
import com.unibuy.smartdevice.structure.BuyStructure
import com.unibuy.smartdevice.structure.SlotStructure
import com.unibuy.v2.UnibuyHelper
import com.unibuy.v2.bean.CartOperationResult
/**
* 多品項購物車 ViewModel Luzui v2 移植
*
* Luzui 版差異
* - 移除 updateLocalGoods()Luzui SlotField.FLOWER + SlotsDao 在出貨後扣本地庫存
* TaiwanStar VMC 貨道機扣庫存由既有 DispatchDialog / MQTT 結案負責購物車不重複處理
* - 寫死中文改用 R.string.cart_max_items
*/
class CartViewModel : ViewModel() {
private val _cartItems = MutableLiveData<List<SlotStructure>>(emptyList())
val cartItems: LiveData<List<SlotStructure>> = _cartItems
val freeGoods: MutableList<BuyStructure> = mutableListOf()
/** 把購物車內容寫入 MyApp.buyList交棒給既有結帳鏈BuyDialog → 付款 → DispatchDialog → MQTT。 */
fun convertToBuyStructure() {
MyApp.getInstance().apply {
buyList.clear()
// 1. 免費商品來店禮General 不做時恆空)
buyList += freeGoods
// 2. 購物車商品
buyList += UnibuyHelper.getCartViewModel()
.getCartItemsSnapshot()
.map { cartItem ->
BuyStructure(cartItem.field, cartItem.slot, cartItem.product, cartItem.cartSelectedCount)
}
}
}
/** 購物車最多 8 件 */
fun hasMinimumCartItems(): Boolean {
return getAllCartItems().totalCount >= 8
}
fun getCartItemsSnapshot(): List<SlotStructure> = _cartItems.value ?: emptyList()
// 添加商品到購物車(以 field + slot + productID 唯一識別)
fun addToCart(slot: SlotStructure): CartOperationResult {
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
val item = currentItems.find {
it.field == slot.field &&
it.slot == slot.slot &&
it.product?.productID == slot.product?.productID
}
return if (item == null) {
if (hasMinimumCartItems()) {
ToastUtils.showLong(R.string.cart_max_items)
return createResult(null)
}
slot.cartSelectedCount = 1
val newList = currentItems.toMutableList().apply { add(slot) }
_cartItems.value = newList
createResult(slot)
} else if (item.cartSelectedCount >= item.count) {
ToastUtils.showLong(R.string.cart_out_of_stock)
createResult(item)
} else {
if (UnibuyHelper.getCartViewModel().hasMinimumCartItems()) {
ToastUtils.showLong(R.string.cart_max_items)
createResult(item)
} else {
item.cartSelectedCount++
_cartItems.value = currentItems
createResult(item)
}
}
}
// 從購物車減少一件
fun removeFromCart(slot: SlotStructure): CartOperationResult {
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
val item = currentItems.find {
it.field == slot.field &&
it.slot == slot.slot &&
it.product?.productID == slot.product?.productID
} ?: return CartOperationResult(0, 0, 0, 0)
return if (item.cartSelectedCount > 1) {
item.cartSelectedCount--
_cartItems.value = currentItems
createResult(item)
} else {
ToastUtils.showLong(R.string.cart_cannot_decrease)
createResult(item)
}
}
// 清空購物車
fun clearCart(): CartOperationResult {
val result = createResult(null)
_cartItems.value = emptyList()
return result.copy(
totalPrice = 0,
totalCount = 0,
currentItemCount = 0,
currentItemPrice = 0
)
}
fun createResult(item: SlotStructure?): CartOperationResult {
val currentCount = item?.cartSelectedCount ?: 0
val currentPrice = (item?.product?.realPrice ?: 0) * currentCount
val currentItems = _cartItems.value ?: emptyList()
return CartOperationResult(
totalPrice = currentItems.sumOf { it.cartSelectedCount * (it.product?.realPrice ?: 0) },
totalCount = currentItems.sumOf { it.cartSelectedCount },
currentItemCount = currentCount,
currentItemPrice = currentPrice
)
}
// 移除整筆商品
fun removeItemFromCart(slot: SlotStructure): CartOperationResult {
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
val productIdToRemove = slot.product?.productID
if (productIdToRemove != null) {
currentItems.removeAll { item ->
item.product?.productID == productIdToRemove
}
}
_cartItems.value = currentItems
return getAllCartItems()
}
fun getAllCartItems(): CartOperationResult {
val currentItems = _cartItems.value ?: emptyList()
return CartOperationResult(
totalPrice = currentItems.sumOf { it.cartSelectedCount * (it.product?.realPrice ?: 0) },
totalCount = currentItems.sumOf { it.cartSelectedCount },
currentItemCount = 0,
currentItemPrice = 0
)
}
}

Some files were not shown because too many files have changed in this diff Show More