[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)
90
.agents/rules/versioning.md
Normal 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`),以便日後追溯特定現場機台的版本程式碼。
|
||||
@ -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
|
||||
|
||||
135
.agents/workflows/release-apk.md
Normal 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
@ -9,3 +9,6 @@
|
||||
.cxx
|
||||
local.properties
|
||||
/outputs
|
||||
/.antigravitycli/
|
||||
*.db
|
||||
mydata.db
|
||||
|
||||
2
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
"java.configuration.updateBuildConfiguration": "automatic"
|
||||
}
|
||||
31
AGENTS.md
Normal 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` 僅作導覽與快速摘要,不作為完整規範來源。
|
||||
@ -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"
|
||||
// =====================================================================
|
||||
// 雙維度 Flavor:project(客戶專案)× 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 {
|
||||
|
||||
@ -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() {
|
||||
// 晟崴版不處理取貨碼
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
444
app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
271
app/src/Cmuh/res/layout/activity_fontend.xml
Normal 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>
|
||||
198
app/src/Cmuh/res/layout/dialog_cmuh_take_medicine.xml
Normal 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>
|
||||
104
app/src/Cmuh/res/layout/dialog_pickup_code_buy.xml
Normal 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>
|
||||
194
app/src/Cmuh/res/layout/dialog_pickup_code_fontend.xml
Normal 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>
|
||||
87
app/src/Cmuh/res/layout/dialog_pickup_error_tip.xml
Normal 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>
|
||||
39
app/src/Cmuh/res/layout/fragment_vmc.xml
Normal 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>
|
||||
109
app/src/Cmuh/res/layout/recycler_dialog_pickup_code_buy_list.xml
Normal 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>
|
||||
102
app/src/Cmuh/res/layout/recycler_dialog_take_medicine_list.xml
Normal 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>
|
||||
@ -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() {
|
||||
// 通用版不處理取貨碼
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
16
app/src/main/java/com/unibuy/smartdevice/AppEntry.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
134
app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java
Normal 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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
12
app/src/main/res/drawable/bg_button_outline.xml
Normal 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>
|
||||
8
app/src/main/res/drawable/bg_button_primary.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/bg_input_modern.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/binding_machine.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
app/src/main/res/drawable/card_reader_checkout.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
app/src/main/res/drawable/card_reader_reboot.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
app/src/main/res/drawable/cargo_lane_management.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
app/src/main/res/drawable/product_list.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
app/src/main/res/drawable/reboot_app.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
app/src/main/res/drawable/sales_page.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
app/src/main/res/drawable/scanner_test.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
app/src/main/res/drawable/system_settings.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
app/src/main/res/drawable/taiwan_star_tech_logo.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 3.8 MiB |
BIN
app/src/main/res/drawable/view_log_records.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
@ -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"
|
||||
|
||||
114
app/src/main/res/layout/dialog_admin_login.xml
Normal 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>
|
||||
@ -101,4 +101,4 @@
|
||||
android:textStyle="bold"/>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
25
app/src/main/res/values-sw720dp/dimens.xml
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||