From e15d0209c6261e68603c1c7b56b9e108005f429a Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 22 May 2026 16:04:52 +0800 Subject: [PATCH 01/15] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=E9=81=A0?= =?UTF-8?q?=E7=AB=AF=20MQTT=20=E6=87=89=E7=94=A8=E7=A8=8B=E5=BC=8F=20OTA?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=E8=88=87=E9=9B=99?= =?UTF-8?q?=E7=89=88=E8=99=9F=E5=8B=95=E6=85=8B=E8=A8=88=E7=AE=97=E6=A9=9F?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 OtaManager.java:實現背景下載 APK 檔案至應用程式私有快取空間,並透過 Root 權限 (su 與 pm install -r) 進行靜默覆蓋安裝的完整邏輯。 2. 修改 MqttService.java:支援 update_app MQTT 指令,解析 payload 中的 url 參數並啟動 OtaManager 進行 OTA 更新。 3. 修改 app/build.gradle:實作雙版號動態計算機制,定義 verMajor、verMinor、verPatch,以符合實體機台的 OTA 覆蓋安裝規範。 4. 新增 .agents/rules/versioning.md:制訂專案版號與發版規範。 5. 修改 .agents/workflows/compiler-apk.md:調整編譯工作流,在編譯前自動檢查程式碼變更與遞增版號,並支援動態尋找編譯產出的最新 APK。 6. 修改 .gitignore:排除 .antigravitycli/ 工具產生的目錄。 7. 修改 .vscode/settings.json:設定自動更新 Java 的建置組態。 --- .agents/rules/versioning.md | 52 +++++++ .agents/workflows/compiler-apk.md | 18 ++- .gitignore | 2 + .vscode/settings.json | 2 +- app/build.gradle | 18 ++- .../smartdevice/service/MqttService.java | 22 +++ .../unibuy/smartdevice/tools/OtaManager.java | 132 ++++++++++++++++++ 7 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 .agents/rules/versioning.md create mode 100644 app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java diff --git a/.agents/rules/versioning.md b/.agents/rules/versioning.md new file mode 100644 index 0000000..67e50d5 --- /dev/null +++ b/.agents/rules/versioning.md @@ -0,0 +1,52 @@ +--- +trigger: always_on +--- + +# TaiwanStar 專案版號與發版規範 + +本規範定義了本專案(販賣機機台 App)的版號命名、自動化計算、實體機台 OTA 更新原則,以確保後續開發與發版時版本管理高度穩定。 + +## 1. 雙版號動態計算機制 (Double Versioning Scheme) + +本專案採用三段式版號變數進行自動化計算,所有配置皆於 `app/build.gradle` 頂部定義。 + +> [!IMPORTANT] +> **開發人員與 AI 助手在修改版號時,嚴禁直接寫死 `versionCode` 或 `versionName`,必須透過修改 Gradle 頂部的變數來進行。** + +* `verMajor` (大版號):代表重大架構變更或大型系統改版(例如 `10`)。 +* `verMinor` (次版號):代表新增商業功能、硬體支援,但向下相容(例如 `8`)。 +* `verPatch` (修補版號):代表 Bug 修復、小優化(例如 `5`)。 + +### 計算公式: +* **`versionCode`** = `verMajor * 10000 + verMinor * 100 + verPatch` +* **`versionName`** = `"${verMajor}_0${verMinor}_${verPatch}_R"` + +--- + +## 2. 實體機台 OTA 自動更新鐵律 + +因為本 App 部署於實體販賣機,更新主要透過後台進行遠端自動下載覆蓋安裝 (OTA)。 + +> [!CAUTION] +> **每次釋出任何新版 APK 至生產環境,`versionCode` 必須嚴格大於前一次發佈的數值!** +> * 若 `versionCode` 相同,即便 `versionName` 不同(例如從 `10_08_2_R` 改為 `10_08_5_R`),機台下載後也會因 `INSTALL_FAILED_ALREADY_EXISTS` 錯誤而**拒絕覆蓋安裝**,導致遠端自動升級完全失效。 + +--- + +## 3. 機台套件與產品風味 (Product Flavors) 命名定義 + +* **命名結構**:`[螢幕尺寸]_[Android版本]_[主板與機型規格]` +* **現行標準 Flavor**:`S_12_XY_U_Standard_` + * `S`:代表 Small 尺寸螢幕(一般 S 號)。 + * `12`:代表針對 Android 12 系統優化之版本。 + * `XY_U_Standard_`:代表 XY 主板、U 規格標準版。 +* **擴充原則**:未來引進新硬體時,必須新增 Flavor 並遵循此命名格式(例如:`M_12_XY_U_Standard_`)。 + +--- + +## 4. 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`),以便日後追溯特定現場機台的版本程式碼。 diff --git a/.agents/workflows/compiler-apk.md b/.agents/workflows/compiler-apk.md index 8a2b18b..57881b1 100644 --- a/.agents/workflows/compiler-apk.md +++ b/.agents/workflows/compiler-apk.md @@ -6,6 +6,12 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體 這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk` 時,AI 會自動為您執行以下步驟: +## 步驟零:自動檢查變更與升級版號 +在開始編譯之前,AI 助手會自動檢查是否有程式碼變更,以落實實體機台的 OTA 升級規範: +1. AI 助手將檢查 `git status --porcelain app/` 的輸出,以偵測是否有未提交的程式碼修改。 +2. 若偵測到變更,且本次任務尚未增加版號,AI 助手將自動讀取 `app/build.gradle` 並將其中的 `verPatch` 變數數值遞增 1(例如由 `5` 變更為 `6`)。 +3. 遞增完成後,向使用者呈報最新的版本號資訊,接著繼續執行步驟一。 + ## 步驟一:設定環境並編譯 Debug APK 使用 Java 17 與 Gradle 編譯專屬 Flavor (`S_12_XY_U_Standard_Debug`) 的程式碼。 @@ -29,11 +35,17 @@ 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 檔案,避免因版號變更導致寫死路徑失效 +APK_SOURCE=$(find app/build/outputs/apk/S_12_XY_U_Standard_/debug/ -name "app-S_12_XY_U_Standard_-*-debug.apk" | head -n 1) APK_DEST="outputs/TaiwanStar_debug.apk" -cp $APK_SOURCE $APK_DEST -echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST" +if [ -n "$APK_SOURCE" ]; then + cp "$APK_SOURCE" "$APK_DEST" + echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST (原始檔名: $(basename $APK_SOURCE))" +else + echo "❌ 找不到編譯產出的 APK 檔案,複製失敗!" + exit 1 +fi ``` ## 步驟三:安裝並啟動 App (如有連接設備) diff --git a/.gitignore b/.gitignore index 96d1b1f..110d88f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ .cxx local.properties /outputs +/.antigravitycli/ + diff --git a/.vscode/settings.json b/.vscode/settings.json index c5f3f6b..e0f15db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "automatic" } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index f151477..64d825f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,16 @@ plugins { alias(libs.plugins.android.application) } +// ========================================================================= +// 核心版號與自動連動定義 +// 每次發佈新版本給機台時,只需遞增 verPatch (修補版號)。 +// 例如:從 5 改為 6,對應的 versionName 就會自動變成 "10_08_6_R", +// versionCode 就會自動變大為 100806,以正確觸發實體機台的 OTA 自動更新。 +// ========================================================================= +def verMajor = 10 +def verMinor = 8 +def verPatch = 6 + android { namespace 'com.unibuy.smartdevice' compileSdk 35 @@ -10,8 +20,8 @@ android { applicationId "com.unibuy.smartdevice" minSdk 21 targetSdk 34 - versionCode 12 - versionName "1_1_${versionCode}" + versionCode verMajor * 10000 + verMinor * 100 + verPatch + versionName "${verMajor}_0${verMinor}_${verPatch}_R" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -22,8 +32,8 @@ android { productFlavors { S_12_XY_U_Standard_ { dimension "app_type" - versionCode 8 - versionName "10_0${versionCode}_2_R" + versionCode verMajor * 10000 + verMinor * 100 + verPatch + versionName "${verMajor}_0${verMinor}_${verPatch}_R" } } diff --git a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java index f2489c7..6fcd5e9 100644 --- a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java +++ b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java @@ -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"); diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java b/app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java new file mode 100644 index 0000000..a6c19d2 --- /dev/null +++ b/app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java @@ -0,0 +1,132 @@ +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); + + /** + * 啟動背景執行緒執行 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(15000); + connection.setReadTimeout(15000); + 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 " + apkPath); + process = Runtime.getRuntime().exec("su"); + os = new DataOutputStream(process.getOutputStream()); + os.writeBytes("pm install -r " + 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) {} + } + } +} From 6b3e05051ff6f26f70e8125dd310ff7347c8bdde Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 27 May 2026 10:16:56 +0800 Subject: [PATCH 02/15] =?UTF-8?q?[FIX]=20=E5=8D=87=E7=B4=9A=E8=A3=9C?= =?UTF-8?q?=E4=B8=81=E7=89=88=E8=99=9F=E8=87=B3=2010.8.8=20=E4=B8=A6?= =?UTF-8?q?=E5=84=AA=E5=8C=96=20APK=20=E8=87=AA=E5=8B=95=E7=B7=A8=E8=AD=AF?= =?UTF-8?q?=E8=88=87=E7=99=BC=E5=B8=83=E8=85=B3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 遞增 app/build.gradle 中的 verPatch 變數至 8(對應版本為 10_08_8_R,versionCode 為 100808),以利機台正常觸發 OTA 覆蓋安裝。 2. 重構 .agents/workflows/compiler-apk.md 腳本中的 APK 複製與命名機制,改為動態尋找並使用原始檔名,避免寫死檔名導致版本變更時失效。 3. 在編譯腳本中加入自動提取與計算版本資訊的輸出(包含 versionName 與 versionCode),方便在 StarCloud 後台登記時作為參考。 4. 更新 ADB 自動安裝路徑尋找方式,使其與動態尋找 APK 機制保持一致。 --- .agents/workflows/compiler-apk.md | 34 ++++++++++++++++++++++++++----- app/build.gradle | 2 +- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.agents/workflows/compiler-apk.md b/.agents/workflows/compiler-apk.md index 57881b1..318cbdf 100644 --- a/.agents/workflows/compiler-apk.md +++ b/.agents/workflows/compiler-apk.md @@ -37,11 +37,29 @@ cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai mkdir -p outputs # 動態尋找編譯產出的最新 APK 檔案,避免因版號變更導致寫死路徑失效 APK_SOURCE=$(find app/build/outputs/apk/S_12_XY_U_Standard_/debug/ -name "app-S_12_XY_U_Standard_-*-debug.apk" | head -n 1) -APK_DEST="outputs/TaiwanStar_debug.apk" 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 (原始檔名: $(basename $APK_SOURCE))" + echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST" + + # 提取與計算版本資訊 + 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}') + VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R" + VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH)) + FLAVOR="S_12_XY_U_Standard_" + + echo "==================================================" + echo "📢 後台更新 keyin 參考資料:" + echo "--------------------------------------------------" + echo "* 產品風味 (Flavor):$FLAVOR" + echo "* 版本名稱 (versionName):$VERSION_NAME" + echo "* 版本代碼 (versionCode):$VERSION_CODE" + echo "* APK 檔案名稱:$APK_NAME" + echo "==================================================" else echo "❌ 找不到編譯產出的 APK 檔案,複製失敗!" exit 1 @@ -60,9 +78,15 @@ DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | w 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-S_12_XY_U_Standard_-*-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 diff --git a/app/build.gradle b/app/build.gradle index 64d825f..9823ef5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { // ========================================================================= def verMajor = 10 def verMinor = 8 -def verPatch = 6 +def verPatch = 8 android { namespace 'com.unibuy.smartdevice' From 3409f93f0764d72572126e72b79402fac3a68272 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 27 May 2026 14:48:22 +0800 Subject: [PATCH 03/15] =?UTF-8?q?[REFACTOR]=20=E9=87=8D=E6=A7=8B=E9=8A=B7?= =?UTF-8?q?=E5=94=AE=E6=B5=81=E7=A8=8B=E8=87=B3=E9=9B=99=E7=B6=AD=E5=BA=A6?= =?UTF-8?q?=20Flavor=20=E6=9E=B6=E6=A7=8B=E3=80=81=E7=B5=B1=E4=B8=80?= =?UTF-8?q?=E5=BF=99=E7=A2=8C=E7=8B=80=E6=85=8B=E4=B8=A6=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E8=A6=8F=E7=AF=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 引進 SaleFlowHandler 與 SaleFlowCallback 介面,分離通用版 (General)、晟崴版 (Chengwai) 與中國醫版 (Cmuh) 的前台銷售進入點,降低 FontendActivity 耦合度。 2. 建立 src/Chengwai/、src/Cmuh/、src/General/ 目錄,將客製 dialog(如 FontendPickupCodeDialog、TakeMedicineDialog)與 layout 移入對應的專屬目錄。 3. 統一狀態檢查機制,將全專案背景排程、定時重啟與日誌備份中的舊 isShowDialog() 替換為更安全的 isMachineBusy();於 NFC 交易與鍵盤 Enter 防誤觸中特意保留原 isShowDialog() 以守護安全性防線。 4. 修正英文 values-en/strings.xml 缺漏與預設 locale 對齊的 sync_product/sync_product_warning,解決 lintVital 報告的 ExtraTranslation 錯誤,使 Release 打包 100% 成功。 5. 微調 app/build.gradle,將起點版號重置為 10_01_1,自動連動 versionCode 100101 與 versionName 10_01_1_R。 6. 修正 .agents/rules/versioning.md 規範,將 S 修正為「來源 (Source)」,將 verMajor 修正為「螢幕大小」,並補齊官方命名規範對照圖表與欄位解析。 7. 升級一鍵編譯 /compiler-apk(新增中文與英文別名智慧防呆映射)與新建發版 /release-apk 工作流。 --- .agents/rules/versioning.md | 58 +- .agents/workflows/compiler-apk.md | 79 ++- .agents/workflows/release-apk.md | 117 ++++ app/build.gradle | 42 +- .../smartdevice/ui/SaleFlowHandler.java | 38 ++ .../ui/dialog/BuyPickupCodeDialog.java | 0 .../ui/dialog/FontendPickupCodeDialog.java | 0 ...RecyclerDialogPickupCodeBuyListAdpter.java | 0 .../structure/MedicineStructure.java | 79 +++ .../smartdevice/ui/SaleFlowHandler.java | 387 ++++++++++++ .../ui/dialog/BuyPickupCodeDialog.java | 263 ++++++++ .../ui/dialog/FontendPickupCodeDialog.java | 429 +++++++++++++ .../ui/dialog/PickupErrorTipDialog.java | 175 ++++++ .../ui/dialog/TakeMedicineDialog.java | 273 ++++++++ ...RecyclerDialogPickupCodeBuyListAdpter.java | 193 ++++++ .../RecyclerDialogTakeMedicineListAdpter.java | 91 +++ .../res/layout/dialog_cmuh_take_medicine.xml | 198 ++++++ .../res/layout/dialog_pickup_code_buy.xml | 104 +++ .../res/layout/dialog_pickup_code_fontend.xml | 194 ++++++ .../res/layout/dialog_pickup_error_tip.xml | 84 +++ app/src/Cmuh/res/layout/fragment_vmc.xml | 40 ++ .../recycler_dialog_pickup_code_buy_list.xml | 109 ++++ .../recycler_dialog_take_medicine_list.xml | 102 +++ .../smartdevice/ui/SaleFlowHandler.java | 38 ++ .../ui/dialog/BuyPickupCodeDialog.java | 264 ++++++++ .../ui/dialog/FontendPickupCodeDialog.java | 591 ++++++++++++++++++ ...RecyclerDialogPickupCodeBuyListAdpter.java | 194 ++++++ .../com/unibuy/smartdevice/HomeActivity.java | 16 +- .../java/com/unibuy/smartdevice/MyApp.java | 60 ++ .../unibuy/smartdevice/database/MemoDao.java | 2 +- .../smartdevice/ui/FontendActivity.java | 34 +- .../smartdevice/ui/SaleFlowCallback.java | 14 + .../ui/devs/electric/ElectricFragment.java | 2 +- .../RecyclerElectricBuyListAdpter.java | 2 +- .../ui/devs/vmc/RecyclerVmcBuyListAdpter.java | 2 +- .../smartdevice/ui/devs/vmc/VmcFragment.java | 6 + .../worker/UploadLogPeriodicWorker.java | 2 +- app/src/main/res/values-en/strings.xml | 4 +- 38 files changed, 4238 insertions(+), 48 deletions(-) create mode 100644 .agents/workflows/release-apk.md create mode 100644 app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java rename app/src/{main => Chengwai}/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java (100%) rename app/src/{main => Chengwai}/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java (100%) rename app/src/{main => Chengwai}/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java (100%) create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/structure/MedicineStructure.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/PickupErrorTipDialog.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/TakeMedicineDialog.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java create mode 100644 app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogTakeMedicineListAdpter.java create mode 100644 app/src/Cmuh/res/layout/dialog_cmuh_take_medicine.xml create mode 100644 app/src/Cmuh/res/layout/dialog_pickup_code_buy.xml create mode 100644 app/src/Cmuh/res/layout/dialog_pickup_code_fontend.xml create mode 100644 app/src/Cmuh/res/layout/dialog_pickup_error_tip.xml create mode 100644 app/src/Cmuh/res/layout/fragment_vmc.xml create mode 100644 app/src/Cmuh/res/layout/recycler_dialog_pickup_code_buy_list.xml create mode 100644 app/src/Cmuh/res/layout/recycler_dialog_take_medicine_list.xml create mode 100644 app/src/General/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java create mode 100644 app/src/General/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java create mode 100644 app/src/General/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java create mode 100644 app/src/General/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java create mode 100644 app/src/main/java/com/unibuy/smartdevice/ui/SaleFlowCallback.java diff --git a/.agents/rules/versioning.md b/.agents/rules/versioning.md index 67e50d5..2fdf7ab 100644 --- a/.agents/rules/versioning.md +++ b/.agents/rules/versioning.md @@ -13,7 +13,7 @@ trigger: always_on > [!IMPORTANT] > **開發人員與 AI 助手在修改版號時,嚴禁直接寫死 `versionCode` 或 `versionName`,必須透過修改 Gradle 頂部的變數來進行。** -* `verMajor` (大版號):代表重大架構變更或大型系統改版(例如 `10`)。 +* **`verMajor` (螢幕大小)**:代表實體機台的螢幕吋數(例如 `10` 代表 10 吋螢幕)。 * `verMinor` (次版號):代表新增商業功能、硬體支援,但向下相容(例如 `8`)。 * `verPatch` (修補版號):代表 Bug 修復、小優化(例如 `5`)。 @@ -23,7 +23,29 @@ trigger: always_on --- -## 2. 實體機台 OTA 自動更新鐵律 +## 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)。 @@ -33,18 +55,34 @@ trigger: always_on --- -## 3. 機台套件與產品風味 (Product Flavors) 命名定義 +## 4. 雙維度產品風味 (Product Flavors) 與 APK 命名規範 -* **命名結構**:`[螢幕尺寸]_[Android版本]_[主板與機型規格]` -* **現行標準 Flavor**:`S_12_XY_U_Standard_` - * `S`:代表 Small 尺寸螢幕(一般 S 號)。 - * `12`:代表針對 Android 12 系統優化之版本。 - * `XY_U_Standard_`:代表 XY 主板、U 規格標準版。 -* **擴充原則**:未來引進新硬體時,必須新增 Flavor 並遵循此命名格式(例如:`M_12_XY_U_Standard_`)。 +本專案採用**雙維度 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` 自動連動生成,**嚴禁手動修改輸出檔名**。 --- -## 4. Git 發版配合流程 +## 5. Git 發版配合流程 1. 自 `develop` 拉出 `release/10.8.5` 發版分支。 2. 遞增 `app/build.gradle` 中的 `verPatch` 變數並 Commit。 diff --git a/.agents/workflows/compiler-apk.md b/.agents/workflows/compiler-apk.md index 318cbdf..4ee952b 100644 --- a/.agents/workflows/compiler-apk.md +++ b/.agents/workflows/compiler-apk.md @@ -6,6 +6,12 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體 這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk` 時,AI 會自動為您執行以下步驟: +> [!TIP] +> **支援參數化指定編譯變體:** +> * 預設(不帶參數):編譯 **`Chengwai` (晟崴客製版)**。 +> * 您可以呼叫 `/compiler-apk General`(或 `general`)編譯 **`General` (通用版)**。 +> * 您可以呼叫 `/compiler-apk Cmuh`(或 `cmuh`)編譯 **`Cmuh` (中國醫客製版)**。 + ## 步驟零:自動檢查變更與升級版號 在開始編譯之前,AI 助手會自動檢查是否有程式碼變更,以落實實體機台的 OTA 升級規範: 1. AI 助手將檢查 `git status --porcelain app/` 的輸出,以偵測是否有未提交的程式碼修改。 @@ -13,7 +19,7 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體 3. 遞增完成後,向使用者呈報最新的版本號資訊,接著繼續執行步驟一。 ## 步驟一:設定環境並編譯 Debug APK -使用 Java 17 與 Gradle 編譯專屬 Flavor (`S_12_XY_U_Standard_Debug`) 的程式碼。 +使用 Java 17 與 Gradle 編譯對應 Flavor 與硬體規格的 Debug 程式碼。 ```bash // turbo @@ -23,9 +29,37 @@ 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 + # 若非預設之中文別名,則嘗試做首字母大寫轉換(相容英文輸入,如 cmuh -> Cmuh) + TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}') + 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}S12XYUDebug" + # 使用 clean 確保完整重新編譯,避免舊快取遮蔽 import 缺失等錯誤 # --no-daemon:強制使用單次 JVM 進程,避免 Daemon 初始化期間被殘留訊號中斷 -./gradlew clean :app:assembleS_12_XY_U_Standard_Debug --no-daemon +./gradlew clean :app:assemble${TARGET_FLAVOR}S12XYUDebug --no-daemon ``` ## 步驟二:導出 APK 到 outputs 資料夾 @@ -35,8 +69,26 @@ cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai // turbo cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai mkdir -p outputs + +# 取得 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 + # 動態尋找編譯產出的最新 APK 檔案,避免因版號變更導致寫死路徑失效 -APK_SOURCE=$(find app/build/outputs/apk/S_12_XY_U_Standard_/debug/ -name "app-S_12_XY_U_Standard_-*-debug.apk" | head -n 1) +APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}S12XYU/debug/ -name "app-S_12_XY_U_${TARGET_FLAVOR}-*-debug.apk" | head -n 1) if [ -n "$APK_SOURCE" ]; then APK_NAME=$(basename "$APK_SOURCE") @@ -50,7 +102,7 @@ if [ -n "$APK_SOURCE" ]; then 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="S_12_XY_U_Standard_" + FLAVOR="${TARGET_FLAVOR}S12XYU" echo "==================================================" echo "📢 後台更新 keyin 參考資料:" @@ -73,12 +125,29 @@ fi // 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 + # 檢查是否有設備連接 DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | wc -l) if [ "$DEVICE_COUNT" -gt 0 ]; then echo "📱 偵測到 $DEVICE_COUNT 個設備,開始安裝..." - APK_TARGET=$(find outputs/ -name "app-S_12_XY_U_Standard_-*-debug.apk" | head -n 1) + APK_TARGET=$(find outputs/ -name "app-S_12_XY_U_${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..." diff --git a/.agents/workflows/release-apk.md b/.agents/workflows/release-apk.md new file mode 100644 index 0000000..074916a --- /dev/null +++ b/.agents/workflows/release-apk.md @@ -0,0 +1,117 @@ +--- +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 + +echo "📦 [Release] 開始編譯正式版變體: ${TARGET_FLAVOR}S12XYURelease" + +# 使用 clean 確保完整重新編譯,避開 incremental 快取問題 +./gradlew clean :app:assemble${TARGET_FLAVOR}S12XYURelease --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 + +# 動態尋找編譯產出的最新正式版 APK 檔案 +APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}S12XYU/release/ -name "app-S_12_XY_U_${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 "==================================================" + + # 提取與計算版本資訊 + 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}') + VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R" + VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH)) + FLAVOR="${TARGET_FLAVOR}S12XYU" + + 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 "==================================================" +``` diff --git a/app/build.gradle b/app/build.gradle index 9823ef5..e2c31e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ plugins { // versionCode 就會自動變大為 100806,以正確觸發實體機台的 OTA 自動更新。 // ========================================================================= def verMajor = 10 -def verMinor = 8 -def verPatch = 8 +def verMinor = 1 +def verPatch = 1 android { namespace 'com.unibuy.smartdevice' @@ -26,14 +26,26 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - // 設定不同場域 - flavorDimensions "app_type" + // ===================================================================== + // 雙維度 Flavor:project(客戶專案)× hardware(硬體規格) + // ===================================================================== + flavorDimensions "project", "hardware" productFlavors { - S_12_XY_U_Standard_ { - dimension "app_type" - versionCode verMajor * 10000 + verMinor * 100 + verPatch - versionName "${verMajor}_0${verMinor}_${verPatch}_R" + General { + dimension "project" + } + + Chengwai { + dimension "project" + } + + Cmuh { + dimension "project" + } + + S12XYU { + dimension "hardware" } } @@ -45,16 +57,20 @@ android { } } - // 自定義打包檔案名稱,包含版本號 + // 自定義打包檔案名稱,遵循公司命名規範:來源_安卓號_主板號_機器系列_專案名稱_螢幕大小_次版號_修補版號_環境 applicationVariants.all { variant -> variant.outputs.each { output -> - def versionCode = variant.versionCode def versionName = variant.versionName - def flavor = variant.productFlavors[0].name + def project = variant.productFlavors[0].name // project dimension + def hardware = variant.productFlavors[1].name // hardware dimension def buildType = variant.buildType.name - // 設定打包檔案名稱:app---.apk - output.outputFileName = "app-${flavor}-${versionName}-${buildType}.apk" + // 將 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}-${versionName}-${buildType}.apk" } } diff --git a/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java new file mode 100644 index 0000000..2ffca5e --- /dev/null +++ b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java @@ -0,0 +1,38 @@ +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 onBuyRequested() { + new MemberCardPickupDialog(context, handlerMain).show(); + } + + public void onBarcodeReceived(String rawData) { + // 晟崴版不處理掃碼 + } + + public void onPickupRequested() { + // 晟崴版不處理取貨碼 + } +} diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java similarity index 100% rename from app/src/main/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java rename to app/src/Chengwai/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java similarity index 100% rename from app/src/main/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java rename to app/src/Chengwai/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java b/app/src/Chengwai/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java similarity index 100% rename from app/src/main/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java rename to app/src/Chengwai/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/structure/MedicineStructure.java b/app/src/Cmuh/java/com/unibuy/smartdevice/structure/MedicineStructure.java new file mode 100644 index 0000000..2392dda --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/structure/MedicineStructure.java @@ -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; + } +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java new file mode 100644 index 0000000..e11e04b --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/SaleFlowHandler.java @@ -0,0 +1,387 @@ +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.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 javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +/** + * Cmuh (中國醫版) 銷售流程處理器。 + * 負責: + * 1. 綁定「40秒內刷條碼」按鈕 (scanbarcodebtn) + * 2. 連接 USB 掃描器背景讀取 + * 3. 解密與解析醫令 (AES 128 ECB) + * 4. 庫存核對與自動加入購物車 + * 5. 顯示領藥與錯誤 Dialog + */ +public class SaleFlowHandler { + + 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() { + if (context instanceof Activity) { + View btnScan = ((Activity) context).findViewById(R.id.scanbarcodebtn); + if (btnScan != null) { + btnScan.setOnClickListener(new BtnQrCodeOnClick(handlerMain, context)); + } + } + } + + 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; + 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()); + + String decryptedStr = MyApp.getInstance().getDecryptedData(); + MyApp.getInstance().getLogs().info("decryptedStr=" + decryptedStr); + + JSONObject jsonObject = new JSONObject(decryptedStr); + addMedicineToByulist(jsonObject); + checkORDCDispatchNumber(decryptedStr); // 判斷領藥號是否已經領過 + + } catch (Exception e) { + throw new LogsParseException(MyApp.getInstance().getLogs(), "錯誤:加密數據長度不是 16 的倍數,可能需要填充"); + } + return haveValuestate; + } + + public void addMedicineToByulist(JSONObject jsonObject) { + JSONArray itemsArray = null; + try { + itemsArray = jsonObject.getJSONArray("ITEM"); + // 清空 BuyList,確保每次掃描都是新的出貨清單 + MyApp.getInstance().getBuyList().clear(); + + int MedicineCount = 0; // 同一種商品全部貨道的總數量 + String MedicineName = "", MedicineSlot = "", MedicineImg = "", MedicinePId = "", MemoSnDrgno = ""; + ArrayList medicineList = new ArrayList<>(); + + // 先進行預判,確認所有 ITEM 都能成功出貨才繼續後面流程 + boolean stockCheckPassed = true; // 預設檢查通過 + + for (int i = 0; i < itemsArray.length(); i++) { + JSONObject itemObject = itemsArray.getJSONObject(i); + String ordc = itemObject.optString("ORDC"); + int requestQty = Integer.parseInt(itemObject.optString("QTY")); + int totalStock = 0; + + for (int j = 0; j < MyApp.getInstance().getSlotList(0).size(); j++) { + if (ordc.equals(MyApp.getInstance().getSlotList(0).get(j).getProduct().getMaterialCode()) + && MyApp.getInstance().getSlotList(0).get(j).getCount() > 0 + && MyApp.getInstance().getSlotList(0).get(j).getCount() <= 20) { + totalStock += MyApp.getInstance().getSlotList(0).get(j).getCount(); + } + } + + if (totalStock == 0) { + MyApp.getInstance().getLogs().info("預判失敗:ORDC = " + ordc + " 找不到貨道或庫存為0"); + String storename = "@商品 [" + ordc + "] 無庫存或未設定貨道,請確認。"; + handlerMain.start("無庫存或未設定貨道,請確認", storename); + stockCheckPassed = false; + break; + } else if (totalStock < requestQty) { + MyApp.getInstance().getLogs().info("預判失敗:ORDC = " + ordc + " 庫存不足,總庫存:" + totalStock + ",需求:" + requestQty); + String storename = "@商品 [" + ordc + "] 庫存不足,請補貨後再掃描。"; + handlerMain.start("庫存不足", storename); + stockCheckPassed = false; + break; + } + } + + if (!stockCheckPassed) { + return; // 直接結束,不進入後續流程 + } + + MyApp.getInstance().getLogs().info("itemsArray.length" + itemsArray.length()); + for (int i = 0; i < itemsArray.length(); i++) { + JSONObject itemObject = itemsArray.getJSONObject(i); + + // 重新初始化這些變數 + MedicineName = ""; + MedicineCount = 0; + MedicineSlot = ""; + MedicineImg = ""; + MedicinePId = ""; + MemoSnDrgno = ""; + + for (int j = 0; j < MyApp.getInstance().getSlotList(0).size(); j++) { + if (itemObject.optString("ORDC").equals(MyApp.getInstance().getSlotList(0).get(j).getProduct().getMaterialCode()) + && MyApp.getInstance().getSlotList(0).get(j).getCount() > 0 + && MyApp.getInstance().getSlotList(0).get(j).getCount() <= 20) { + MedicineCount += MyApp.getInstance().getSlotList(0).get(j).getCount(); + MedicineSlot += MyApp.getInstance().getSlotList(0).get(j).getSlot() + "-" + MyApp.getInstance().getSlotList(0).get(j).getCount() + ","; + + if (MedicineCount >= Integer.valueOf(itemObject.optString("QTY"))) { + MedicinePId = MyApp.getInstance().getSlotList(0).get(j).getProduct().getProductID(); + MedicineName = MyApp.getInstance().getSlotList(0).get(j).getProduct().getProductName(); + MedicineImg = MyApp.getInstance().getSlotList(0).get(j).getProduct().getProductImg(); + MemoSnDrgno = "{\"SN\": \"" + jsonObject.optString("SN") + "\", \"DRGNO\": \"" + jsonObject.optString("DRGNO") + "\"}"; + } + } + } + medicineList.add(new MedicineStructure(itemObject.optString("ORDC"), MedicineCount, MedicineName, MedicineSlot, MedicineImg, MedicinePId, MemoSnDrgno)); + } + + /* 這裡將商品依據領藥數量加入 BuyList */ + for (int i = 0; i < itemsArray.length(); i++) { + JSONObject itemObject = itemsArray.getJSONObject(i); + int requestQty = Integer.parseInt(itemObject.optString("QTY")); + + 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); + } + } + + public void addMedicineToBuyList(String MedicinePidStr, String MedicineNameStr, int SlotNumber, String MedicineImg, String MemoSnDrgno, int count) { + ProductStructure productStructure = new ProductStructure(MedicinePidStr, MedicineImg, MedicineNameStr, 0, 0, 0, 0, MemoSnDrgno, "", "", ""); + MyApp.getInstance().getLogs().info("aaaaaa " + MedicinePidStr + " " + MemoSnDrgno + " " + MedicineNameStr); + BuyStructure buyProduct = new BuyStructure(0, SlotNumber, productStructure, count); + MyApp.getInstance().getBuyList().add(buyProduct); + } + + public void 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; + handlerMain.start(getClass().getSimpleName(), "@此單已取物完成,請洽藥局發藥窗口!"); + MyApp.getInstance().getBuyList().clear(); + } + } +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java new file mode 100644 index 0000000..c88d5fa --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/BuyPickupCodeDialog.java @@ -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 optionMap = new HashMap<>(); + static { + for (Option option : Option.values()) { + optionMap.put(option.getOption(), option); + } + } + + @Override + protected Context setCtx() { + return getContext(); + } + + @Override + protected Class setCls() { + return getClass(); + } + + @Override + protected HandlerMain setHandlerMain() { + return new ToastHandlerMain(getContext(), getLogs()) { + @Override + protected void execute(Context context, int commandCode, String message) { + 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(); + } + } +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java new file mode 100644 index 0000000..03b205a --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/FontendPickupCodeDialog.java @@ -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 optionMap = new HashMap<>(); + static { + for (Option option : Option.values()) { + optionMap.put(option.getOption(), option); + } + } + + @Override + protected Context setCtx() { + return getContext(); + } + + @Override + protected Class setCls() { + return getClass(); + } + + @Override + protected HandlerMain setHandlerMain() { + return new ToastHandlerMain(getCtx(), getLogs()) { + @Override + protected void execute(Context context, int commandCode, String message) { + 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(); + } + } +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/PickupErrorTipDialog.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/PickupErrorTipDialog.java new file mode 100644 index 0000000..4866868 --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/PickupErrorTipDialog.java @@ -0,0 +1,175 @@ +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.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 optionMap = new HashMap<>(); + static { + for (Option option : Option.values()) { + optionMap.put(option.getOption(), option); + } + } + + @Override + protected Context setCtx() { return this.getContext(); } + + @Override + protected Class setCls() { return getClass(); } + + @Override + protected HandlerMain setHandlerMain() { + + return new ToastHandlerMain(getContext(), getLogs()) { + @Override + protected void execute(Context context, int commandCode, String message) { +// 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); + } + + 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("否("+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(); + } + } + +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/TakeMedicineDialog.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/TakeMedicineDialog.java new file mode 100644 index 0000000..d26846d --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/dialog/TakeMedicineDialog.java @@ -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 optionMap = new HashMap<>(); + static { + for (Option option : Option.values()) { + optionMap.put(option.getOption(), option); + } + } + + @Override + protected Context setCtx() { + return getContext(); + } + + @Override + protected Class 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(); + } + } +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java new file mode 100644 index 0000000..fdae52f --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogPickupCodeBuyListAdpter.java @@ -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 { + private Logs logs; + private Context context; + private final List buyList; + private HandlerMain handlerMain; + + public RecyclerDialogPickupCodeBuyListAdpter(Context context, List 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("點我選擇商品"); + } +// 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); + } +} diff --git a/app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogTakeMedicineListAdpter.java b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogTakeMedicineListAdpter.java new file mode 100644 index 0000000..7a0bf2d --- /dev/null +++ b/app/src/Cmuh/java/com/unibuy/smartdevice/ui/recycler/RecyclerDialogTakeMedicineListAdpter.java @@ -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 { + private Logs logs; + private Context context; + private final List buyList; + private HandlerMain handlerMain; + + public RecyclerDialogTakeMedicineListAdpter(Context context, List 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(position)); + 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(); + } + +} diff --git a/app/src/Cmuh/res/layout/dialog_cmuh_take_medicine.xml b/app/src/Cmuh/res/layout/dialog_cmuh_take_medicine.xml new file mode 100644 index 0000000..9d9adb8 --- /dev/null +++ b/app/src/Cmuh/res/layout/dialog_cmuh_take_medicine.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +