[FIX] 優化 MQTT 遠端重啟邏輯與機台忙碌狀態判斷

1. 在 MyApp 實作 getBusyReason(),整合對話框、購物車、NFC 支付狀態,提供更精確的忙碌診斷資訊。
2. 重構 MqttService 遠端解鎖/重啟邏輯,使用新的忙碌檢查機制並記錄詳細拒絕原因。
3. 修正 HomeActivity 進入時強制清空購物車,確保逾時回首頁後機台能正確進入閒置狀態。
4. 更新 FontendActivity 重啟邏輯,改用 isMachineBusy() 提升穩定性。
5. 移除出貨對話框 (DispatchDialog) 的「異常回報」按鈕,簡化使用者介面。
6. 優化購物車 Adapter (Electric/VMC),確保遠端庫存更新時能即時同步 UI 點選狀態。
This commit is contained in:
sky121113 2026-05-12 16:41:52 +08:00
parent ccab0905c6
commit 93a9ad7e2d
8 changed files with 58 additions and 48 deletions

View File

@ -192,6 +192,9 @@ public class HomeActivity extends AppCompatActivityAbstract {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 🚩 進入首頁時強制清空購物車確保機台處於閒置狀態避免 MQTT 遠端重啟被阻擋
MyApp.getInstance().getBuyList().clear();
// 啟動鎖定檢查
com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao(getApplicationContext());
if (settingsDao.isMachineLocked()) {

View File

@ -118,7 +118,6 @@ public class MyApp extends Application {
public List<InvoiceItemStructure> invoiceItems;
public ReportDispatchInfoStructure reportDispatchInfoData;
public CreditCardReceiptStructure creditCardReceiptInfoData;
public boolean isShowDialog;
public volatile boolean isNfcPaymentActive; // 用於控制 NFC 支付感應器的活動門禁
public boolean isDetectHost;
@ -808,14 +807,46 @@ public class MyApp extends Application {
this.closeSlot = closeSlot;
}
private volatile boolean isShowDialog;
public boolean isShowDialog() {
return isShowDialog;
}
public void setShowDialog(boolean showDialog) {
if (this.isShowDialog != showDialog) {
logs.info("setShowDialog: " + isShowDialog + " -> " + showDialog);
}
isShowDialog = showDialog;
}
public boolean isMachineBusy() {
return !getBusyReason().isEmpty();
}
/**
* 取得機台忙碌的原因若不忙碌則回傳空字串
* @return 忙碌原因描述
*/
public String getBusyReason() {
StringBuilder reason = new StringBuilder();
if (isShowDialog()) {
reason.append("[Dialog Open] ");
}
if (!getBuyList().isEmpty()) {
reason.append("[Cart not empty: ").append(getBuyList().size()).append(" items] ");
}
if (isNfcPaymentActive()) {
reason.append("[NFC Payment Active] ");
}
String result = reason.toString().trim();
if (!result.isEmpty()) {
logs.debug("Machine busy check: " + result);
}
return result;
}
public String getSuAccess() {
return suAccess;
}

View File

@ -95,15 +95,20 @@ public class MqttService extends Service {
logs.info("Executing remote reboot command...");
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
String busyReason = MyApp.getInstance().getBusyReason();
if (idle < 60 || isDialog) {
String reason = isDialog ? "Dialog is open" : "User interacting (idle: " + idle + "s)";
logs.info("Remote reboot rejected: " + reason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + reason);
// 🚩 若機台忙碌對話框開著購物車有東西NFC 交易中則拒絕
if (!busyReason.isEmpty()) {
logs.info("Remote reboot rejected: " + busyReason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + busyReason);
return;
}
// 🚩 若不忙碌但閒置時間不足僅記錄警告但允許重啟遠端控制優先
if (idle < 60) {
logs.info("Remote reboot accepted despite low idle (" + idle + "s), as machine is not busy.");
}
mqttManager.publishCommandAck(cmdId, "success", "Rebooting soon...");
// 🚩 優雅重啟ACK 發出後先 gracefulDisconnect不觸發 LWT killProcess
new android.os.Handler(android.os.Looper.getMainLooper())
@ -136,13 +141,11 @@ public class MqttService extends Service {
} else if ("unlock".equals(cmd)) {
logs.info("Executing remote unlock command...");
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
String busyReason = MyApp.getInstance().getBusyReason();
if (idle < 60 || isDialog) {
String reason = isDialog ? "Dialog is open" : "User interacting (idle: " + idle + "s)";
logs.info("Remote unlock/reboot rejected: " + reason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + reason);
if (!busyReason.isEmpty()) {
logs.info("Remote unlock rejected: " + busyReason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + busyReason);
return;
}

View File

@ -436,7 +436,8 @@ public class FontendActivity extends AppCompatActivityAbstract {
}
idleRunnable = () -> {
getLogs().info("⚠️ 無操作 3 分鐘,自動返回首頁");
getLogs().info("⚠️ 無操作 3 分鐘,自動返回首頁(清空購物車)");
MyApp.getInstance().getBuyList().clear(); // 🚩 逾時返回首頁時務必清空購物車避免機台進入永久忙碌狀態
Intent intent = new Intent(FontendActivity.this, HomeActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
@ -684,7 +685,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
protected void execute(HandlerMain handlerMain) {
for (int testIndex=0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
if (!MyApp.getInstance().isMachineBusy()) {
getLogs().debug("RestartHost testCount:" + testCount);
testCount++;
} else {
@ -749,9 +750,9 @@ public class FontendActivity extends AppCompatActivityAbstract {
// 最多等待 10 分鐘120 * 5 確保機台有機會重啟
for (int i = 0; i < 120; i++) {
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
if (idle >= 90 && !isDialog) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒且無對話框,執行真重啟");
boolean isBusy = MyApp.getInstance().isMachineBusy();
if (idle >= 90 && !isBusy) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒且機台不忙碌,執行真重啟");
// 切回主執行緒執行重啟
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);

View File

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

View File

@ -148,7 +148,7 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
public void onClick(View v) {
SlotStructure slotProduct = SlotList.get(position);
if (slotProduct.isLock()) return;
if (MyApp.getInstance().isShowDialog) return;
if (MyApp.getInstance().isShowDialog()) return;
BuyStructure buyProduct = new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1);
MyApp.getInstance().getBuyList().add(buyProduct);

View File

@ -296,24 +296,6 @@ public class DispatchDialog extends DialogAbstract {
currentlyProcessingGoods();
});
this.binding.buttonCancel.setOnClickListener(v -> {
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(UploadLogOneTimeWorker.class)
.setInitialDelay(5, TimeUnit.SECONDS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(getCtx()).enqueueUniqueWork(
"oneTimeWorkRequest", // 設定唯一名稱避免重複
ExistingWorkPolicy.REPLACE,
oneTimeWorkRequest
);
getSrcHandlerMain().start(getClass().getSimpleName(), "@"+getCtx().getString(R.string.reported_please_contact_site_for_processing));//已回報請現場聯絡處理
dialogCancel();
});
setButtonOkText(0);
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());

View File

@ -182,19 +182,9 @@
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/buttonCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:text="@string/error_report"
android:textSize="20sp"
android:textColor="@color/black" />
<Button
android:id="@+id/buttonOk"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/white"