[PROMOTE] 晉升開發分支 dev 至生產分支 main

包含以下主要功能與修復:
1. [FIX] 修正 OTA 連線超時與首頁執行緒併發競態導致 UI 異常問題 (808b9c4)
2. [FEAT] 優化 OTA 靜默安裝邏輯,支援降版本覆蓋安裝 (8768e95)
3. [STYLE] 優化 CMUH 專屬佈局元件比例與樣式排版 (2ea55b9)
4. [FEAT] 新增 CMUH 客製初始畫面跳轉與多處頁面重啟邏輯優化 (7b9b96c)
5. [FIX] 修正中國醫 (CMUH) 領藥異常處理與銷貨流水號關聯 (4c9fb5e)
6. [FIX] 實作半途出貨失敗的 MQTT Finalize 結案回報機制 (b2b6776)
7. [REFACTOR] 優化中國醫(Cmuh)售貨與領藥流程,並重構 API 連線模組 (71a0591)
8. [FEAT] 主控台 UI 全面翻新 (Console UI Overhaul) (984bbe7)
9. [REFACTOR] 重構銷售流程至雙維度 Flavor 架構與版本計算規範 (3409f93)
10. [FEAT] 新增遠端 MQTT 應用程式 OTA 更新功能與雙版號動態計算機制 (e15d020)
This commit is contained in:
sky121113 2026-06-02 10:52:50 +08:00
commit 31a073475e
81 changed files with 7315 additions and 943 deletions

View File

@ -0,0 +1,90 @@
---
trigger: always_on
---
# TaiwanStar 專案版號與發版規範
本規範定義了本專案(販賣機機台 App的版號命名、自動化計算、實體機台 OTA 更新原則,以確保後續開發與發版時版本管理高度穩定。
## 1. 雙版號動態計算機制 (Double Versioning Scheme)
本專案採用三段式版號變數進行自動化計算,所有配置皆於 `app/build.gradle` 頂部定義。
> [!IMPORTANT]
> **開發人員與 AI 助手在修改版號時,嚴禁直接寫死 `versionCode``versionName`,必須透過修改 Gradle 頂部的變數來進行。**
* **`verMajor` (螢幕大小)**:代表實體機台的螢幕吋數(例如 `10` 代表 10 吋螢幕)。
* `verMinor` (次版號):代表新增商業功能、硬體支援,但向下相容(例如 `8`)。
* `verPatch` (修補版號):代表 Bug 修復、小優化(例如 `5`)。
### 計算公式:
* **`versionCode`** = `verMajor * 10000 + verMinor * 100 + verPatch`
* **`versionName`** = `"${verMajor}_0${verMinor}_${verPatch}_R"`
---
## 2. 公司官方命名規範對照
依據公司標準規範,實體機台發佈之 APK 與版本名稱需嚴格符合以下欄位對照:
```
來源 _ 安卓號 _ 主板號 _ 機器系列 _ 專案名稱 _ 螢幕大小 _ 次版號 _ 修補版號 _ 環境
S _ 12 _ XY _ U _ Chengwai _ 10 _ 08 _ 8 _ R
```
### 欄位解析:
* **`S` (來源 / Source)**:代表程式碼與專案來源(**非螢幕尺寸**)。
* **`12` (安卓號)**Android 系統主版本號。
* **`XY` (主板號)**:主板型號規格。
* **`U` (機器系列 / 機型規格)**:例如代表 U 系列機型。
* **`Chengwai` (專案名稱)**:客戶/銷售場景專案代碼(如 `General`, `Chengwai`, `Cmuh`)。
* **`10` (螢幕大小)**:對應 Gradle 中的 `verMajor`
* **`08` (次版號)**:對應 Gradle 中的 `verMinor`(格式化為雙位數)。
* **`8` (修補版號)**:對應 Gradle 中的 `verPatch`
* **`R` (環境)**:環境代碼,例如 `R` 代表 Release 生產環境。
---
## 3. 實體機台 OTA 自動更新鐵律
因為本 App 部署於實體販賣機,更新主要透過後台進行遠端自動下載覆蓋安裝 (OTA)。
> [!CAUTION]
> **每次釋出任何新版 APK 至生產環境,`versionCode` 必須嚴格大於前一次發佈的數值!**
> * 若 `versionCode` 相同,即便 `versionName` 不同(例如從 `10_08_2_R` 改為 `10_08_5_R`),機台下載後也會因 `INSTALL_FAILED_ALREADY_EXISTS` 錯誤而**拒絕覆蓋安裝**,導致遠端自動升級完全失效。
---
## 4. 雙維度產品風味 (Product Flavors) 與 APK 命名規範
本專案採用**雙維度 Flavor** 架構,以完美分離「客戶銷售場景」與「底層硬體規格」,方便後續快速擴充。
### 4.1 雙維度定義 (flavorDimensions)
本專案於 `app/build.gradle` 中定義了兩個 Flavor 維度:
1. **`project` 維度 (客戶/銷售場景區隔)**
* **`General`** (通用主幹):預設顯示付款方式選單(彈出 `BuyDialog`)。未來新客戶最常用此通用主幹。
* **`Chengwai`** (晟崴客製):直接跳出員工卡刷卡畫面(彈出 `MemberCardPickupDialog`),不顯示付款選單。
* **`Cmuh`** (中國醫客製):支援 QR Code 掃碼、處方籤醫令離線 AES 解密與解析領藥流程。
2. **`hardware` 維度 (硬體與系統規格區隔)**
* **`S12XYU`**:代表 `S` (來源) + `12` (安卓號) + `XY` (主板號) + `U` (機器系列/機型規格)。
### 4.2 實體機台 APK 命名鐵律
為了符合實體機台 OTA 平台發佈規範,打包輸出的 APK 檔名必須嚴格連動雙維度,並遵循以下格式:
`app-[格式化硬體規格]_[專案名稱]-[版本號]-[BuildType].apk`
* **格式化規則**`S12XYU` 會在 Gradle 打包時被自動展開格式化為 `S_12_XY_U`
* **輸出檔名範例**
* `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` 自動連動生成,**嚴禁手動修改輸出檔名**。
---
## 5. Git 發版配合流程
1. 自 `develop` 拉出 `release/10.8.5` 發版分支。
2. 遞增 `app/build.gradle` 中的 `verPatch` 變數並 Commit。
3. 測試無誤後,合併回 `main``develop`
4. **必須在 `main` 合併節點打上 Git Tag**,格式為 `v[Major].[Minor].[Patch]`(例如 `v10.8.5`),以便日後追溯特定現場機台的版本程式碼。

View File

@ -6,8 +6,20 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk`AI 會自動為您執行以下步驟:
> [!TIP]
> **支援智慧硬體判斷與參數化覆寫:**
> * 預設(不帶參數):編譯 **`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 升級規範:
1. AI 助手將檢查 `git status --porcelain app/` 的輸出,以偵測是否有未提交的程式碼修改。
2. 若偵測到變更且本次任務尚未增加版號AI 助手將自動讀取 `app/build.gradle` 並將其中的 `verPatch` 變數數值遞增 1。
3. 遞增完成後,向使用者呈報最新的版本號資訊,接著繼續執行步驟一。
## 步驟一:設定環境並編譯 Debug APK
使用 Java 17 與 Gradle 編譯專屬 Flavor (`S_12_XY_U_Standard_Debug`) 的程式碼。
使用 Java 17 與 Gradle 編譯對應 Flavor 與硬體規格的 Debug 程式碼。
```bash
// turbo
@ -17,9 +29,50 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
# 取得目標 Flavor 參數
RAW_INPUT="$1"
TARGET_FLAVOR="Chengwai"
if [ -n "$RAW_INPUT" ]; then
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
# 安全防護:檢查是否為支援的客製項目
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 缺失等錯誤
# --no-daemon強制使用單次 JVM 進程,避免 Daemon 初始化期間被殘留訊號中斷
./gradlew clean :app:assembleS_12_XY_U_Standard_Debug --no-daemon
./gradlew clean :app:assemble${TARGET_FLAVOR}${TARGET_HARDWARE}Debug --no-daemon
```
## 步驟二:導出 APK 到 outputs 資料夾
@ -29,11 +82,87 @@ cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
// turbo
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
mkdir -p outputs
APK_SOURCE="app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk"
APK_DEST="outputs/TaiwanStar_debug.apk"
cp $APK_SOURCE $APK_DEST
echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST"
# 取得 Flavor 參數
RAW_INPUT="$1"
TARGET_FLAVOR="Chengwai"
if [ -n "$RAW_INPUT" ]; then
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
# 動態尋找編譯產出的最新 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 "* 產品風味 (Flavor)$FLAVOR"
echo "* 版本名稱 (versionName)$VERSION_NAME"
echo "* 版本代碼 (versionCode)$VERSION_CODE"
echo "* APK 檔案名稱:$APK_NAME"
echo "* 更新說明:[請在此處附上口語化的一句話更新說明]"
echo "=================================================="
else
echo "❌ 找不到編譯產出的 APK 檔案,複製失敗!"
exit 1
fi
```
## 步驟三:安裝並啟動 App (如有連接設備)
@ -43,14 +172,62 @@ echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST"
// turbo
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
# 取得 Flavor 參數
RAW_INPUT="$1"
TARGET_FLAVOR="Chengwai"
if [ -n "$RAW_INPUT" ]; then
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 個設備,開始安裝..."
$ADB_PATH install -r -d -t "/home/mama/projects/TaiwanStar_general_size_s_Chengwai/outputs/TaiwanStar_debug.apk"
echo "🚀 安裝完成,正在啟動 App..."
$ADB_PATH shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity
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..."
$ADB_PATH shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity
else
echo "❌ 找不到 outputs 目錄下的 APK 檔案,安裝失敗!"
exit 1
fi
else
echo "⚠️ 未偵測到連接的手機或設備,已保留 APK 檔案,跳過安裝步驟。"
fi

View File

@ -0,0 +1,135 @@
---
description: 自動編譯正式發行 Release APK、升級 Patch 版本號、並產出公司 OTA 平台必備之 Keyin 參考資料
---
# 生產環境發版打包工作流 (release-apk)
這個 Workflow 專門為應用程式「生產環境上線/發版」設計。當您準備好將特定客戶的最新功能派送至實體販賣機時,呼叫 `/release-apk` 將自動為您完成嚴格的發行檢查、版號遞增、Release 簽章打包與發版資訊生成。
> [!TIP]
> **支援參數化指定編譯變體:**
> * 預設(不帶參數):打包 **`Chengwai` (晟崴客製正式版)**。
> * 您可以呼叫 `/release-apk General`(或 `general`)打包 **`General` (通用正式版)**。
> * 您可以呼叫 `/release-apk Cmuh`(或 `cmuh`)打包 **`Cmuh` (中國醫客製正式版)**。
---
## 步驟零:發行檢查與版號自動遞增 (OTA 鐵律)
為了確保實體機台能夠順利觸發 OTA 自動下載覆蓋安裝,新編譯的 `versionCode` 必須嚴格大於現場的舊版號:
1. AI 助手將讀取 `app/build.gradle` 並自動將 `verPatch`(修補版號)**遞增 1**(例如從 `8` 自動升級為 `9`),確保 versionCode 順利變大。
2. 自動在 terminal 呈報即將打包的最新版本號資訊。
## 步驟一:清理快取並進行正式簽章編譯
採用單次 JVM 進程強制重新編譯,以確保代碼 100% 乾淨,並執行 Release 模式下的**代碼優化、混淆、與嚴格的 `lintVital` 審查**。
```bash
// turbo
# 終止殘留的 Gradle Daemon 程序,避免多個 Daemon 互搶資源,同時避免誤殺 VS Code Gradle 擴充功能
./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
# 取得目標 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
# 安全防護:檢查是否為支援的客製項目
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_FLAVOR}${HW_FLAVOR}Release --no-daemon
```
## 步驟二:導出正式 APK 並產出後台 Keyin 派送參考資料
將封裝完畢、具備正式簽章的 APK 導出至最外層 `outputs/` 資料夾,並自動計算產出公司 OTA 派送後台所需的全部對照資料。
```bash
// turbo
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
mkdir -p outputs
# 取得 Flavor 參數
TARGET_FLAVOR="Chengwai"
if [ -n "$1" ]; then
TARGET_FLAVOR=$(echo "$1" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
HW_FLAVOR="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
HW_FLAVOR="S9XYU"
fi
# 動態尋找編譯產出的最新正式版 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")
APK_DEST="outputs/$APK_NAME"
cp "$APK_SOURCE" "$APK_DEST"
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. 專案/客戶風味 (Flavor) : $FLAVOR"
echo "2. 版本代碼 (versionCode) : $VERSION_CODE"
echo "3. 版本名稱 (versionName) : $VERSION_NAME"
echo "4. 螢幕吋數 (Screen Size) : ${MAJOR} 吋"
echo "5. 正式版 APK 檔名 : $APK_NAME"
echo "--------------------------------------------------"
echo "📢 [溫馨提醒] 請將此 APK 上傳至 OTA 平台,並設定為對應機型的強迫升級。"
echo "=================================================="
else
echo "❌ 找不到編譯產出的正式版 APK 檔案,封裝失敗!"
exit 1
fi
```
## 步驟三:自動化 Git 發版標記引導
為了落實公司發版規範第 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="v${MAJOR}.${MINOR}.${PATCH}"
echo "=================================================="
echo "🔔 Git 發版追溯標記指南"
echo "--------------------------------------------------"
echo "請在專案合併回 main 分支後,執行以下指令打上發版 Tag"
echo "git tag -a $TAG_NAME -m \"TaiwanStar 發版 - 版本 $TAG_NAME\""
echo "git push origin $TAG_NAME"
echo "=================================================="
```

3
.gitignore vendored
View File

@ -9,3 +9,6 @@
.cxx
local.properties
/outputs
/.antigravitycli/
*.db
mydata.db

View File

@ -1,3 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
"java.configuration.updateBuildConfiguration": "automatic"
}

31
AGENTS.md Normal file
View File

@ -0,0 +1,31 @@
# TaiwanStar Agent Rules Index
本文件為 Agent 規則入口與導覽摘要,不承載完整規範細節。
## Rule Sources (Single Source of Truth)
以下檔案為唯一真實來源Single Source of Truth
- [.agents/rules/star-cloud-api.md](.agents/rules/star-cloud-api.md)
- [.agents/rules/versioning.md](.agents/rules/versioning.md)
任何規則更新,請只修改上述原始檔案;`AGENTS.md` 僅同步摘要與導覽。
## Critical Rules Snapshot
### Star-Cloud API
- 除 `B014`(綁定)外,請求需帶 `Authorization: Bearer [MachineToken]`
- 涉及 Token`ApiKey`)變更時,必須同步持久化到資料庫。
- 網路錯誤不可吞例外,需回傳可見錯誤到 UI如 Toast/Handler
### Versioning
- 禁止直接手改成品 `versionCode`/`versionName` 值。
- 版號管理透過 `verMajor`、`verMinor`、`verPatch` 變數。
- OTA 發版時,`versionCode` 必須嚴格遞增。
## When Rules Conflict
- 規則細節若有衝突,以 `.agents/rules` 下原始規則檔為準。
- `AGENTS.md` 僅作導覽與快速摘要,不作為完整規範來源。

View File

@ -2,6 +2,16 @@ plugins {
alias(libs.plugins.android.application)
}
// =========================================================================
//
// verPatch ()
// (verMajor) Flavor (S12XYU / S9XYU)
//
// =========================================================================
def verMinor = 1
def verPatch = 56
android {
namespace 'com.unibuy.smartdevice'
compileSdk 35
@ -10,20 +20,38 @@ android {
applicationId "com.unibuy.smartdevice"
minSdk 21
targetSdk 34
versionCode 12
versionName "1_1_${versionCode}"
// 10 Sync applicationVariants
versionCode 10 * 10000 + verMinor * 100 + verPatch
versionName "10_0${verMinor}_${verPatch}_R"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
//
flavorDimensions "app_type"
// =====================================================================
// Flavorproject× hardware
// =====================================================================
flavorDimensions "project", "hardware"
productFlavors {
S_12_XY_U_Standard_ {
dimension "app_type"
versionCode 8
versionName "10_0${versionCode}_2_R"
General {
dimension "project"
}
Chengwai {
dimension "project"
}
Cmuh {
dimension "project"
}
S12XYU {
dimension "hardware"
}
S9XYU {
dimension "hardware"
minSdk 28
}
}
@ -35,19 +63,48 @@ android {
}
}
//
//
// _安卓號_主板號_機器系列_專案名稱_螢幕大小_次版號_修補版號_環境
applicationVariants.all { variant ->
variant.outputs.each { output ->
def versionCode = variant.versionCode
def versionName = variant.versionName
def flavor = variant.productFlavors[0].name
def buildType = variant.buildType.name
def project = variant.productFlavors[0].name // project dimension
def hardware = variant.productFlavors[1].name // hardware dimension
def buildType = variant.buildType.name
// app-<flavor>-<versionName>-<buildType>.apk
output.outputFileName = "app-${flavor}-${versionName}-${buildType}.apk"
// (verMajor)
def verMajor = 10
def calculatedMinor = verMinor //
def calculatedPatch = verPatch //
if (hardware == "S9XYU") {
verMajor = 21 // 21.5
// 9
// calculatedMinor = 1
// calculatedPatch = 6
} else if (hardware == "S12XYU") {
verMajor = 10 // 10
}
// Variant
def calculatedVersionCode = verMajor * 10000 + calculatedMinor * 100 + calculatedPatch
def calculatedVersionName = "${verMajor}_0${calculatedMinor}_${calculatedPatch}_R"
variant.outputs.each { output ->
output.versionCodeOverride = calculatedVersionCode
output.versionNameOverride = calculatedVersionName
// hardware flavor S12XYU S_12_XY_U
def hwFormatted = hardware
.replaceAll(/^([A-Z])(\d+)([A-Z]+)([A-Z])$/, '$1_$2_$3_$4')
// app-S_12_XY_U_Chengwai-10_08_8_R-release.apk
output.outputFileName = "app-${hwFormatted}_${project}-${calculatedVersionName}-${buildType}.apk"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
@ -55,6 +112,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
packaging {

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

@ -0,0 +1,79 @@
package com.unibuy.smartdevice.structure;
public class MedicineStructure {
String MaterialCode;
int MedicineCount;
String MedicineName;
String MedicineSlot;
String MedicineImg;
String productId;
String MemoSnDrgno;
// 建構子
public MedicineStructure(String MaterialCode, int MedicineCount, String MedicineName, String MedicineSlot, String MedicineImg, String productId, String MemoSnDrgno) {
this.MaterialCode = MaterialCode;
this.MedicineCount = MedicineCount;
this.MedicineName = MedicineName;
this.MedicineSlot = MedicineSlot;
this.MedicineImg = MedicineImg;
this.productId = productId;
this.MemoSnDrgno = MemoSnDrgno;
}
public String getMaterialCode() {
return MaterialCode;
}
public void setMaterialCode(String MaterialCode) {
this.MaterialCode = MaterialCode;
}
public int getMedicineCount() {
return MedicineCount;
}
public void setMedicineCount(int MedicineCount) {
this.MedicineCount = MedicineCount;
}
public String getMedicineName() {
return MedicineName;
}
public void setMedicineName(String MedicineName) {
this.MedicineName = MedicineName;
}
public String getMedicineSlot() {
return MedicineSlot;
}
public void setMedicineSlot(String MedicineSlot) {
this.MedicineSlot = MedicineSlot;
}
public String getMedicineImg() {
return MedicineImg;
}
public void setMedicineImg(String MedicineImg) {
this.MedicineImg = MedicineImg;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getMemoSnDrgno() {
return MemoSnDrgno;
}
public void setMemoSnDrgno(String MemoSnDrgno) {
this.MemoSnDrgno = MemoSnDrgno;
}
}

View File

@ -0,0 +1,444 @@
package com.unibuy.smartdevice.ui;
import android.app.Activity;
import android.content.Context;
import android.util.Base64;
import android.view.View;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.devices.DeviceType;
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.LogsUnsupportedOperationException;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.MedicineStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainSchedulerOnClick;
import com.unibuy.smartdevice.ui.dialog.FontendPickupCodeDialog;
import com.unibuy.smartdevice.ui.dialog.PickupErrorTipDialog;
import com.unibuy.smartdevice.ui.dialog.TakeMedicineDialog;
import com.unibuy.smartdevice.ui.devs.vmc.VmcFragment;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* Cmuh (中國醫版) 銷售流程處理器
* 負責
* 1. 綁定40秒內刷條碼按鈕 (scanbarcodebtn)
* 2. 連接 USB 掃描器背景讀取
* 3. 解密與解析醫令 (AES 128 ECB)
* 4. 庫存核對與自動加入購物車
* 5. 顯示領藥與錯誤 Dialog
*/
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 SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
this.context = context;
this.handlerMain = handlerMain;
this.callback = callback;
}
/**
* Activity onResume() 中會被呼叫此時尋找並綁定 VmcFragment 中的掃碼按鈕
*/
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("✅ 成功在 Cmuh SaleFlowHandler 綁定掃碼按鈕!");
} else {
MyApp.getInstance().getLogs().info("❌ 在 Cmuh SaleFlowHandler 找不到掃碼按鈕 R.id.scanbarcodebtn");
}
}
}
public void onBuyRequested() {
// 中國醫版沒有購物車付款此方法不執行
}
public void onBarcodeReceived(String rawData) {
// 若是經由其他方式傳入的 Barcode 可在此處直接處理
try {
barcodeAESDecryption(rawData);
} catch (Exception e) {
if (callback != null) {
callback.onSaleFlowError("掃碼處理失敗:" + e.getMessage());
}
}
}
/**
* CMUH 點選取貨按鈕開啟輸入取貨碼的 Dialog
*/
public void onPickupRequested() {
new FontendPickupCodeDialog(context, handlerMain).show();
}
// 內部掃碼 Button Click 監聽器與背景 Task
private class BtnQrCodeOnClick extends HandlerMainSchedulerOnClick {
private final Context context;
public BtnQrCodeOnClick(HandlerMain srcHandlerMain, Context context) {
super(srcHandlerMain);
this.context = context;
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
@Override
protected void execute(HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), "@40秒內刷條碼");
openBarcode = true;
haveValuestate = false;
try {
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
if (size > 5) {
if (!openBarcode) continue;
openBarcode = false; // 立即標記防止重複進入解碼邏輯
MyApp.getInstance().getLogs().info("✅ 觸發掃碼源頭防呆,此筆掃碼僅處理一次");
String barCodeString = new String(buffer, 0, size).trim();
if (barCodeString.isEmpty()) {
openBarcode = true;
continue;
}
MyApp.getInstance().getLogs().info("掃描成功: [" + barCodeString + "]");
try {
barcodeAESDecryption(barCodeString);
} catch (LogsParseException e) {
MyApp.getInstance().getLogs().warning(e);
handlerMain.send(getClass().getSimpleName(), VmcFragment.Option.DECRYPTION_ERROR.getOption(), e.getLocalizedMessage());
openBarcode = true;
return;
}
if (haveValuestate) {
((Activity) context).runOnUiThread(() -> new TakeMedicineDialog(context, handlerMain).show());
} else {
((Activity) context).runOnUiThread(() -> new PickupErrorTipDialog(context, handlerMain).show());
openBarcode = true;
}
break; // 處理完成跳出掃描迴圈
}
} else {
break;
}
}
usbDev.closePort();
} catch (LogsUnsupportedOperationException | LogsIOException | LogsEmptyException e) {
MyApp.getInstance().getLogs().warning(e);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@Override
public void onClick(View v) {
if (System.currentTimeMillis() - lastClickTime < 3000) {
MyApp.getInstance().getLogs().info("⚠️ 點擊太快,觸發防呆");
return;
}
lastClickTime = System.currentTimeMillis();
setRun(true);
start();
}
@Override
public boolean onLongClick(View v) {
return false;
}
@Override
protected Class<?> setCls() {
return getClass();
}
}
// 醫令 AES 解密與解析庫存邏輯
private boolean barcodeAESDecryption(String scanBarcode) throws JSONException, LogsParseException {
try {
String keyString = "cmuhch@mis@drug1"; // 16字節的 key
String encryptedBase64 = scanBarcode;
if (encryptedBase64 == null || encryptedBase64.isEmpty()) {
throw new IllegalArgumentException("錯誤:加密數據為 null 或空,無法解密");
}
byte[] encryptedBytes = Base64.decode(encryptedBase64, Base64.DEFAULT);
if (encryptedBytes == null || encryptedBytes.length == 0) {
throw new IllegalArgumentException("錯誤Base64 解碼後的數據為 null 或空");
}
if (encryptedBytes.length % 16 != 0) {
throw new IllegalArgumentException("錯誤:加密數據長度不是 16 的倍數,可能需要填充");
}
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); // 無填充模式
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
MyApp.getInstance().setDecryptedData("");
MyApp.getInstance().setDecryptedData(new String(decryptedBytes, StandardCharsets.UTF_8).trim());
MyApp.getInstance().setCmuhPickupErrorMessage("");
String decryptedStr = MyApp.getInstance().getDecryptedData();
MyApp.getInstance().getLogs().info("decryptedStr=" + decryptedStr);
JSONObject jsonObject = new JSONObject(decryptedStr);
if (checkORDCDispatchNumber(decryptedStr)) {
return false;
}
addMedicineToByulist(jsonObject);
} catch (Exception e) {
throw new LogsParseException(MyApp.getInstance().getLogs(), "錯誤:加密數據長度不是 16 的倍數,可能需要填充");
}
return haveValuestate;
}
public void addMedicineToByulist(JSONObject jsonObject) {
try {
JSONArray itemsArray = jsonObject.getJSONArray("ITEM");
// 清空 BuyList確保每次掃描都是新的出貨清單
MyApp.getInstance().getBuyList().clear();
int MedicineCount = 0; // 同一種商品全部貨道的總數量
String MedicineName = "", MedicineSlot = "", MedicineImg = "", MedicinePId = "", MemoSnDrgno = "";
ArrayList<MedicineStructure> medicineList = new ArrayList<>();
Map<String, List<SlotStructure>> materialSlotMap = buildMaterialSlotMap();
for (int i = 0; i < itemsArray.length(); i++) {
JSONObject itemObject = itemsArray.getJSONObject(i);
String ordc = itemObject.optString("ORDC", "").trim();
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
if (ordc.isEmpty()) {
failWithCode(ORDC_EMPTY, context.getString(R.string.cmuh_ordc_empty));
return;
}
List<SlotStructure> matchedSlots = materialSlotMap.get(ordc);
if (matchedSlots == null || matchedSlots.isEmpty()) {
failWithCode(MATERIAL_NOT_FOUND, context.getString(R.string.cmuh_material_not_found, ordc));
return;
}
int totalStock = 0;
for (SlotStructure slotData : matchedSlots) {
if (isAvailableCmuhStock(slotData)) {
totalStock += slotData.getCount();
}
}
if (totalStock <= 0) {
failWithCode(STOCK_EMPTY, context.getString(R.string.cmuh_stock_empty, ordc));
return;
}
if (requestQty <= 0 || totalStock < requestQty) {
failWithCode(STOCK_NOT_ENOUGH, context.getString(R.string.cmuh_stock_not_enough, ordc, requestQty, totalStock));
return;
}
}
MyApp.getInstance().getLogs().info("itemsArray.length" + itemsArray.length());
for (int i = 0; i < itemsArray.length(); i++) {
JSONObject itemObject = itemsArray.getJSONObject(i);
String ordc = itemObject.optString("ORDC", "").trim();
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
// 重新初始化這些變數
MedicineName = "";
MedicineCount = 0;
MedicineSlot = "";
MedicineImg = "";
MedicinePId = "";
MemoSnDrgno = "";
List<SlotStructure> matchedSlots = materialSlotMap.get(ordc);
for (SlotStructure matchedSlot : matchedSlots) {
if (!isAvailableCmuhStock(matchedSlot)) continue;
MedicineCount += matchedSlot.getCount();
MedicineSlot += matchedSlot.getSlot() + "-" + matchedSlot.getCount() + ",";
if (MedicineCount >= requestQty) {
MedicinePId = matchedSlot.getProduct().getProductID();
MedicineName = matchedSlot.getProduct().getProductName();
MedicineImg = matchedSlot.getProduct().getProductImg();
MemoSnDrgno = "{\"SN\": \"" + jsonObject.optString("SN") + "\", \"DRGNO\": \"" + jsonObject.optString("DRGNO") + "\"}";
}
}
medicineList.add(new MedicineStructure(ordc, MedicineCount, MedicineName, MedicineSlot, MedicineImg, MedicinePId, MemoSnDrgno));
}
/* 這裡將商品依據領藥數量加入 BuyList */
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) {
int checkcount = 0;
int slotnum = 0;
for (int j = 0; j < requestQty; j++) {
checkcount += 1;
if (checkcount > Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[0].split("-")[1])) {
slotnum = Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[1].split("-")[0]);
} else {
slotnum = Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[0].split("-")[0]);
}
addMedicineToBuyList(
medicineList.get(i).getProductId(),
medicineList.get(i).getMedicineName(),
slotnum,
medicineList.get(i).getMedicineImg(),
medicineList.get(i).getMemoSnDrgno(),
1
);
}
} else {
addMedicineToBuyList(
medicineList.get(i).getProductId(),
medicineList.get(i).getMedicineName(),
Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[0].split("-")[0]),
medicineList.get(i).getMedicineImg(),
medicineList.get(i).getMemoSnDrgno(),
1
);
}
} else {
if (medicineList.get(i).getMedicineCount() < 1) {
handlerMain.start("這裡判斷貨道數量不足提示警語", "@領藥單有貨道數量問題,請洽藥局發藥窗口!");
MyApp.getInstance().getBuyList().clear();
}
}
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private Map<String, List<SlotStructure>> buildMaterialSlotMap() {
Map<String, List<SlotStructure>> materialSlotMap = new HashMap<>();
List<SlotStructure> slotList = MyApp.getInstance().getSlotList(0);
for (SlotStructure slotData : slotList) {
if (slotData == null || slotData.getProduct() == null) continue;
String productId = slotData.getProduct().getProductID();
String materialCode = slotData.getProduct().getMaterialCode();
if (productId == null || productId.isEmpty() || "?".equals(productId)) continue;
if (materialCode == null || materialCode.trim().isEmpty()) continue;
if (slotData.getSlot() <= 0) {
MyApp.getInstance().getLogs().info("Skip invalid VMC slot for CMUH pickup: " + slotData.getSlot());
continue;
}
materialSlotMap.computeIfAbsent(materialCode.trim(), k -> new ArrayList<>()).add(slotData);
}
return materialSlotMap;
}
private boolean isAvailableCmuhStock(SlotStructure slotData) {
return slotData != null && slotData.getCount() > 0 && slotData.getCount() <= 20;
}
private int safeParseInt(String value, int defaultValue) {
try {
return Integer.parseInt(value);
} catch (Exception ignored) {
return defaultValue;
}
}
private void failWithCode(String code, String message) {
MyApp.getInstance().getLogs().info("[" + code + "] " + message);
MyApp.getInstance().setCmuhPickupErrorMessage(message);
haveValuestate = false;
openBarcode = false;
MyApp.getInstance().getBuyList().clear();
}
public void addMedicineToBuyList(String MedicinePidStr, String MedicineNameStr, int SlotNumber, String MedicineImg, String MemoSnDrgno, int count) {
if (SlotNumber <= 0) {
failWithCode(MATERIAL_NOT_FOUND, context.getString(R.string.cmuh_material_not_found, MedicinePidStr));
MyApp.getInstance().getLogs().info("CMUH pickup blocked invalid slot: slot=" + SlotNumber + ", pid=" + MedicinePidStr + ", name=" + MedicineNameStr);
return;
}
ProductStructure productStructure = new ProductStructure(MedicinePidStr, MedicineImg, MedicineNameStr, 0, 0, 0, 0, MemoSnDrgno, "", "", "");
MyApp.getInstance().getLogs().info("CMUH pickup add buy item: slot=" + SlotNumber + ", pid=" + MedicinePidStr + ", name=" + MedicineNameStr);
BuyStructure buyProduct = new BuyStructure(0, SlotNumber, productStructure, count);
MyApp.getInstance().getBuyList().add(buyProduct);
}
public boolean checkORDCDispatchNumber(String decryptedStr) throws JSONException {
JSONObject jsonObject = new JSONObject(decryptedStr);
JSONObject newItem = new JSONObject();
newItem.put("SN", jsonObject.optString("SN"));
newItem.put("DRGNO", jsonObject.optString("DRGNO"));
String jsonStr = MyApp.getInstance().getMemoMap().get("CMUH").get(0).getData().toString();
JSONObject jsonObject2 = new JSONObject(jsonStr);
JSONArray itemArray = jsonObject2.getJSONArray("ITEM");
boolean exists = false;
for (int i = 0; i < itemArray.length(); i++) {
JSONObject item = itemArray.getJSONObject(i);
if (item.getString("SN").equals(newItem.getString("SN")) && item.getString("DRGNO").equals(newItem.getString("DRGNO"))) {
exists = true;
break;
}
}
if (exists) {
MyApp.getInstance().getLogs().info("⚠️ 此項目已存在,不新增!");
openBarcode = false;
haveValuestate = false;
MyApp.getInstance().setCmuhPickupErrorMessage("此單已取物完成,請洽藥局發藥窗口!");
MyApp.getInstance().getBuyList().clear();
return true;
}
return false;
}
}

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,181 @@
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.DialogPickupErrorTipBinding;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class PickupErrorTipDialog 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 PickupErrorTipDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
getLogs().info("PickupErrorTipDialog:" + 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) {
// 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:
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 DialogPickupErrorTipBinding binding;
//private RecyclerDialogTakeMedicineListAdpter recyclerDialogTakeMedicineListAdpter;
private CloseDialogOnThreadHandler closeDialogCountdown;
@Override
protected void onCreate(Context context) {
binding = DialogPickupErrorTipBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String decryptedStr = MyApp.getInstance().getDecryptedData();
getLogs().info(decryptedStr);
try {
JSONObject jsonObject = new JSONObject(decryptedStr);
binding.tvDrgnoid.setText("領藥號:"+jsonObject.optString("DRGNO"));
} catch (JSONException e) {
getLogs().warning(e);
}
String errorMessage = MyApp.getInstance().getCmuhPickupErrorMessage();
if (errorMessage == null || errorMessage.trim().isEmpty()) {
errorMessage = getCtx().getString(R.string.cmuh_pickup_failed_contact_counter);
}
binding.tvErrorMessage.setText(errorMessage);
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
binding.btnNonPerson.setOnClickListener(v -> dialogCancel());
}
public void addMedicineToBuyList(String MedicinePidStr,String MedicineNameStr,int SlotNumber,String MedicineImg){
ProductStructure productStructure = new ProductStructure(MedicinePidStr,MedicineImg,MedicineNameStr,0, 0, 0, 0, "", "", "", "");
//getLogs().info("aaaa"+SlotNumber);
BuyStructure buyProduct = new BuyStructure(0, SlotNumber, productStructure, 1);
MyApp.getInstance().getBuyList().add(buyProduct);
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
/*int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogTakeMedicineListAdpter.notifyItemRangeRemoved(0, buyListSize);
}*/
cancel();
}
public void cancelOnThreadCountdown() {
/*int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogTakeMedicineListAdpter.notifyItemRangeRemoved(0, buyListSize);
}*/
getTools().dialogClose();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.btnNonPerson.setText(getCtx().getString(R.string.close_countdown, 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

@ -0,0 +1,273 @@
package com.unibuy.smartdevice.ui.dialog;
import android.content.Context;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.databinding.DialogCmuhTakeMedicineBinding;
import com.unibuy.smartdevice.external.HttpAPI;
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.RecyclerDialogTakeMedicineListAdpter;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class TakeMedicineDialog extends DialogAbstract {
public enum Option {
TOAST(0),
COUNTDOWN_CANCEL(1),
SET_COUNTDOWN_TEXT(2),
SET_PRICE(3),
RESET_COUNTDOWM(4),
RESET_COUNTDOWM_TOAST(5),
CHANGE_DISPATCH(6),
REPORT_FLOW(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();
}
private boolean isChangeDispatch;
private boolean isRun;
boolean isResponse;
private HttpAPI httpAPI;
@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) {
getSrcHandlerMain().start("這裡會出現貨道無貨提示警語","@再刷一次,若不行,請洽發藥窗口!");
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:
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM:
closeDialogCountdown.setCountdown(40);
break;
case RESET_COUNTDOWM_TOAST:
super.execute(context, commandCode, message);
closeDialogCountdown.setCountdown(40);
break;
case CHANGE_DISPATCH:
changeDispatch();
break;
case REPORT_FLOW:
getLogs().info(MyApp.getInstance().getReportFlowInfoData().toString());
if (MyApp.getInstance().getReportFlowInfoData().getFlowStatusType() == 0) {
start(getSrcClassName(), CheckoutCashDialog.Option.CHANGE_BUY.getOption(), "change buy");
} else {
if (MyApp.getInstance().getReportFlowInfoData().getInvoiceInfo().isEmpty()) {
start(getSrcClassName(), CheckoutCashDialog.Option.CHANGE_DISPATCH.getOption(), "change dispatch");
} else {
start(getSrcClassName(), CheckoutCashDialog.Option.POST_INVOICE.getOption(), "post invoice");
}
}
// new HandlerMainCountdown(this) {
// @Override
// protected Class<?> setCls() {
// return this.getClass();
// }
//
// @Override
// protected HandlerMain setHandlerMain() {
// return getSrcHandlerMain();
// }
//
// @Override
// protected void execute(long countdown, HandlerMain handlerMain) {
//
// }
//
// @Override
// protected void close(HandlerMain handlerMain) {
// if (MyApp.getInstance().getReportFlowInfoData().getFlowStatusType() == 0) {
// handlerMain.start(getSrcClassName(), CheckoutCashDialog.Option.CHANGE_BUY.getOption(), "change buy");
// } else {
// if (MyApp.getInstance().getReportFlowInfoData().getInvoiceInfo().isEmpty()) {
// handlerMain.start(getSrcClassName(), CheckoutCashDialog.Option.CHANGE_DISPATCH.getOption(), "change dispatch");
// } else {
// handlerMain.start(getSrcClassName(), CheckoutCashDialog.Option.POST_INVOICE.getOption(), "post invoice");
// }
// }
// }
// }.start(2);
break;
}
}
}
};
}
private DialogCmuhTakeMedicineBinding binding;
private RecyclerDialogTakeMedicineListAdpter recyclerDialogTakeMedicineListAdpter;
private CloseDialogOnThreadHandler closeDialogCountdown;
public TakeMedicineDialog(Context context, HandlerMain srcHandlerMain) {
super(context, srcHandlerMain);
getLogs().info("open:" + getClass().getSimpleName());
}
@Override
protected void onCreate(Context context) {
binding = DialogCmuhTakeMedicineBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
this.isChangeDispatch = true;
this.isRun = true;
isResponse = true;
this.httpAPI = new HttpAPI(getHandlerMain());
this.recyclerDialogTakeMedicineListAdpter = new RecyclerDialogTakeMedicineListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogTakeMedicineListAdpter);
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
MyApp.getInstance().initialReportData();
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
String decryptedStr = MyApp.getInstance().getDecryptedData();
JSONObject jsonObject = null;
try {
jsonObject = new JSONObject(decryptedStr);
} catch (JSONException e) {
throw new RuntimeException(e);
}
binding.tvHostId.setText("管理單位ID"+jsonObject.optString("HOSP_ID"));
binding.tvSn.setText("序號:"+jsonObject.optString("SN"));
binding.tvName.setText("姓名:"+jsonObject.optString("NAME"));
binding.tvDrgno.setText("領藥號:"+jsonObject.optString("DRGNO"));
binding.tvData.setText("備註:"+jsonObject.optString("DATA"));
binding.tvPrintTime.setText("列印時間:"+jsonObject.optString("PRINT_TIME"));
this.binding.buttonAddProduct.setOnClickListener(v -> gotoAddProduct());
//this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
closeDialogCountdown.start(40);
// if (MyApp.getInstance().getDevSet().isShoppingCar()) {
// binding.buttonAddProduct.setVisibility(View.VISIBLE);
// } else {
// binding.buttonAddProduct.setVisibility(View.GONE);
// }
}
public void gotoAddProduct() {
//getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.ADD_PRODUCT.getOption(), "請在40秒內完成加購動作");
changeDispatch();
closeDialogCountdown.shutdown();
cancel();
}
public void changeDispatch() {
if (isChangeDispatch){
isRun = false;
isChangeDispatch = false;
//closeMoneyType();
closeDialogCountdown.shutdown();
changeDialog(DispatchDialog.class);
}
}
public void dialogCancel() {
closeDialogCountdown.shutdown();
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogTakeMedicineListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
cancel();
}
public void cancelOnThreadCountdown() {
int buyListSize = MyApp.getInstance().getBuyList().size();
MyApp.getInstance().getBuyList().clear();
if (buyListSize > 0) {
recyclerDialogTakeMedicineListAdpter.notifyItemRangeRemoved(0, buyListSize);
}
getTools().dialogClose();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.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

@ -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,91 @@
package com.unibuy.smartdevice.ui.recycler;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.unibuy.smartdevice.databinding.RecyclerDialogTakeMedicineListBinding;
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.tools.ImageGlide;
import java.util.List;
public class RecyclerDialogTakeMedicineListAdpter extends RecyclerView.Adapter<RecyclerDialogTakeMedicineListAdpter.ViewHolder> {
private Logs logs;
private Context context;
private final List<BuyStructure> buyList;
private HandlerMain handlerMain;
public RecyclerDialogTakeMedicineListAdpter(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 {
RecyclerDialogTakeMedicineListBinding binding;
public ViewHolder(@NonNull RecyclerDialogTakeMedicineListBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 使用 ViewBinding 來綁定佈局
RecyclerDialogTakeMedicineListBinding binding = RecyclerDialogTakeMedicineListBinding.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(buyProduct.getSlot()));
String productID = buyProduct.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
} else {
holder.binding.textProductName.setText("點我選擇商品");
}
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 count = buyProduct.getCount();
holder.binding.textCount.setText("x"+String.valueOf(count));
logs.info("field:" + buyProduct.getField());
try {
SlotField slotField = SlotField.getSlotField(buyProduct.getField());
logs.info("slotField:" + slotField.isCanAddPurchased());
} catch (LogsEmptyException e) {
}
}
@Override
public int getItemCount() {
return buyList.size();
}
}

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

@ -0,0 +1,198 @@
<?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/linearLayout1"
android:layout_width="match_parent"
android:layout_height="360dp"
android:orientation="vertical"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="50dp"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/tv_host_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="36dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="100dp"
/>
<TextView
android:id="@+id/tv_sn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="36dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="100dp"
/>
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="36dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="100dp"
/>
<TextView
android:id="@+id/tv_drgno"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="36dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="100dp"
/>
<TextView
android:id="@+id/tv_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="36dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="100dp"
/>
<TextView
android:id="@+id/tv_print_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="36dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="100dp"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout112"
android:layout_width="match_parent"
android:layout_height="10dp"
android:gravity="center"
android:orientation="horizontal"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"
app:layout_constraintTop_toBottomOf="@id/linearLayout1"
app:layout_constraintStart_toStartOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#000000"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout1111"
android:layout_width="match_parent"
android:layout_height="150dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/linearLayout112"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="請確認領藥人的訊息內容是否正確\n再按下「確定取貨」按鈕"
android:textSize="36dp"
android:gravity="center"
android:textAlignment="center"/>
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/light_gray"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/linearLayout1111"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/red"
android:gravity="center"
android:text="出貨明細:"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerDialogBuyList"
android:layout_width="match_parent"
android:layout_height="750dp"
/>
<LinearLayout
android:id="@+id/layoutNfcPay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
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:visibility="gone"
android:text="取消(40)"
android:textSize="30sp"
android:textColor="@color/black" />
<Button
android:id="@+id/buttonAddProduct"
android:layout_width="wrap_content"
android:layout_height="120dp"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:text="確定取貨"
android:textColor="@color/black"
android:textSize="60sp"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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,87 @@
<?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">
<LinearLayout
android:id="@+id/linearLayout211"
android:layout_width="match_parent"
android:layout_height="150dp"
android:gravity="center"
android:orientation="horizontal"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/tv_drgnoid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="領藥號:"
android:textSize="60dp"
android:layout_gravity="bottom"
android:gravity="center"
android:textAlignment="center"/>
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout212"
android:layout_width="match_parent"
android:layout_height="300dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/linearLayout211"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/tv_error_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/cmuh_pickup_failed_contact_counter"
android:textSize="42sp"
android:gravity="center"
android:textAlignment="center"/>
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout1121"
android:layout_width="match_parent"
android:layout_height="10dp"
android:gravity="center"
android:orientation="horizontal"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"
app:layout_constraintTop_toBottomOf="@id/linearLayout212"
app:layout_constraintStart_toStartOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#B0B0B0"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:orientation="horizontal"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"
app:layout_constraintTop_toBottomOf="@id/linearLayout1121"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/btn_non_person"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/close_40"
android:textSize="30sp"
tools:ignore="MissingConstraints" />
</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,102 @@
<?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/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

@ -0,0 +1,16 @@
package com.unibuy.smartdevice;
import com.unibuy.smartdevice.ui.FontendActivity;
public final class AppEntry {
private AppEntry() {
}
public static boolean isCmuh() {
return "Cmuh".equalsIgnoreCase(BuildConfig.FLAVOR_project);
}
public static Class<?> initialActivity() {
return isCmuh() ? FontendActivity.class : HomeActivity.class;
}
}

View File

@ -239,6 +239,14 @@ public class HomeActivity extends AppCompatActivityAbstract {
}
settingsDao.close();
if (AppEntry.isCmuh()) {
Intent intent = new Intent(this, FontendActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
return;
}
binding = ActivityHomeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
@ -312,7 +320,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
getLogs().info("🔄HomeActivity - Scheduled Hard App restarting via gracefulDisconnect...");
com.unibuy.smartdevice.external.mqtt.MqttManager.getInstance().gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -343,7 +351,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
protected void execute(HandlerMain handlerMain) {
for (int testIndex=0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("RestartHost testCount:" + testCount);
testCount++;
} else {
@ -389,16 +397,16 @@ public class HomeActivity extends AppCompatActivityAbstract {
// 最多等待 10 分鐘120 * 5 確保機台有機會重啟
for (int i = 0; i < 120; i++) {
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
boolean isBusy = MyApp.getInstance().isMachineBusy();
if (idle >= 90 && !isDialog) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒且無對話框,執行真重啟");
if (idle >= 90 && !isBusy) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒且機台不忙碌,執行真重啟");
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
ReSetAPP2();
});
return;
}
getLogs().debug("RestartAPP2閒置 " + idle + " 秒,Dialog:" + isDialog + ",等待 5 秒後重新判斷 (" + (i+1) + "/120)");
getLogs().debug("RestartAPP2閒置 " + idle + " 秒,BusyReason:" + MyApp.getInstance().getBusyReason() + ",等待 5 秒後重新判斷 (" + (i+1) + "/120)");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
@ -427,7 +435,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
protected void execute(HandlerMain handlerMain) {
for (int testIndex=0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("RestartHost testCount:" + testCount);
testCount++;
} else {
@ -473,7 +481,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
protected void execute(HandlerMain handlerMain) {
for (int testIndex=0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("NfcCheckout testCount:" + testCount);
testCount++;
} else {
@ -556,7 +564,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
Looper.prepare(); // 確保這個執行緒有 Looper
for (int testIndex=0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("CMUHAutoRun testCount:" + testCount);
testCount++;
} else {

View File

@ -17,8 +17,11 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.LayoutInflater;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.graphics.Insets;
@ -57,6 +60,7 @@ import com.unibuy.smartdevice.ui.tools.VideoImageMixedPlayer;
import com.unibuy.smartdevice.utils.LanguageHelper;
import java.util.List;
import java.util.Locale;
public class MainActivity extends AppCompatActivityAbstract {
@Override
@ -69,6 +73,8 @@ public class MainActivity extends AppCompatActivityAbstract {
return this.getClass();
}
private AlertDialog activeLoginDialog;
@Override
protected HandlerMain setHandlerMain() {
return new ToastHandlerMain(getCtx(), getLogs()) {
@ -76,10 +82,29 @@ public class MainActivity extends AppCompatActivityAbstract {
public void execute(Context context, int commandCode, String message) {
super.execute(context, commandCode, message);
if ("登入成功".equals(message)) {
if (pendingLoginTraceId != null) {
long now = System.currentTimeMillis();
getLogs().info("[LoginTrace][" + pendingLoginTraceId + "] callback=success totalMs="
+ (now - pendingLoginStartMs) + " msg=" + message);
}
updateLoginStatus();
if (activeLoginDialog != null && activeLoginDialog.isShowing()) {
activeLoginDialog.dismiss();
activeLoginDialog = null;
}
if (pendingAction != null) {
pendingAction.run();
pendingAction = null;
}
pendingLoginTraceId = null;
pendingLoginStartMs = 0L;
} else if (pendingLoginTraceId != null && message != null
&& (message.contains("登入失敗") || message.contains("網路錯誤") || message.contains("解析失敗"))) {
long now = System.currentTimeMillis();
getLogs().info("[LoginTrace][" + pendingLoginTraceId + "] callback=failure totalMs="
+ (now - pendingLoginStartMs) + " msg=" + message + " code=" + commandCode);
pendingLoginTraceId = null;
pendingLoginStartMs = 0L;
}
}
};
@ -89,6 +114,8 @@ public class MainActivity extends AppCompatActivityAbstract {
private HttpAPI httpAPI;
private StarCloudAPI starCloudAPI;
private Runnable pendingAction;
private String pendingLoginTraceId;
private long pendingLoginStartMs;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -111,79 +138,66 @@ public class MainActivity extends AppCompatActivityAbstract {
// ---- 環境尺寸設定 ----
initialData();
updateLoginStatus();
binding.btnStatusAction.setOnClickListener(v -> {
if (MyApp.getInstance().getSuAccess().isEmpty()) {
showLoginDialog(getCtx(), this::updateLoginStatus);
} else {
MyApp.getInstance().setSuAccess("");
updateLoginStatus();
Toast.makeText(getCtx(), "已登出後台權限", Toast.LENGTH_SHORT).show();
}
});
binding.buttonGotoSetting.setOnClickListener(v -> {
pendingAction = new Runnable() {
@Override
public void run() {
getTools().gotoActivity(SettingActivity.class);
}
};
Runnable action = () -> getTools().gotoActivity(SettingActivity.class);
if (!MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
if (MyApp.getInstance().getSuAccess().isEmpty()) {
showLoginDialog(getCtx());
showLoginDialog(getCtx(), action);
return;
}
}
pendingAction.run();
pendingAction = null;
action.run();
});
binding.buttonGotoSystemSet.setOnClickListener(v -> {
pendingAction = new Runnable() {
@Override
public void run() {
getTools().gotoActivity(SystemSetActivity.class);
}
};
Runnable action = () -> getTools().gotoActivity(SystemSetActivity.class);
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
getHandlerMain().start("MainActivity", "@先綁定機器");
return;
}
if (MyApp.getInstance().getSuAccess().isEmpty()) {
showLoginDialog(getCtx());
showLoginDialog(getCtx(), action);
return;
}
pendingAction.run();
pendingAction = null;
action.run();
});
binding.buttonGotoProductList.setOnClickListener(v -> {
pendingAction = new Runnable() {
@Override
public void run() {
getTools().gotoActivity(ProductListActivity.class);
}
};
Runnable action = () -> getTools().gotoActivity(ProductListActivity.class);
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
getHandlerMain().start("MainActivity", "@先綁定機器");
return;
}
if (MyApp.getInstance().getSuAccess().isEmpty()) {
showLoginDialog(getCtx());
showLoginDialog(getCtx(), action);
return;
}
pendingAction.run();
pendingAction = null;
action.run();
});
binding.buttonGotoSlotList.setOnClickListener(v -> {
pendingAction = new Runnable() {
@Override
public void run() {
getTools().gotoActivity(VmcSlotListActivity.class);
}
};
Runnable action = () -> getTools().gotoActivity(VmcSlotListActivity.class);
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
getHandlerMain().start("MainActivity", "@先綁定機器");
return;
}
if (MyApp.getInstance().getSuAccess().isEmpty()) {
showLoginDialog(getCtx());
showLoginDialog(getCtx(), action);
return;
}
pendingAction.run();
pendingAction = null;
action.run();
});
binding.buttonGotoShoppingPlatform.setOnClickListener(v -> {
@ -247,17 +261,18 @@ public class MainActivity extends AppCompatActivityAbstract {
});
binding.buttonGotoXyshjTest.setOnClickListener(v -> {
Runnable action = () -> getTools().gotoActivity(XyshjTestActivity.class);
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
getHandlerMain().start("MainActivity", "@先綁定機器");
return;
}
if (MyApp.getInstance().getSuAccess().isEmpty()) {
showLoginDialog(getCtx());
showLoginDialog(getCtx(), action);
return;
}
getTools().gotoActivity(XyshjTestActivity.class);
action.run();
});
binding.buttonresetapp.setOnClickListener(v -> {
@ -265,12 +280,15 @@ public class MainActivity extends AppCompatActivityAbstract {
ReSetAPP();
});
binding.buttonOpenSystemSettings.setOnClickListener(v -> {
// 開啟 Android 系統設定
Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
binding.buttonGotoAndroidSettings.setOnClickListener(v -> {
try {
Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS);
startActivity(intent);
} catch (Exception e) {
Toast.makeText(getCtx(), "無法開啟安卓設定頁", Toast.LENGTH_SHORT).show();
}
});
}
private static class ButtonQrCodeTestOnClick extends HandlerMainSchedulerOnClick {
@ -476,10 +494,16 @@ public class MainActivity extends AppCompatActivityAbstract {
}
private void initialData() {
if (MyApp.getInstance().getMachine() != null || MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
if (MyApp.getInstance().getMachine() != null && MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
SettingsDao settingsDao = new SettingsDao(this);
SettingStructure setting = settingsDao.getOne(MyApp.getInstance().getMachine().getClass().getSimpleName());
MyApp.getInstance().getMachine().setMachineID(setting.getData(1));
if (setting != null && setting.getDataCount() >= 2) {
MyApp.getInstance().getMachine().setApiKey(setting.getData(0));
MyApp.getInstance().getMachine().setMachineID(setting.getData(1));
if (setting.getDataCount() >= 3 && setting.getData(2) != null && !setting.getData(2).isEmpty()) {
MyApp.getInstance().getMachine().setMqttEnv(setting.getData(2));
}
}
settingsDao.close();
}
}
@ -518,88 +542,103 @@ public class MainActivity extends AppCompatActivityAbstract {
}
public void showLoginDialog(Context context) {
// 創建一個垂直的 LinearLayout
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(50, 20, 50, 10);
showLoginDialog(context, null);
}
// 帳號輸入框
final EditText accessInput = new EditText(context);
accessInput.setHint("帳號");
layout.addView(accessInput);
public void showLoginDialog(Context context, Runnable onSuccess) {
// 使用 LayoutInflater 載入自訂佈局
View view = LayoutInflater.from(context).inflate(R.layout.dialog_admin_login, null);
final EditText accessInput = view.findViewById(R.id.etAccess);
final EditText passwordInput = view.findViewById(R.id.etPassword);
final CheckBox rememberAccountCb = view.findViewById(R.id.cbRemember);
androidx.appcompat.widget.AppCompatButton positiveButton = view.findViewById(R.id.btnLogin);
androidx.appcompat.widget.AppCompatButton negativeButton = view.findViewById(R.id.btnCancel);
getTools().setImmHideByFocusChange(accessInput);
getTools().setImmOpenByFocusChange(accessInput);
// 密碼輸入框
final EditText passwordInput = new EditText(context);
passwordInput.setHint("密碼");
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(passwordInput);
getTools().setImmHideByFocusChange(passwordInput);
// 讀取紀錄的帳號
android.content.SharedPreferences prefs = context.getSharedPreferences("LoginPrefs", Context.MODE_PRIVATE);
String savedAccount = prefs.getString("saved_account", "");
if (!savedAccount.isEmpty()) {
accessInput.setText(savedAccount);
}
rememberAccountCb.setChecked(!savedAccount.isEmpty()); // 如果有存過帳號預設打勾
// 建立對話框
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle("登入")
.setView(layout)
.setPositiveButton("登入", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String access = accessInput.getText().toString().trim();
String password = passwordInput.getText().toString().trim();
.setView(view)
.create();
activeLoginDialog = dialog;
if (access.isEmpty() || password.isEmpty()) {
Toast.makeText(context, "請輸入帳號和密碼", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "正在驗證登入...", Toast.LENGTH_SHORT).show();
starCloudAPI.login(access, password);
}
// 設定對話框視窗背景為透明以便呈現圓角設計
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
}
getTools().hideSystemBars((Activity) getCtx());
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
getTools().hideSystemBars((Activity) getCtx());
dialog.dismiss();
}
}).create();
// 設定點擊事件
positiveButton.setOnClickListener(v -> {
long clickStartMs = System.currentTimeMillis();
String access = accessInput.getText().toString().trim();
String password = passwordInput.getText().toString().trim();
String traceId = String.format(Locale.US, "L%tH%<tM%<tS-%03d", clickStartMs, clickStartMs % 1000);
getLogs().info("[LoginTrace][" + traceId + "] click_login accountLen=" + access.length());
// 🛠 Debug Backdoor: 長按對話框標題 3 秒跳過登入
dialog.setOnShowListener(d -> {
int titleId = context.getResources().getIdentifier("alertTitle", "id", "android");
View titleView = dialog.findViewById(titleId);
if (titleView != null) {
final Handler titleHandler = new Handler();
final Runnable titleRunnable = () -> {
Toast.makeText(context, "🛠️ Debug Mode: 跳過登入", Toast.LENGTH_SHORT).show();
MyApp.getInstance().setSuAccess("DEBUG_MODE");
if (pendingAction != null) {
pendingAction.run();
pendingAction = null;
}
dialog.dismiss();
};
titleView.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case android.view.MotionEvent.ACTION_DOWN:
titleHandler.postDelayed(titleRunnable, 2000); // 2秒
break;
case android.view.MotionEvent.ACTION_UP:
case android.view.MotionEvent.ACTION_CANCEL:
titleHandler.removeCallbacks(titleRunnable);
break;
}
return true;
});
if (access.isEmpty() || password.isEmpty()) {
getLogs().info("[LoginTrace][" + traceId + "] blocked_empty_input");
Toast.makeText(context, "請輸入帳號和密碼", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "正在驗證登入...", Toast.LENGTH_SHORT).show();
// 為了處理 StarCloudAPI 登入成功後的回調
// 我們重新設計 handlerMain 讓它能夠在 onSuccess 回調中跑 pendingAction
pendingAction = onSuccess;
pendingLoginTraceId = traceId;
pendingLoginStartMs = clickStartMs;
getLogs().info("[LoginTrace][" + traceId + "] dispatch_starcloud_login");
starCloudAPI.login(access, password, traceId, clickStartMs);
if (rememberAccountCb.isChecked()) {
prefs.edit().putString("saved_account", access).apply();
getLogs().info("[LoginTrace][" + traceId + "] remember_account=save");
} else {
prefs.edit().remove("saved_account").apply();
getLogs().info("[LoginTrace][" + traceId + "] remember_account=clear");
}
// 🛠 關鍵優化不再此處立即 dismiss()改由 API 回報成功時關閉
}
getTools().hideSystemBars((Activity) getCtx());
});
negativeButton.setOnClickListener(v -> {
getTools().hideSystemBars((Activity) getCtx());
dialog.dismiss();
activeLoginDialog = null;
});
dialog.show();
}
/**
* 更新右上角登入狀態顯示
*/
private void updateLoginStatus() {
String suAccess = MyApp.getInstance().getSuAccess();
if (suAccess.isEmpty()) {
binding.tvWelcomeTitle.setText("歡迎使用 AAT\n智能販賣機系統");
binding.tvWelcomeHint.setText("請點擊右上角登入帳號");
binding.btnStatusAction.setText("登入");
binding.btnStatusAction.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.primary)));
} else {
binding.tvWelcomeTitle.setText("歡迎使用 AAT\n" + suAccess);
binding.tvWelcomeHint.setText("請點擊下方按鈕操作");
binding.btnStatusAction.setText("登出");
binding.btnStatusAction.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.error)));
}
}
private void enterShoppingPlatform() {
MyApp.getInstance().setSuAccess("");
getTools().gotoActivity(FontendActivity.class);
@ -610,7 +649,7 @@ public class MainActivity extends AppCompatActivityAbstract {
getLogs().info("🔄MainActivity - Manual App restarting via gracefulDisconnect...");
com.unibuy.smartdevice.external.mqtt.MqttManager.getInstance().gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -634,15 +673,15 @@ public class MainActivity extends AppCompatActivityAbstract {
super.onStart();
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
binding.buttonGotoSystemSet.setVisibility(View.GONE);
binding.buttonGotoProductList.setVisibility(View.GONE);
binding.buttonGotoSlotList.setVisibility(View.GONE);
binding.buttonGotoShoppingPlatform.setVisibility(View.GONE);
binding.wrapButtonGotoSystemSet.setVisibility(View.GONE);
binding.wrapButtonGotoProductList.setVisibility(View.GONE);
binding.wrapButtonGotoSlotList.setVisibility(View.GONE);
binding.wrapButtonGotoShoppingPlatform.setVisibility(View.GONE);
} else {
binding.buttonGotoSystemSet.setVisibility(View.VISIBLE);
binding.buttonGotoProductList.setVisibility(View.VISIBLE);
binding.buttonGotoSlotList.setVisibility(View.VISIBLE);
binding.buttonGotoShoppingPlatform.setVisibility(View.VISIBLE);
binding.wrapButtonGotoSystemSet.setVisibility(View.VISIBLE);
binding.wrapButtonGotoProductList.setVisibility(View.VISIBLE);
binding.wrapButtonGotoSlotList.setVisibility(View.VISIBLE);
binding.wrapButtonGotoShoppingPlatform.setVisibility(View.VISIBLE);
}
}
}
}

View File

@ -97,6 +97,9 @@ public class MyApp extends Application {
private Map<Integer, List<SlotStructure>> slotMap;
private List<MarketingPlanStructure> marketingPlanList;
private List<BuyStructure> buyList;
private String decryptedData;
private String cmuhPickupErrorMessage;
private Map<String, List<MemoStructure>> memoMap;
private CashSetStructure cashSetStructure;
private AccessCodeStructure accessCodeStructure;
private FreeGiftStructure freeGiftStructure;
@ -277,6 +280,8 @@ public class MyApp extends Application {
initialSlotData();
this.marketingPlanList = new ArrayList<>();
this.buyList = new ArrayList<>();
this.memoMap = new HashMap<>();
initialMemoData();
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(
UploadLogPeriodicWorker.class,
@ -406,6 +411,45 @@ public class MyApp extends Application {
);
}
public static boolean checkAndReconnectDev() {
boolean isAlive = false;
Logs staticLogs = new Logs(MyApp.class);
if (devXinYuanController != null) {
try {
// 發送同步包測試
devXinYuanController.addSendBuffer(
devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)
);
Thread.sleep(100);
if (!devXinYuanController.getReadBufferByMax().isEmpty()) {
isAlive = true;
}
} catch (Exception e) {
isAlive = false;
}
}
// 若不活躍則重連
if (!isAlive) {
staticLogs.info("下位機失連,重新連線中...");
if (devXinYuanController != null) {
devXinYuanController.shutdown();
}
devXinYuanController = new DevXinYuanController(devXinYuanController.getHandlerMain());
devXinYuanController.start();
devXinYuanController.addSendBuffer(
devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)
);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
public void initMqtt() {
String machineId = MyApp.getInstance().getMachine().getMachineID();
String apiKey = MyApp.getInstance().getMachine().getApiKey();
@ -667,6 +711,70 @@ public class MyApp extends Application {
return buyList;
}
public Logs getLogs() {
return logs;
}
public void setDecryptedData(String decryptedData) {
this.decryptedData = decryptedData;
}
public String getDecryptedData() {
return decryptedData;
}
public void setCmuhPickupErrorMessage(String cmuhPickupErrorMessage) {
this.cmuhPickupErrorMessage = cmuhPickupErrorMessage;
}
public String getCmuhPickupErrorMessage() {
return cmuhPickupErrorMessage;
}
public Map<String, List<MemoStructure>> getMemoMap() {
return memoMap;
}
private void initialMemoData() {
logs.debug("initialMemoData");
// 確保 memoMap 已初始化
if (getInstance().getMemoMap() == null) {
logs.info("memoMap初始化");
getInstance().memoMap = new HashMap<>();
}
// 開架構
if (!getInstance().getMemoMap().containsKey("CMUH")) {
getInstance().getMemoMap().put("CMUH", new ArrayList<>());
logs.info("CMUH memo db open");
}
// 查詢資料庫
MemoDao memoDao = new MemoDao(this);
try {
Map<String, List<MemoStructure>> memoMapNew = memoDao.getAll();
if (memoMapNew != null && memoMapNew.get("CMUH") != null && !memoMapNew.get("CMUH").isEmpty()) {
getInstance().getMemoMap().get("CMUH").addAll(memoMapNew.get("CMUH"));
}
} catch (Exception e) {
logs.info("📛 讀取 memo 資料庫失敗或無資料,跳過。");
} finally {
memoDao.close();
}
// 無資料就初始化一筆資料
if (getInstance().getMemoMap().get("CMUH").isEmpty()) {
logs.info("初始化CMUH Memo資料表");
String memoStr = "{\"ITEM\": [{\"SN\": \"00000000\", \"DRGNO\": \"0000\"}]}";
try {
getInstance().getMemoMap().get("CMUH").add(new MemoStructure("CMUH", new JSONObject(memoStr), new JSONObject()));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}
public EsunpayStructure getEsunpay() {
return esunpay;
}

View File

@ -108,7 +108,7 @@ public class MemoDao {
}
// 獲取所有 Products
private Map<String, List<MemoStructure>> getAll() {
public Map<String, List<MemoStructure>> getAll() {
Map<String, List<MemoStructure>> memoMap = new HashMap<>();
Cursor cursor = database.query(

View File

@ -46,6 +46,18 @@ public enum ComPort {
}
public String getFilepath() {
if (this == COM_PORT_S1) {
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
return "/dev/ttyS1";
}
return "/dev/ttyS7";
}
if (this == COM_PORT_S2) {
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
return "/dev/ttyS2";
}
return "/dev/ttyS6";
}
return filepath;
}

View File

@ -12,6 +12,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -37,7 +38,7 @@ public class Logs {
return className;
}
public void writeFile(String data) {
public synchronized void writeFile(String data) {
File txtDebug = new File(Tools.getExternalFilesDir(), "debug.txt");
if (txtDebug.exists()) {
txtDebug.setReadable(true, false);
@ -48,8 +49,9 @@ public class Logs {
data = dateTime + " " + data;
try (FileOutputStream fos = new FileOutputStream(txtDebug, true)) { // try-with-resources 自動 close()
fos.write(data.getBytes());
fos.write("\n".getBytes()); // 換行
fos.write(data.getBytes(StandardCharsets.UTF_8));
fos.write("\n".getBytes(StandardCharsets.UTF_8)); // 換行
fos.flush();
} catch (IOException e) {
warning(e);
}

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,10 @@ import java.util.ArrayList;
public class StarCloudAPI {
private static final String TAG = "StarCloudAPI";
public static String domainNameByNewHost = "https://logs.setdomains.work/";
public static String updateOnDomainNameByNewHost = domainNameByNewHost + "api/machine/"
+ MyApp.getInstance().getMachine().getMachineID() + "/log/update";
private String getBaseUrl() {
if ("prod".equals(MyApp.getInstance().getMachine().getMqttEnv())) {
return "https://starcloud.taiwan-star.com.tw/api/v1/app/";
@ -51,13 +55,26 @@ public class StarCloudAPI {
this.srcHandlerMain = srcHandlerMain;
}
private void traceLogin(String message) {
if (handlerMain != null && handlerMain.getLogs() != null) {
handlerMain.getLogs().info("[LoginTrace]" + message);
} else {
Log.i("LoginTrace", message);
}
}
/**
* B000: 管理員登入驗證
*/
public void login(final String account, final String password) {
login(account, password, "NO_TRACE", System.currentTimeMillis());
}
public void login(final String account, final String password, final String traceId, final long startMs) {
Log.i(TAG, "login() called for account: " + account);
final String url = getBaseUrl() + "admin/login/B000";
Log.i(TAG, "Starting B000 request to: " + url);
traceLogin("[" + traceId + "] b000_start fromUiMs=" + (System.currentTimeMillis() - startMs));
// 不依賴 FormPostMainScheduler (會靜默吞掉 IO exception)
// 直接用 Thread + HttpURLConnection 確保錯誤一定看得到
@ -66,6 +83,7 @@ public class StarCloudAPI {
public void run() {
int responseCode = -1;
String receive = "";
long threadStartMs = System.currentTimeMillis();
try {
JSONObject json = new JSONObject();
json.put("Su_Account", account);
@ -90,10 +108,15 @@ public class StarCloudAPI {
conn.setDoOutput(true);
byte[] bodyBytes = json.toString().getBytes("UTF-8");
long writeStartMs = System.currentTimeMillis();
conn.getOutputStream().write(bodyBytes);
conn.getOutputStream().flush();
traceLogin("[" + traceId + "] b000_write_done tMs=" + (System.currentTimeMillis() - writeStartMs));
long responseStartMs = System.currentTimeMillis();
responseCode = conn.getResponseCode();
traceLogin("[" + traceId + "] b000_response_code=" + responseCode
+ " waitMs=" + (System.currentTimeMillis() - responseStartMs));
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
java.io.BufferedReader br = new java.io.BufferedReader(
@ -105,8 +128,12 @@ public class StarCloudAPI {
conn.disconnect();
Log.d(TAG, "B000 Response (" + responseCode + "): " + receive);
traceLogin("[" + traceId + "] b000_network_done totalNetMs="
+ (System.currentTimeMillis() - threadStartMs));
} catch (final Exception e) {
Log.e(TAG, "B000 Network Error", e);
traceLogin("[" + traceId + "] b000_network_error afterMs="
+ (System.currentTimeMillis() - threadStartMs) + " err=" + e.getMessage());
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
@ -122,6 +149,7 @@ public class StarCloudAPI {
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
long mainStartMs = System.currentTimeMillis();
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
@ -129,16 +157,28 @@ public class StarCloudAPI {
MyApp.getInstance().setSuAccess(res.optString("Su_Account", account));
// 移除覆蓋 ApiKey 的邏輯因為 B000 回傳的可能是 Admin Token但我們需要保留 Machine Token
Log.i(TAG, "Login success for user: " + account);
traceLogin("[" + traceId + "] b000_parse_success parseMs="
+ (System.currentTimeMillis() - mainStartMs)
+ " totalMs=" + (System.currentTimeMillis() - startMs));
handlerMain.start("StarCloudAPI", 200, "登入成功");
} else {
traceLogin("[" + traceId + "] b000_business_fail code=401 parseMs="
+ (System.currentTimeMillis() - mainStartMs)
+ " totalMs=" + (System.currentTimeMillis() - startMs));
handlerMain.start("StarCloudAPI", 401,
"登入失敗: " + res.optString("message", "帳號密碼錯誤"));
}
} catch (Exception e) {
Log.e(TAG, "B000 Parse Error: " + e.getMessage());
traceLogin("[" + traceId + "] b000_parse_error parseMs="
+ (System.currentTimeMillis() - mainStartMs)
+ " totalMs=" + (System.currentTimeMillis() - startMs)
+ " err=" + e.getMessage());
handlerMain.start("StarCloudAPI", 500, "登入解析失敗: " + e.getMessage());
}
} else {
traceLogin("[" + traceId + "] b000_http_fail code=" + finalCode
+ " totalMs=" + (System.currentTimeMillis() - startMs));
handlerMain.start("StarCloudAPI", finalCode,
"網路錯誤 (" + finalCode + "): " + finalReceive);
}

View File

@ -15,7 +15,7 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.unibuy.smartdevice.HomeActivity;
import com.unibuy.smartdevice.AppEntry;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.devices.SlotField;
@ -57,7 +57,7 @@ public class MqttService extends Service {
createNotificationChannel();
// 2. 啟動為前台服務確保不被系統殺掉
Intent notificationIntent = new Intent(this, HomeActivity.class);
Intent notificationIntent = new Intent(this, AppEntry.initialActivity());
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
@ -118,7 +118,7 @@ public class MqttService extends Service {
.postDelayed(() -> mqttManager.gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent restartIntent = new Intent(getApplicationContext(),
com.unibuy.smartdevice.HomeActivity.class);
AppEntry.initialActivity());
restartIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(restartIntent);
@ -160,12 +160,12 @@ public class MqttService extends Service {
mqttManager.publishCommandAck(cmdId, "success", "Machine unlocked, restarting...");
// 🚩 優雅重啟 gracefulDisconnect不觸發 LWT killProcess
// 解鎖後重啟到初始畫面 (HomeActivity)
// 解鎖後重啟到目前版本的初始畫面
new android.os.Handler(android.os.Looper.getMainLooper())
.postDelayed(() -> mqttManager.gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent restartIntent = new Intent(getApplicationContext(),
com.unibuy.smartdevice.HomeActivity.class);
AppEntry.initialActivity());
restartIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(restartIntent);
@ -448,6 +448,28 @@ public class MqttService extends Service {
logs.info("Failed to parse or start dispense command: " + e.getMessage());
mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage());
}
} else if ("update_app".equals(cmd)) {
logs.info("Executing remote app OTA update command...");
try {
String apkUrl = null;
if (command.getPayload() != null && command.getPayload().isJsonObject()) {
com.google.gson.JsonObject payloadObj = command.getPayload().getAsJsonObject();
if (payloadObj.has("url")) {
apkUrl = payloadObj.get("url").getAsString();
}
}
if (apkUrl == null || apkUrl.isEmpty()) {
throw new Exception("Missing 'url' parameter in update_app payload.");
}
// 啟動背景 OTA 更新管理器
com.unibuy.smartdevice.tools.OtaManager.startUpdate(getApplicationContext(), apkUrl, cmdId);
} catch (Exception e) {
logs.info("Failed to start remote app update: " + e.getMessage());
mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage());
}
} else {
logs.info("Unknown MQTT command: " + cmd);
mqttManager.publishCommandAck(cmdId, "failed", "Unknown command");

View File

@ -119,6 +119,11 @@ public class TransactionFinalizePayload {
*/
private int payment_status;
/**
* 訂單出貨狀態0 = 全部失敗, 1 = 全部成功, 2 = 部分失敗
*/
private int delivery_status;
/** 支付 Request 原始內容 (JSON String) */
private String payment_request;
@ -210,6 +215,14 @@ public class TransactionFinalizePayload {
this.payment_status = payment_status;
}
public int getDelivery_status() {
return delivery_status;
}
public void setDelivery_status(int delivery_status) {
this.delivery_status = delivery_status;
}
public String getPayment_request() {
return payment_request;
}

View File

@ -361,10 +361,11 @@ public class HttpConnect {
return outputFile;
}
public void uploadFile(String uploadUrl, File logFile) {
public boolean uploadFile(String uploadUrl, File logFile) {
HttpURLConnection connection = null;
DataOutputStream outputStream = null;
FileInputStream fileInputStream = null;
boolean uploadSuccess = false;
String boundary = "*****"; // 分隔符
String lineEnd = "\r\n";
@ -373,7 +374,7 @@ public class HttpConnect {
try {
if (!logFile.exists()) {
logs.debug("檔案不存在:" + logFile.getAbsolutePath());
return;
return false;
}
logs.debug("開始上傳:" + logFile.getAbsolutePath());
@ -413,6 +414,7 @@ public class HttpConnect {
if (responseCode == HttpURLConnection.HTTP_OK) {
logs.info("Log 上傳成功!");
logFile.delete();
uploadSuccess = true;
} else {
logs.debug("Log 上傳失敗HTTP 狀態碼:" + responseCode);
@ -444,6 +446,8 @@ public class HttpConnect {
connection.disconnect();
}
}
return uploadSuccess;
}
public File getFileFromUrl(String urlString) throws LogsFileNullException, LogsEmptyException {

View File

@ -0,0 +1,134 @@
package com.unibuy.smartdevice.tools;
import android.content.Context;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.external.mqtt.MqttManager;
import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* 負責遠端 APK 的背景下載與 Root 靜默覆蓋安裝管理
*/
public class OtaManager {
private static final String TAG = "OtaManager";
private static final Logs logs = new Logs(OtaManager.class);
private static final int DOWNLOAD_CONNECT_TIMEOUT_MS = 30000;
private static final int DOWNLOAD_READ_TIMEOUT_MS = 120000;
/**
* 啟動背景執行緒執行 APP OTA 更新
*
* @param context 應用程式 Context
* @param apkUrl 新版 APK 的下載位址
* @param cmdId MQTT 指令 ID用於回報執行結果
*/
public static void startUpdate(Context context, String apkUrl, String cmdId) {
new Thread(new Runnable() {
@Override
public void run() {
MqttManager mqttManager = MqttManager.getInstance();
try {
logs.info("Starting OTA update process. URL: " + apkUrl + " (cmdId: " + cmdId + ")");
mqttManager.publishCommandAck(cmdId, "success", "Downloading update APK...");
// 1. 背景下載 APK APP 私有快取空間
File apkFile = downloadApk(context, apkUrl);
if (apkFile == null || !apkFile.exists()) {
throw new Exception("Downloaded APK file is null or does not exist.");
}
logs.info("APK downloaded successfully to: " + apkFile.getAbsolutePath());
// 2. 執行 Root 靜默安裝
mqttManager.publishCommandAck(cmdId, "success", "Installing update APK...");
boolean installSuccess = silentInstall(apkFile.getAbsolutePath());
if (installSuccess) {
logs.info("Silent installation triggered successfully. App should be restarted by BootReceiver soon.");
// 安裝成功後系統會因為 ACTION_MY_PACKAGE_REPLACED 自動重啟主 APP
// 此處已不需要再做其他處理
} else {
throw new Exception("Silent installation command returned non-zero status.");
}
} catch (Exception e) {
logs.error(com.unibuy.smartdevice.exception.ErrorCode.UNKNOWN_ERROR, "OTA Update failed: " + e.getMessage());
mqttManager.publishCommandAck(cmdId, "failed", "OTA Update failed: " + e.getMessage());
}
}
}).start();
}
/**
* 從網路下載 APK 檔案並存入 APP 私有快取目錄中
*/
private static File downloadApk(Context context, String apkUrl) throws Exception {
URL url = new URL(apkUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(DOWNLOAD_CONNECT_TIMEOUT_MS);
connection.setReadTimeout(DOWNLOAD_READ_TIMEOUT_MS);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new Exception("Server returned HTTP " + connection.getResponseCode()
+ " " + connection.getResponseMessage());
}
// 使用 APP 私有的外部快取目錄免去 Android 12 WRITE_EXTERNAL_STORAGE 權限宣告與申請
File cacheDir = context.getExternalCacheDir();
if (cacheDir == null) {
cacheDir = context.getCacheDir();
}
File apkFile = new File(cacheDir, "update_ota.apk");
if (apkFile.exists()) {
apkFile.delete();
}
try (InputStream input = new BufferedInputStream(connection.getInputStream());
FileOutputStream output = new FileOutputStream(apkFile)) {
byte[] data = new byte[4096];
int count;
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
output.flush();
} finally {
connection.disconnect();
}
return apkFile;
}
/**
* 調用 Root 權限執行 pm install -r 靜默安裝
*/
private static boolean silentInstall(String apkPath) {
Process process = null;
DataOutputStream os = null;
try {
logs.info("Executing silent install command via su: pm install -r -d " + apkPath);
process = Runtime.getRuntime().exec("su");
os = new DataOutputStream(process.getOutputStream());
os.writeBytes("pm install -r -d " + apkPath + "\n");
os.writeBytes("exit\n");
os.flush();
int exitValue = process.waitFor();
logs.info("Silent install process exit value: " + exitValue);
return exitValue == 0;
} catch (Exception e) {
logs.info("Failed to execute silent install su command: " + e.getMessage());
logs.warning(e);
return false;
} finally {
try {
if (os != null) os.close();
if (process != null) process.destroy();
} catch (Exception ignored) {}
}
}
}

View File

@ -29,12 +29,18 @@ public class TransactionFinalizeBuilder {
private static final String TAG = "TransactionFinalizeBuilder";
private static final SimpleDateFormat SDF =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
private static final int ORDER_STATUS_FAILED = 0;
private static final int ORDER_STATUS_SUCCESS = 1;
private static final int ORDER_STATUS_PARTIAL_FAILED = 2;
/** 訂單建立時間(支付完成時的時間點) */
private final String orderMachineTime;
/** 機台本地流水號格式YYYYMMDDXXXX例如 202605110001 */
private final String flowId;
private String flowId;
/** 銷貨紀錄訂單號,預設同 flow_id特殊流程可覆寫 */
private String orderNo;
/** 累積的出貨結果清單 */
private final List<TransactionFinalizePayload.DispenseRecord> dispenseRecords = new ArrayList<>();
@ -45,9 +51,18 @@ public class TransactionFinalizeBuilder {
this.orderMachineTime = SDF.format(new Date());
// 在進入出貨流程時立即生成本次交易流水號一次交易只生成一次
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
this.orderNo = this.flowId;
Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId);
}
public void setFlowId(String flowId) {
this.flowId = flowId;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public void setCodeId(String codeId) {
this.codeId = codeId;
}
@ -112,25 +127,48 @@ public class TransactionFinalizeBuilder {
// --- 組裝 order ---
TransactionFinalizePayload.OrderPayload order = buildOrderPayload(flowInfo);
order.setDelivery_status(calculateOrderStatus());
payload.setOrder(order);
// --- 出貨記錄 ---
payload.setDispense(new ArrayList<>(dispenseRecords));
Log.i(TAG, "TransactionFinalizePayload built: flow_id=" + flowId
+ ", order_no=" + order.getOrder_no()
+ ", items=" + order.getItems().size()
+ ", dispense=" + dispenseRecords.size());
return payload;
}
private int calculateOrderStatus() {
boolean hasSuccess = false;
boolean hasFailed = false;
for (TransactionFinalizePayload.DispenseRecord record : dispenseRecords) {
if (record.getDispense_status() == 1) {
hasSuccess = true;
} else {
hasFailed = true;
}
}
if (hasSuccess && hasFailed) {
return ORDER_STATUS_PARTIAL_FAILED;
}
if (hasSuccess) {
return ORDER_STATUS_SUCCESS;
}
return ORDER_STATUS_FAILED;
}
/**
* MyApp 的全域狀態組裝 OrderPayload
*/
private TransactionFinalizePayload.OrderPayload buildOrderPayload(ReportFlowInfoStructure flowInfo) {
TransactionFinalizePayload.OrderPayload order = new TransactionFinalizePayload.OrderPayload();
order.setOrder_no(flowId);
order.setOrder_no(orderNo != null && !orderNo.isEmpty() ? orderNo : flowId);
// 金額資訊
int totalAmount = flowInfo.getTotalPrice();

View File

@ -33,8 +33,8 @@ import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.unibuy.smartdevice.AppCompatActivityAbstract;
import com.unibuy.smartdevice.AppEntry;
import com.unibuy.smartdevice.FragmentAbstract;
import com.unibuy.smartdevice.HomeActivity;
import com.unibuy.smartdevice.MainActivity;
import com.unibuy.smartdevice.MyApp;
@ -80,7 +80,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class FontendActivity extends AppCompatActivityAbstract {
public class FontendActivity extends AppCompatActivityAbstract implements SaleFlowCallback {
public enum Option {
TOAST(0),
BUY_DIALOG_OPEN(1),
@ -118,11 +118,11 @@ public class FontendActivity extends AppCompatActivityAbstract {
private Fragment activeFragment;
private Fragment vmcFragment;
private Fragment electricFragment;
private static ReportShotTimeByScheduler reportOnScheduler;
private ReportShotTimeByScheduler reportOnScheduler;
private ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;
private static final long IDLE_TIMEOUT = 5 * 60 * 1000; // 3 分鐘
private static final long IDLE_TIMEOUT = 5 * 60 * 1000; // 5 分鐘
private final long TIME_LIMIT = 200000;
private int clickCount = 0;
private long firstClickTime = 0;
@ -130,6 +130,18 @@ public class FontendActivity extends AppCompatActivityAbstract {
private Handler idleHandler = new Handler();
private Runnable idleRunnable;
private SaleFlowHandler saleFlowHandler;
@Override
public void onSaleFlowReset() {
getHandlerMain().start(getClass().getSimpleName(), Option.RESET_DATA.getOption(), "reset data");
}
@Override
public void onSaleFlowError(String message) {
getHandlerMain().start(getClass().getSimpleName(), Option.TOAST.getOption(), message);
}
// 遠端庫存更新監聽器輕量級只刷新 Adapter 不重建 Fragment
private final BroadcastReceiver slotUpdateReceiver = new BroadcastReceiver() {
@Override
@ -188,7 +200,11 @@ public class FontendActivity extends AppCompatActivityAbstract {
if (MyApp.getInstance().getBuyList().isEmpty()) {
start(getClass().getSimpleName(), getString(R.string.no_products_purchased));
} else {
new BuyDialog(context, this).show();
if (saleFlowHandler != null) {
saleFlowHandler.onBuyRequested();
} else {
new BuyDialog(context, this).show();
}
}
break;
case ADD_PRODUCT:
@ -247,7 +263,8 @@ public class FontendActivity extends AppCompatActivityAbstract {
// 第一層定時重啟到了整點就啟動等待模式RestartAPP2 自己會等閒置 >= 90 秒才真重啟
if (timeByM.equals("04:00") || timeByM.equals("08:00") || timeByM.equals("12:00")
|| timeByM.equals("16:00") || timeByM.equals("20:00") || timeByM.equals("23:59")) {
android.content.SharedPreferences prefs = getSharedPreferences("AppRestartPrefs", Context.MODE_PRIVATE);
android.content.SharedPreferences prefs = getSharedPreferences("AppRestartPrefs",
Context.MODE_PRIVATE);
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
String currentSlot = today + " " + timeByM;
String lastRestartSlot = prefs.getString("last_restart_slot", "");
@ -264,11 +281,11 @@ public class FontendActivity extends AppCompatActivityAbstract {
// 第二層閒置深度刷新任何時間閒置超過 30 分鐘就重啟 (暫時關閉改依賴定時重啟)
/*
if (MyApp.getInstance().getIdleSeconds() >= 30 * 60) {
getLogs().info("閒置超過 30 分鐘,執行深度排毒重啟...");
new RestartAPP2(this).start();
}
*/
* if (MyApp.getInstance().getIdleSeconds() >= 30 * 60) {
* getLogs().info("閒置超過 30 分鐘,執行深度排毒重啟...");
* new RestartAPP2(this).start();
* }
*/
// 這裡判斷每5分鐘去跑連線下位機的狀態
// String minutePart = timeByM.split(":")[1]; // 取得 "mm" 部分
@ -289,24 +306,24 @@ public class FontendActivity extends AppCompatActivityAbstract {
// new CMUHAutoRun(this).start();
}
getLogs().update("ThreadCount", String.valueOf(Tools.threadCount()));
String tc = getLogs().getMessage();
getLogs().update("ViceoCount", String.valueOf(VideoEoxPlayer.getExoPlayerTotal()));
String vc = getLogs().getMessage();
binding.textThreadCount.setText("TC:" + tc + " VC:" + vc);
getLogs().clearMessage();
// 直接使用區域變數更新 UI避免多執行緒 Race Condition 導致 getLogs().getMessage() 被改寫或清空而使 UI 變空
String threadCountStr = String.valueOf(Tools.threadCount());
String videoCountStr = String.valueOf(VideoEoxPlayer.getExoPlayerTotal());
binding.textThreadCount.setText("TC:" + threadCountStr + " VC:" + videoCountStr);
getLogs().update("ThreadCount", threadCountStr);
getLogs().update("ViceoCount", videoCountStr);
Tools.threadInfos();
String networkDevice = getNetworkDevice();
binding.textNetwork.setText(getString(R.string.network) + "" + networkDevice);
getLogs().update("Network", networkDevice);
binding.textNetwork.setText(getString(R.string.network) + "" + getLogs().getMessage());
getLogs().clearMessage();
DevXinYuanController.setVersion(ver);
String tempStr = DevXinYuanController.getTemperature() + "";
binding.textTemperature.setText(getString(R.string.temp) + ":" + tempStr);
getLogs().update("Temperature", String.valueOf(DevXinYuanController.getTemperature()), "");
binding.textTemperature.setText(getString(R.string.temp) + ":" + getLogs().getMessage());
getLogs().clearMessage();
try {
// httpAPI.detectHost();
@ -391,6 +408,10 @@ public class FontendActivity extends AppCompatActivityAbstract {
binding = ActivityFontendBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 初始化銷售流程處理器
saleFlowHandler = new SaleFlowHandler(this, getHandlerMain(), this);
saleFlowHandler.onSetupUI();
httpAPI = new HttpAPI(getHandlerMain());
this.connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
@ -416,7 +437,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
}
public void ReSetAPP() {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -429,7 +450,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
}
public void ReSetAPP2() {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -443,6 +464,10 @@ public class FontendActivity extends AppCompatActivityAbstract {
Process.killProcess(Process.myPid());
}
public SaleFlowHandler getSaleFlowHandler() {
return saleFlowHandler;
}
public void startOnSwitchFragment() {
try {
stopOnSwitchFragment(); // 先安全釋放舊播放器
@ -462,7 +487,8 @@ public class FontendActivity extends AppCompatActivityAbstract {
videoEoxPlayer = new VideoEoxPlayer(getCtx()); // 用新版建構子
getHandlerMain().getHandler().postDelayed(() -> {
if (isFinishing() || isDestroyed()) return;
if (isFinishing() || isDestroyed())
return;
if (videoEoxPlayer != null) {
// videoEoxPlayer.playMedia(binding.videoView, binding.imageAd); // 傳入 view
// 開始播放
@ -497,9 +523,9 @@ public class FontendActivity extends AppCompatActivityAbstract {
resetIdleTimer();
return;
}
getLogs().info("⚠️ 無操作 3 分鐘,自動返回首頁(清空購物車)");
getLogs().info("⚠️ 無操作 3 分鐘,自動返回初始畫面(清空購物車)");
MyApp.getInstance().getBuyList().clear(); // 🚩 逾時返回首頁時務必清空購物車避免機台進入永久忙碌狀態
Intent intent = new Intent(FontendActivity.this, HomeActivity.class);
Intent intent = new Intent(FontendActivity.this, AppEntry.initialActivity());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
@ -655,7 +681,11 @@ public class FontendActivity extends AppCompatActivityAbstract {
}
binding.floatButtonPickupCode.setOnClickListener(view -> {
new FontendPickupCodeDialog(getCtx(), getHandlerMain()).show();
if (saleFlowHandler != null) {
saleFlowHandler.onPickupRequested();
} else {
new FontendPickupCodeDialog(getCtx(), getHandlerMain()).show();
}
});
binding.floatButtonPickupCode.setVisibility(View.GONE);
@ -671,12 +701,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
binding.floatButtonShoppingCart.setVisibility(View.GONE);
}
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
}
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
startHeaderReportScheduler();
// binding.btnswitchlanTW.setOnClickListener(v -> switchLanguage("tw"));
//
@ -724,6 +749,20 @@ public class FontendActivity extends AppCompatActivityAbstract {
startOnSwitchFragment();
}
private void startHeaderReportScheduler() {
stopHeaderReportScheduler();
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
getHandlerMain().start(getClass().getSimpleName(), Option.REPORT_AND_SHOT_TIME.getOption(), "start");
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
}
private void stopHeaderReportScheduler() {
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
}
private void hiddenMultiLanguageButton() {
binding.btnswitchlanTW.setVisibility(View.GONE);
binding.btnswitchlanEn.setVisibility(View.GONE);
@ -811,7 +850,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
/**
* 現代化重啟不再使用 sleep 等待直接確認閒置秒數後執行真重啟
* - 只要閒置 >= 90 立刻啟動 HomeActivity killProcess
* - 只要閒置 >= 90 立刻啟動目前版本的初始畫面 killProcess
* - 若有人正在使用閒置 < 90 則等待 5 秒後再重新判斷最多等待 10 分鐘
*/
private class RestartAPP2 extends HandlerMainScheduler {
@ -835,7 +874,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
getLogs().info("RestartAPP2執行真重啟 (MQTT 優雅斷線模式)");
com.unibuy.smartdevice.external.mqtt.MqttManager.getInstance().gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
@ -881,7 +920,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
protected void execute(HandlerMain handlerMain) {
for (int testIndex = 0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("NfcCheckout testCount:" + testCount);
testCount++;
} else {
@ -965,7 +1004,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
Looper.prepare(); // 確保這個執行緒有 Looper
for (int testIndex = 0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("CMUHAutoRun testCount:" + testCount);
testCount++;
} else {
@ -1201,9 +1240,6 @@ public class FontendActivity extends AppCompatActivityAbstract {
}
}
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
stopHeaderReportScheduler();
}
}
}

View File

@ -0,0 +1,14 @@
package com.unibuy.smartdevice.ui;
/**
* SaleFlowHandler -> FontendActivity 的回呼介面
* Handler 完成特定工作後透過此介面通知 Activity 更新 UI 或狀態
*/
public interface SaleFlowCallback {
/** 當銷售流程完整結束(已出貨或取消),需要重置前台狀態 */
void onSaleFlowReset();
/** 當流程中需要顯示錯誤訊息Toast / Dialog */
void onSaleFlowError(String message);
}

View File

@ -193,7 +193,7 @@ public class ElectricFragment extends FragmentAbstract {
throw new RuntimeException(e);
}
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
try {
httpAPI.reportElectricInfo(electricInfoByJsonArray);
} catch (LogsParseException | LogsSettingEmptyException e) {

View File

@ -186,7 +186,7 @@ public class RecyclerElectricBuyListAdpter extends RecyclerView.Adapter<Recycler
public void onClick(View v) {
SlotStructure slotProduct = slotList.get(position);
if (slotProduct.isLock()) return;
if (MyApp.getInstance().isShowDialog()) return;
if (MyApp.getInstance().isMachineBusy()) return;
logs.info("marketingPlan:" + slotProduct.getProduct().getMarketingPlan());

View File

@ -156,7 +156,7 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
SlotStructure slotProduct = SlotList.get(position);
if (slotProduct.isLock())
return;
if (MyApp.getInstance().isShowDialog())
if (MyApp.getInstance().isMachineBusy())
return;
// 防呆正在檢查出貨口狀態時不允許再次點擊
if (isCheckingSlot)

View File

@ -24,6 +24,9 @@ import com.unibuy.smartdevice.structure.ProductStructure;
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 java.util.ArrayList;
import java.util.HashMap;
@ -38,6 +41,7 @@ public class VmcFragment extends FragmentAbstract {
ADD_PRODUCT_OPEN(3),
ADD_PRODUCT_CLOSE(4),
ADD_PRODUCT_FREE_GIFT(5),
DECRYPTION_ERROR(6),
;
private int option;
@ -129,6 +133,11 @@ public class VmcFragment extends FragmentAbstract {
sizeList.clear();
}
break;
case ADD_PRODUCT_FREE_GIFT:
break;
case DECRYPTION_ERROR:
super.execute(context, commandCode, "@掃碼失敗,請再掃描一次");
break;
}
}
};
@ -225,7 +234,16 @@ public class VmcFragment extends FragmentAbstract {
setUseStartOnSwitchFragment(false);
}
// 解決生命週期時序競爭VmcFragment 視圖加載完成後重新綁定大掃碼按鈕
if (getActivity() instanceof FontendActivity) {
com.unibuy.smartdevice.ui.SaleFlowHandler handler = ((FontendActivity) getActivity()).getSaleFlowHandler();
if (handler != null) {
handler.onSetupUI(root);
}
}
return root;
}
/**

View File

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.unibuy.smartdevice.AppCompatActivityAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.databinding.ActivityVmcSlotListBinding;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.ErrorCode;
@ -28,6 +29,9 @@ import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
import com.unibuy.smartdevice.ui.devs.electric.ElectricSlotListActivity;
import java.util.HashMap;
import java.util.Map;
public class VmcSlotListActivity extends AppCompatActivityAbstract {
private ActivityVmcSlotListBinding binding;
private ActivityResultLauncher<Intent> resultLauncher;
@ -146,6 +150,11 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
if (currentFocus != null) {
currentFocus.clearFocus();
}
String validationError = validateMaterialCodeConsistency();
if (!validationError.isEmpty()) {
getHandlerMain().start(getClass().getSimpleName(), "@" + validationError);
return;
}
// 1. 本地儲存
saveData();
@ -270,4 +279,18 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
getLogs().error(ErrorCode.NETWORK_UNKNOWN_ERROR, "同步啟動失敗: " + e.getMessage());
}
}
}
private String validateMaterialCodeConsistency() {
for (SlotStructure slot : MyApp.getInstance().getSlotList(SlotField.VMC.getField())) {
if (slot == null || slot.getProduct() == null) continue;
ProductStructure product = slot.getProduct();
String productId = product.getProductID() == null ? "" : product.getProductID().trim();
if (productId.isEmpty() || "?".equals(productId)) continue;
String materialCode = product.getMaterialCode() == null ? "" : product.getMaterialCode().trim();
if (materialCode.isEmpty()) {
return getString(R.string.vmc_material_code_empty, slot.getSlot(), productId);
}
}
return "";
}
}

View File

@ -23,6 +23,7 @@ import com.unibuy.smartdevice.controller.DevController;
import com.unibuy.smartdevice.controller.DevDigitalDisplayController;
import com.unibuy.smartdevice.controller.DevElectricController;
import com.unibuy.smartdevice.database.SlotsDao;
import com.unibuy.smartdevice.database.MemoDao;
import com.unibuy.smartdevice.databinding.DialogDispatchBinding;
import com.unibuy.smartdevice.devices.ComPort;
@ -43,6 +44,7 @@ import com.unibuy.smartdevice.structure.ProductStructure;
import com.unibuy.smartdevice.structure.ReportDispatchInfoStructure;
import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.structure.MemoStructure;
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.ToastHandlerMain;
@ -53,6 +55,7 @@ import com.unibuy.smartdevice.ui.tools.ImageGlide;
import com.unibuy.smartdevice.ui.tools.StringRefactoring;
import com.unibuy.smartdevice.worker.UploadLogOneTimeWorker;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -193,6 +196,7 @@ public class DispatchDialog extends DialogAbstract {
for (String logItem : deliveryLogs) {
getLogs().info(logItem);
}
finalizeFailedDispatch(message);
new Handler().postDelayed(() -> {
Context ctx = getContext();
@ -249,6 +253,9 @@ public class DispatchDialog extends DialogAbstract {
httpAPI = new HttpAPI(getHandlerMain());
// 初始化 Finalize Builder在支付完成進入出貨流程時建立
finalizeBuilder = new TransactionFinalizeBuilder();
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
MyApp.getInstance().getReportFlowInfoData().setFlowType(42);
}
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
@ -258,7 +265,7 @@ public class DispatchDialog extends DialogAbstract {
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
if (buyList.size() > 1) {
getLogs().info("⚠️ BuyList 有多筆,只處理第一筆,其餘略過");
getLogs().info("✅ BuyList 有多筆,將依序連續出貨");
}
for (BuyStructure buyStructure: buyList) {
@ -291,11 +298,59 @@ public class DispatchDialog extends DialogAbstract {
int pcount = buyStructure.getCount();
deliveryLogs.add("▲ 應出貨明細 - " + ptId + " / " + pName + " / 數量 " + pcount);
break; // 加入這行防呆只處理一筆後跳出
}
getLogs().info("DispatchList size:" + this.dispatchList.size());
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
try {
String patName = "未知患者";
String sn = "";
String drgNo = "";
// 優先從全域解密資料讀取最完整包含姓名與領藥號
String decryptedStr = MyApp.getInstance().getDecryptedData();
if (decryptedStr != null && !decryptedStr.isEmpty()) {
JSONObject fullJson = new JSONObject(decryptedStr);
patName = fullJson.optString("NAME", "");
if (patName.isEmpty()) patName = fullJson.optString("PATNAME", "");
if (patName.isEmpty()) patName = fullJson.optString("patName", "");
if (patName.isEmpty()) patName = "未知患者";
sn = fullJson.optString("SN", "");
drgNo = fullJson.optString("DRGNO", "");
}
// 如果全域資料讀不到嘗試從商品行銷計劃 (marketingPlan) 讀取 (備用防呆)
if (sn.isEmpty() && this.dispatchList != null && !this.dispatchList.isEmpty()) {
String marketingPlan = this.dispatchList.get(0).getProduct().getMarketingPlan();
if (marketingPlan != null && !marketingPlan.isEmpty()) {
JSONObject rxJson = new JSONObject(marketingPlan);
sn = rxJson.optString("SN", "");
if (drgNo.isEmpty()) {
drgNo = rxJson.optString("DRGNO", "");
}
}
}
// 按照最新要求[患者姓名]([藥單號]) 用括號
String barcodeInfo = patName + "(" + drgNo + ")";
getLogs().info("✅ 中國醫 QR Code 取物單資訊打包上報:姓名 " + patName + ", 藥單號 " + drgNo + " -> barcodeInfo: " + barcodeInfo);
// 設定到 flowBarCode (會連動到後台 member_barcode 欄位)
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(barcodeInfo);
// CMUH: flow_id 保留 App 機台流水號order_no 使用取物單序號 SN 方便後台對藥單
if (sn != null && !sn.isEmpty()) {
finalizeBuilder.setOrderNo(sn);
MyApp.getInstance().getReportFlowInfoData().setOrderId(sn);
getLogs().info("✅ 中國醫銷貨紀錄 order_no 已設定為取物單序號 (SN)" + sn);
}
} catch (Exception e) {
getLogs().warning(e);
}
}
this.recyclerDialogDispatchListAdpter = new RecyclerDialogDispatchListAdpter(getCtx(), this.dispatchList, getHandlerMain());
this.binding.recyclerDialogDispatchList.setAdapter(recyclerDialogDispatchListAdpter);
this.binding.recyclerDialogDispatchList.setLayoutManager(new LinearLayoutManager(context));
@ -452,17 +507,30 @@ public class DispatchDialog extends DialogAbstract {
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));//請檢查商品有無取出再確認取貨
}
} else {
if (checkSlotRunCount >= 3) {
boolean isCmuh = "Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project);
int maxRetries = isCmuh ? 10 : 3;
if (checkSlotRunCount >= maxRetries) {
buttonControlCountdown.shutdown();
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.slave_computer_did_not_respond_wait_3_seconds_try_again));//下位機無回應 請等3秒後再重試
if (isCmuh) {
getLogs().info("機台未回應,已達 Cmuh 最大重試次數 10 次,嘗試重新連線下位機");
MyApp.getInstance().getDevXinYuanController().start();
if (!MyApp.checkAndReconnectDev()) {
getLogs().info("無法連線到下位機,出貨作業中止");
}
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), "領藥有問題 請等3秒後再重掃");
} else {
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.slave_computer_did_not_respond_wait_3_seconds_try_again));
MyApp.getInstance().getDevXinYuanController().start();
}
checkSlotRunCount = 0;
MyApp.getInstance().getDevXinYuanController().start();
} else {
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot...");
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot..." + (isCmuh ? " (溫和模式,不發送指令)" : ""));
isCheckSlotByVMC = false; // 重置旗標允許下次回調進入
checkSlotRunCount++;
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(handlerMain, slot));
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
if (!isCmuh) {
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
}
}
}
}
@ -825,10 +893,13 @@ public class DispatchDialog extends DialogAbstract {
if (isFinalizing) return; // 防呆如果已經在結案倒數了直接跳出
isFinalizing = true;
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
saveData();
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
insertDrgnoToMemo();
}
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
saveData();
getLogs().info("📦 出貨狀況總結:");
for (String logItem : deliveryLogs) {
@ -883,6 +954,79 @@ public class DispatchDialog extends DialogAbstract {
isGetProductByVMC = false; // 解鎖出貨成功判定
}
private void finalizeFailedDispatch(String message) {
if (isFinalizing) {
return;
}
isFinalizing = true;
if (finalizeBuilder == null) {
getLogs().info("finalizeBuilder is null, cannot publish failed dispatch finalize: " + message);
return;
}
addFailedDispenseRecordsFromCurrent();
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project) && hasAnySuccessfulDispatch()) {
insertDrgnoToMemo();
} else if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
getLogs().info("CMUH 整單未出貨成功,不寫入 SN/DRGNO Memo允許重新掃碼領藥");
}
getLogs().info("出貨失敗,發送 MQTT Finalize 結案訊息: " + message);
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
saveData();
}
private void addFailedDispenseRecordsFromCurrent() {
if (dispatchList == null || dispatchList.isEmpty()) {
return;
}
int startIndex = Math.max(0, dispatchIndex);
for (int i = startIndex; i < dispatchList.size(); i++) {
DispatchStructure dispatch = dispatchList.get(i);
int failedQuantity = dispatch.getQuantity() - dispatch.getCount();
if (failedQuantity <= 0) {
continue;
}
ProductStructure product = dispatch.getProduct();
int productId = 0;
try {
productId = Integer.parseInt(product.getProductID());
} catch (Exception ignored) {
}
int remaining = 0;
try {
remaining = MyApp.getInstance().getSlotData(dispatch.getField(), dispatch.getSlot()).getCount();
} catch (LogsEmptyException e) {
getLogs().warning(e);
}
int amount = product.getRealPrice() * failedQuantity;
finalizeBuilder.addDispenseRecord(dispatch.getSlot(), productId, amount, 0, false, remaining);
getLogs().info("[Finalize] 已累積出貨失敗記錄: slot=" + dispatch.getSlot()
+ ", product=" + productId
+ ", failedQuantity=" + failedQuantity
+ ", remaining=" + remaining);
}
}
private boolean hasAnySuccessfulDispatch() {
if (dispatchList == null || dispatchList.isEmpty()) {
return false;
}
for (DispatchStructure dispatch : dispatchList) {
if (dispatch.getCount() > 0) {
return true;
}
}
return false;
}
private void deliverylot(int msgflag){
String productId = dispatchList.get(dispatchIndex).getProduct().getProductID();
@ -1054,4 +1198,48 @@ public class DispatchDialog extends DialogAbstract {
}
}
public void insertDrgnoToMemo() {
try {
/*刷藥單解碼後進來的json資料*/
JSONObject jsonObject = new JSONObject(dispatchList.get(0).getProduct().getMarketingPlan());
// **要新增的 ITEM 資料**
JSONObject newItem = new JSONObject();
newItem.put("SN", jsonObject.optString("SN")); // **領藥人藥單序號**
newItem.put("DRGNO", jsonObject.optString("DRGNO")); // **領藥人醫令**
/*從Memo的JSON資料中讀取SN及DRGNO的全部資料*/
String jsonStr = MyApp.getInstance().getMemoMap().get("CMUH").get(0).getData().toString();
JSONObject jsonObject2 = new JSONObject(jsonStr);
JSONArray itemArray = jsonObject2.getJSONArray("ITEM");
// **判斷是否已存在**
boolean exists = false;
for (int i = 0; i < itemArray.length(); i++) {
JSONObject item = itemArray.getJSONObject(i);
if (item.getString("SN").equals(newItem.getString("SN")) && item.getString("DRGNO").equals(newItem.getString("DRGNO"))) {
exists = true;
break; // **如果已存在就結束迴圈**
}
}
// **如果不存在才加入**
if (!exists) {
itemArray.put(newItem);
getLogs().info("✅ 領藥人醫令新增到Memo成功" + newItem.toString());
} else {
getLogs().info("⚠️ 領藥人醫令已存在,不新增!");
}
// **輸出結果**並新增到Memo SQLite資料庫 data欄位中
MyApp.getInstance().getMemoMap().get("CMUH").get(0).setData(jsonObject2);
// 寫入 SQLite
MemoStructure updatedMemo = MyApp.getInstance().getMemoMap().get("CMUH").get(0);
MemoDao memoDao = new MemoDao(getCtx());
memoDao.insertOnlyOne(updatedMemo);
memoDao.close();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -9,6 +9,7 @@ import androidx.work.WorkerParameters;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.external.HttpAPI;
import com.unibuy.smartdevice.external.StarCloudAPI;
import com.unibuy.smartdevice.tools.HttpConnect;
import com.unibuy.smartdevice.tools.Tools;
@ -32,31 +33,60 @@ public class UploadLogOneTimeWorker extends Worker {
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
logs.debug("背景工作執行失敗:沒有機器序號,使用暫時序號 00000000");
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/00000000/log/update";
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/00000000/log/update";
} else {
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
}
logs.debug("url:" + HttpAPI.updateOnDomainNameByNewHost);
logs.debug("url:" + StarCloudAPI.updateOnDomainNameByNewHost);
File txtDebug = new File(Tools.getExternalFilesDir(), "debug.txt");
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
if (yyyyMMddHHmmByNow.length() == 12) {
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
}
File newFile = new File(Tools.getExternalFilesDir(), yyyyMMddHHmmByNow + ".txt");
// 1. 旋轉目前的 debug.txt
if (txtDebug.exists() && txtDebug.length() > 0) {
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
if (yyyyMMddHHmmByNow.length() == 12) {
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
}
// 加上毫秒時間戳記確保檔名在任何情況下都唯一防止 renameTo 失敗
String uniqueFileName = yyyyMMddHHmmByNow + "_" + System.currentTimeMillis() + ".txt";
File newFile = new File(Tools.getExternalFilesDir(), uniqueFileName);
if (txtDebug.exists()) {
boolean renamed = txtDebug.renameTo(newFile);
logs.debug("newFile:" + newFile.getAbsolutePath() + " renamed:" + renamed);
if (renamed) {
HttpConnect httpConnect = new HttpConnect();
httpConnect.uploadFile(HttpAPI.updateOnDomainNameByNewHost, newFile);
if (!renamed) {
logs.debug("重新命名日誌檔失敗,將重試。");
return Result.retry();
}
}
return Result.success();// 工作成功
// 2. 掃描 ExternalFilesDir 批次補上傳非 active 的所有 txt 日誌檔
File dir = Tools.getExternalFilesDir();
boolean anyUploadFailed = false;
if (dir != null && dir.exists()) {
File[] files = dir.listFiles((d, name) -> name.endsWith(".txt")
&& !name.equals("debug.txt")
&& !name.equals("crash_pending.txt"));
if (files != null && files.length > 0) {
logs.debug("發現待上傳歷史日誌共 " + files.length + " 個,開始批次上傳。");
HttpConnect httpConnect = new HttpConnect();
for (File file : files) {
boolean success = httpConnect.uploadFile(StarCloudAPI.updateOnDomainNameByNewHost, file);
if (!success) {
logs.debug("檔案上傳失敗,保留至下次重試:" + file.getName());
anyUploadFailed = true;
}
}
}
}
if (anyUploadFailed) {
logs.debug("部分歷史日誌上傳失敗,排程重試中。");
return Result.retry();
}
return Result.success();
}
}

View File

@ -9,6 +9,7 @@ import androidx.work.WorkerParameters;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.exception.Logs;
import com.unibuy.smartdevice.external.HttpAPI;
import com.unibuy.smartdevice.external.StarCloudAPI;
import com.unibuy.smartdevice.tools.HttpConnect;
import com.unibuy.smartdevice.tools.Tools;
@ -39,7 +40,7 @@ public class UploadLogPeriodicWorker extends Worker {
public Result doWork() {
logs.debug("進入背景工作:"+getClass().getSimpleName()+" "+getId());
if (MyApp.getInstance().isShowDialog()) {
if (MyApp.getInstance().isMachineBusy()) {
logs.debug("系統使用中,延後處理備份事項");
return Result.retry();
@ -47,33 +48,60 @@ public class UploadLogPeriodicWorker extends Worker {
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
logs.debug("背景工作執行失敗:沒有機器序號,使用暫時序號 00000000");
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/00000000/log/update";
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/00000000/log/update";
} else {
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
}
logs.debug("url:" + HttpAPI.updateOnDomainNameByNewHost);
logs.debug("url:" + StarCloudAPI.updateOnDomainNameByNewHost);
File txtDebug = new File(Tools.getExternalFilesDir(), "debug.txt");
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
if (yyyyMMddHHmmByNow.length() == 12) {
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
}
File newFile = new File(Tools.getExternalFilesDir(), yyyyMMddHHmmByNow + ".txt");
// 1. 旋轉目前的 debug.txt
if (txtDebug.exists() && txtDebug.length() > 0) {
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
if (yyyyMMddHHmmByNow.length() == 12) {
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
}
// 加上毫秒時間戳記確保檔名在任何情況下都唯一防止 renameTo 失敗
String uniqueFileName = yyyyMMddHHmmByNow + "_" + System.currentTimeMillis() + ".txt";
File newFile = new File(Tools.getExternalFilesDir(), uniqueFileName);
if (txtDebug.exists()) {
boolean renamed = txtDebug.renameTo(newFile);
logs.debug("newFile:" + newFile.getAbsolutePath() + " renamed:" + renamed);
if (renamed) {
HttpConnect httpConnect = new HttpConnect();
httpConnect.uploadFile(HttpAPI.updateOnDomainNameByNewHost, newFile);
} else {
if (!renamed) {
logs.debug("重新命名日誌檔失敗,將重試。");
return Result.retry();
}
}
// 2. 掃描 ExternalFilesDir 批次補上傳非 active 的所有 txt 日誌檔
File dir = Tools.getExternalFilesDir();
boolean anyUploadFailed = false;
if (dir != null && dir.exists()) {
File[] files = dir.listFiles((d, name) -> name.endsWith(".txt")
&& !name.equals("debug.txt")
&& !name.equals("crash_pending.txt"));
if (files != null && files.length > 0) {
logs.debug("發現待上傳歷史日誌共 " + files.length + " 個,開始批次上傳。");
HttpConnect httpConnect = new HttpConnect();
for (File file : files) {
boolean success = httpConnect.uploadFile(StarCloudAPI.updateOnDomainNameByNewHost, file);
if (!success) {
logs.debug("檔案上傳失敗,保留至下次重試:" + file.getName());
anyUploadFailed = true;
}
}
}
}
if (anyUploadFailed) {
logs.debug("部分歷史日誌上傳失敗,排程重試中。");
return Result.retry();
}
return Result.success();
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 背景透明 -->
<solid android:color="@color/transparent" />
<!-- 外框線設定 -->
<stroke
android:width="2dp"
android:color="@color/outline" />
<!-- 圓角設定 (與輸入框 bg_input_modern 的 12dp 圓角一致) -->
<corners android:radius="12dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 主色背景 -->
<solid android:color="@color/primary" />
<!-- 圓角設定 (與輸入框 bg_input_modern 的 12dp 圓角一致) -->
<corners android:radius="12dp" />
</shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/surface" />
<stroke android:width="1.5dp" android:color="@color/outlineVariant" />
<corners android:radius="12dp" />
<padding
android:left="16dp"
android:top="12dp"
android:right="16dp"
android:bottom="12dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -107,8 +107,8 @@
<TextView
android:id="@+id/textNetwork"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_height="match_parent"
android:gravity="center_vertical|end"
android:text="@string/no_internet"
android:textColor="@color/white"
android:textSize="18sp"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="36dp"
android:background="@color/surface">
<!-- 標題 -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登入後台管理帳號"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/onSurface"
android:layout_marginBottom="24dp" />
<!-- 帳號標籤 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="管理員帳號"
android:textSize="22sp"
android:textColor="@color/onSurfaceVariant"
android:layout_marginBottom="8dp"
android:layout_marginStart="4dp" />
<!-- 帳號輸入框 -->
<EditText
android:id="@+id/etAccess"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="請輸入帳號"
android:textSize="24sp"
android:background="@drawable/bg_input_modern"
android:singleLine="true"
android:padding="16dp"
android:layout_marginBottom="24dp" />
<!-- 密碼標籤 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="管理員密碼"
android:textSize="22sp"
android:textColor="@color/onSurfaceVariant"
android:layout_marginBottom="8dp"
android:layout_marginStart="4dp" />
<!-- 密碼輸入框 -->
<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="請輸入密碼"
android:textSize="24sp"
android:background="@drawable/bg_input_modern"
android:inputType="textPassword"
android:singleLine="true"
android:padding="16dp"
android:layout_marginBottom="20dp" />
<!-- 記住帳號 -->
<CheckBox
android:id="@+id/cbRemember"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="記住帳號"
android:textSize="22sp"
android:textColor="@color/onSurfaceVariant"
android:layout_marginBottom="28dp" />
<!-- 按鈕區塊 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<!-- 取消按鈕 -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_outline"
android:text="取消"
android:textSize="22sp"
android:textColor="@color/outline"
android:paddingStart="40dp"
android:paddingEnd="40dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:layout_marginEnd="20dp"
android:textAllCaps="false" />
<!-- 登入按鈕 -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_primary"
android:text="登入"
android:textSize="22sp"
android:textColor="@color/white"
android:paddingStart="40dp"
android:paddingEnd="40dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:textAllCaps="false" />
</LinearLayout>
</LinearLayout>

View File

@ -101,4 +101,4 @@
android:textStyle="bold"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -390,7 +390,15 @@
<string name="there_is_something_at_the_pickup_port">There is something at the pickup port, please remove it first</string>
<string name="i_knew">knew</string>
<string name="input_vehicle">Input Vehicle</string>
<string name="reload_product">Reload Products</string>
<string name="reload_product_warning">This will download all products from the cloud and overwrite any unsaved local inventory changes. Are you sure you want to continue?</string>
<string name="sync_product">Reload Products</string>
<string name="sync_product_warning">This will download all products from the cloud and overwrite any unsaved local inventory changes. Are you sure you want to continue?</string>
<string name="downloading_product_data">Downloading product data...</string>
</resources>
<string name="cmuh_ordc_empty">Missing ORDC in prescription data. Please scan again or contact pharmacy staff.</string>
<string name="cmuh_material_not_found">Item [%1$s] has no assigned slot. Please contact pharmacy staff.</string>
<string name="cmuh_stock_empty">Item [%1$s] is currently out of stock. Please contact pharmacy staff.</string>
<string name="cmuh_stock_not_enough">Item [%1$s] has insufficient stock. Required: %2$d, Available: %3$d</string>
<string name="vmc_material_code_empty">MATERIAL_CODE_EMPTY Slot %1$d product %2$s is missing material code. Please fix product master data first.</string>
<string name="cmuh_pickup_failed_contact_counter">Pickup failed\nPlease contact the counter</string>
<string name="close_40">Close(40)</string>
<string name="close_countdown">Close(%1$d)</string>
</resources>

View File

@ -395,4 +395,12 @@
<string name="downloading_product_data">商品データを同期中...</string>
<string name="syncing">同期中... </string>
<string name="sync_completed">商品データの同期が完了しました</string>
</resources>
<string name="cmuh_ordc_empty">処方データに ORDC がありません。再度スキャンするか薬局窓口へお問い合わせください。</string>
<string name="cmuh_material_not_found">商品 [%1$s] はスロット未設定です。薬局窓口へお問い合わせください。</string>
<string name="cmuh_stock_empty">商品 [%1$s] は現在在庫切れです。薬局窓口へお問い合わせください。</string>
<string name="cmuh_stock_not_enough">商品 [%1$s] の在庫が不足しています。必要数: %2$d、在庫: %3$d</string>
<string name="vmc_material_code_empty">MATERIAL_CODE_EMPTY スロット %1$d の商品 %2$s に物料コードがありません。商品マスタを修正してください。</string>
<string name="cmuh_pickup_failed_contact_counter">受け取り失敗\nカウンターへお問い合わせください</string>
<string name="close_40">閉じる(40)</string>
<string name="close_countdown">閉じる(%1$d)</string>
</resources>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Default screen margins for large screens -->
<dimen name="activity_horizontal_margin">24dp</dimen>
<dimen name="activity_vertical_margin">24dp</dimen>
<dimen name="fab_margin">24dp</dimen>
<dimen name="bottom_nav_icon_size">68dp</dimen>
<dimen name="bottom_navigation_height">165dp</dimen>
<!-- 歡迎標題與登入區 (放大約 1.5 倍) -->
<dimen name="welcome_title_text_size">26sp</dimen>
<dimen name="welcome_hint_text_size">22sp</dimen>
<dimen name="login_btn_text_size">28sp</dimen>
<dimen name="header_logo_width">360dp</dimen>
<dimen name="header_logo_height">255dp</dimen>
<!-- 群組標題 (放大約 1.5 倍) -->
<dimen name="group_title_text_size">26sp</dimen>
<!-- 選單功能按鈕 (放大約 1.5 倍) -->
<dimen name="menu_btn_text_size">26sp</dimen>
<dimen name="menu_btn_icon_size">108dp</dimen>
<dimen name="menu_btn_inset">12dp</dimen>
<dimen name="menu_btn_corner_radius">24dp</dimen>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="black">#272727</color>
<color name="white">#FFFFFFFF</color>
<color name="red">#FFFF0000</color>
<color name="light_gray">#FFA9A9A9</color>
@ -8,4 +8,57 @@
<color name="transparent_black">#AA000000</color>
<color name="transparent_white">#80FFFFFF</color>
<color name="transparent">#00000000</color>
<!-- 全域系統色彩系統 (Material 3 標準色系 - 沉穩藍視角) -->
<!-- Primary 主色 (#005DAD) -->
<color name="primary">#005DAD</color>
<color name="onPrimary">#FFFFFF</color>
<color name="primaryContainer">#D1E4FF</color>
<color name="onPrimaryContainer">#001D36</color>
<!-- Secondary 次色 (藍灰色調) -->
<color name="secondary">#535F70</color>
<color name="onSecondary">#FFFFFF</color>
<color name="secondaryContainer">#D7E3F7</color>
<color name="onSecondaryContainer">#101C2B</color>
<!-- Semantic: Success 成功狀態 -->
<color name="success">#2E7D32</color>
<color name="onSuccess">#FFFFFF</color>
<color name="successContainer">#C8E6C9</color>
<color name="onSuccessContainer">#003300</color>
<!-- Semantic: Warning 警告狀態 -->
<color name="warning">#EF6C00</color>
<color name="onWarning">#FFFFFF</color>
<color name="warningContainer">#FFE0B2</color>
<color name="onWarningContainer">#4A2600</color>
<!-- Semantic: Error / Dangerous 錯誤與危險狀態 -->
<color name="error">#BA1A1A</color>
<color name="onError">#FFFFFF</color>
<color name="errorContainer">#FFDAD6</color>
<color name="onErrorContainer">#410002</color>
<!-- Semantic: Info 提示狀態 -->
<color name="info">#00639B</color>
<color name="onInfo">#FFFFFF</color>
<color name="infoContainer">#D1E4FF</color>
<color name="onInfoContainer">#001D35</color>
<!-- Background & Surface 背景與表面 (調整為微冷調白) -->
<color name="paperColor">#F5F7FA</color>
<color name="surface">#F8F9FF</color>
<color name="onSurface">#191C1E</color>
<color name="surfaceVariant">#DFE2EB</color>
<color name="onSurfaceVariant">#43474E</color>
<color name="background">#F8F9FF</color>
<color name="onBackground">#191C1E</color>
<!-- Outlines & Dividers 邊框與分隔線 -->
<color name="outline">#73777F</color>
<color name="outlineVariant">#C3C7CF</color>
<color name="divider">#DFE2EB</color>
</resources>

View File

@ -5,4 +5,20 @@
<dimen name="fab_margin">16dp</dimen>
<dimen name="bottom_nav_icon_size">45dp</dimen>
<dimen name="bottom_navigation_height">110dp</dimen>
<!-- 歡迎標題與登入區 -->
<dimen name="welcome_title_text_size">18sp</dimen>
<dimen name="welcome_hint_text_size">16sp</dimen>
<dimen name="login_btn_text_size">20sp</dimen>
<dimen name="header_logo_width">240dp</dimen>
<dimen name="header_logo_height">170dp</dimen>
<!-- 群組標題 -->
<dimen name="group_title_text_size">18sp</dimen>
<!-- 選單功能按鈕 -->
<dimen name="menu_btn_text_size">18sp</dimen>
<dimen name="menu_btn_icon_size">72dp</dimen>
<dimen name="menu_btn_inset">8dp</dimen>
<dimen name="menu_btn_corner_radius">16dp</dimen>
</resources>

View File

@ -312,4 +312,12 @@
<string name="upload_success">資料上傳成功</string>
<string name="upload_failed">資料上傳失敗</string>
<string name="upload_to_backend_and_return">上傳後台並返回</string>
</resources>
<string name="cmuh_ordc_empty">領藥單缺少 ORDC請重新掃碼或洽藥局窗口。</string>
<string name="cmuh_material_not_found">商品 [%1$s] 未設定貨道,請洽藥局窗口。</string>
<string name="cmuh_stock_empty">商品 [%1$s] 目前無庫存,請洽藥局窗口。</string>
<string name="cmuh_stock_not_enough">商品 [%1$s] 庫存不足,需求:%2$d可用%3$d</string>
<string name="vmc_material_code_empty">MATERIAL_CODE_EMPTY 貨道 %1$d 商品 %2$s 缺少物料編號,請先修正商品主檔。</string>
<string name="cmuh_pickup_failed_contact_counter">取物失敗\n請洽櫃台</string>
<string name="close_40">關閉(40)</string>
<string name="close_countdown">關閉(%1$d)</string>
</resources>