[FIX] 修正 OTA 連線超時與首頁執行緒併發競態導致 UI 異常問題

1. OtaManager.java: 將下載的連線逾時從 15 秒調高至 30 秒,讀取逾時調高至 120 秒,提升 OTA 下載穩定性。
2. FontendActivity.java: 修正多執行緒更新 UI 時,共用 logs 緩衝區所引發的競態條件 (Race Condition) 導致欄位變空的問題,改採區域變數更新;並安全封裝重構 reportOnScheduler 的生命週期管理方法;調整閒置逾時為 5 分鐘。
3. build.gradle: 依據實體機台 OTA 版號自動化規範,安全遞增 verPatch 修補版號至 56。
4. compiler-apk.md: 優化編譯工作流中的後台更新說明提示欄位。
This commit is contained in:
sky121113 2026-06-01 17:48:32 +08:00
parent 8768e95178
commit 808b9c40d9
4 changed files with 43 additions and 33 deletions

View File

@ -157,6 +157,7 @@ if [ -n "$APK_SOURCE" ]; then
echo "* 版本名稱 (versionName)$VERSION_NAME"
echo "* 版本代碼 (versionCode)$VERSION_CODE"
echo "* APK 檔案名稱:$APK_NAME"
echo "* 更新說明:[請在此處附上口語化的一句話更新說明]"
echo "=================================================="
else
echo "❌ 找不到編譯產出的 APK 檔案,複製失敗!"

View File

@ -9,7 +9,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 38
def verPatch = 56
android {

View File

@ -17,6 +17,8 @@ import java.net.URL;
public class OtaManager {
private static final String TAG = "OtaManager";
private static final Logs logs = new Logs(OtaManager.class);
private static final int DOWNLOAD_CONNECT_TIMEOUT_MS = 30000;
private static final int DOWNLOAD_READ_TIMEOUT_MS = 120000;
/**
* 啟動背景執行緒執行 APP OTA 更新
@ -67,8 +69,8 @@ public class OtaManager {
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.setConnectTimeout(DOWNLOAD_CONNECT_TIMEOUT_MS);
connection.setReadTimeout(DOWNLOAD_READ_TIMEOUT_MS);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {

View File

@ -118,11 +118,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
private Fragment activeFragment;
private Fragment vmcFragment;
private Fragment electricFragment;
private static ReportShotTimeByScheduler reportOnScheduler;
private ReportShotTimeByScheduler reportOnScheduler;
private ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;
private static final long IDLE_TIMEOUT = 5 * 60 * 1000; // 3 分鐘
private static final long IDLE_TIMEOUT = 5 * 60 * 1000; // 5 分鐘
private final long TIME_LIMIT = 200000;
private int clickCount = 0;
private long firstClickTime = 0;
@ -263,7 +263,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
// 第一層定時重啟到了整點就啟動等待模式RestartAPP2 自己會等閒置 >= 90 秒才真重啟
if (timeByM.equals("04:00") || timeByM.equals("08:00") || timeByM.equals("12:00")
|| timeByM.equals("16:00") || timeByM.equals("20:00") || timeByM.equals("23:59")) {
android.content.SharedPreferences prefs = getSharedPreferences("AppRestartPrefs", Context.MODE_PRIVATE);
android.content.SharedPreferences prefs = getSharedPreferences("AppRestartPrefs",
Context.MODE_PRIVATE);
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
String currentSlot = today + " " + timeByM;
String lastRestartSlot = prefs.getString("last_restart_slot", "");
@ -280,11 +281,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
// 第二層閒置深度刷新任何時間閒置超過 30 分鐘就重啟 (暫時關閉改依賴定時重啟)
/*
if (MyApp.getInstance().getIdleSeconds() >= 30 * 60) {
getLogs().info("閒置超過 30 分鐘,執行深度排毒重啟...");
new RestartAPP2(this).start();
}
*/
* if (MyApp.getInstance().getIdleSeconds() >= 30 * 60) {
* getLogs().info("閒置超過 30 分鐘,執行深度排毒重啟...");
* new RestartAPP2(this).start();
* }
*/
// 這裡判斷每5分鐘去跑連線下位機的狀態
// String minutePart = timeByM.split(":")[1]; // 取得 "mm" 部分
@ -305,24 +306,24 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
// new CMUHAutoRun(this).start();
}
getLogs().update("ThreadCount", String.valueOf(Tools.threadCount()));
String tc = getLogs().getMessage();
getLogs().update("ViceoCount", String.valueOf(VideoEoxPlayer.getExoPlayerTotal()));
String vc = getLogs().getMessage();
binding.textThreadCount.setText("TC:" + tc + " VC:" + vc);
getLogs().clearMessage();
// 直接使用區域變數更新 UI避免多執行緒 Race Condition 導致 getLogs().getMessage() 被改寫或清空而使 UI 變空
String threadCountStr = String.valueOf(Tools.threadCount());
String videoCountStr = String.valueOf(VideoEoxPlayer.getExoPlayerTotal());
binding.textThreadCount.setText("TC:" + threadCountStr + " VC:" + videoCountStr);
getLogs().update("ThreadCount", threadCountStr);
getLogs().update("ViceoCount", videoCountStr);
Tools.threadInfos();
String networkDevice = getNetworkDevice();
binding.textNetwork.setText(getString(R.string.network) + "" + networkDevice);
getLogs().update("Network", networkDevice);
binding.textNetwork.setText(getString(R.string.network) + "" + getLogs().getMessage());
getLogs().clearMessage();
DevXinYuanController.setVersion(ver);
String tempStr = DevXinYuanController.getTemperature() + "";
binding.textTemperature.setText(getString(R.string.temp) + ":" + tempStr);
getLogs().update("Temperature", String.valueOf(DevXinYuanController.getTemperature()), "");
binding.textTemperature.setText(getString(R.string.temp) + ":" + getLogs().getMessage());
getLogs().clearMessage();
try {
// httpAPI.detectHost();
@ -467,7 +468,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
return saleFlowHandler;
}
public void startOnSwitchFragment() {
try {
stopOnSwitchFragment(); // 先安全釋放舊播放器
@ -487,7 +487,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
videoEoxPlayer = new VideoEoxPlayer(getCtx()); // 用新版建構子
getHandlerMain().getHandler().postDelayed(() -> {
if (isFinishing() || isDestroyed()) return;
if (isFinishing() || isDestroyed())
return;
if (videoEoxPlayer != null) {
// videoEoxPlayer.playMedia(binding.videoView, binding.imageAd); // 傳入 view
// 開始播放
@ -700,12 +701,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
binding.floatButtonShoppingCart.setVisibility(View.GONE);
}
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
}
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
startHeaderReportScheduler();
// binding.btnswitchlanTW.setOnClickListener(v -> switchLanguage("tw"));
//
@ -753,6 +749,20 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
startOnSwitchFragment();
}
private void startHeaderReportScheduler() {
stopHeaderReportScheduler();
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
getHandlerMain().start(getClass().getSimpleName(), Option.REPORT_AND_SHOT_TIME.getOption(), "start");
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
}
private void stopHeaderReportScheduler() {
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
}
private void hiddenMultiLanguageButton() {
binding.btnswitchlanTW.setVisibility(View.GONE);
binding.btnswitchlanEn.setVisibility(View.GONE);
@ -1230,9 +1240,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
}
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
stopHeaderReportScheduler();
}
}