[FIX] 修復廣告同步導致的 Activity 銷毀崩潰與局部廣告清空失效問題

1. 修正 HomeActivity 與 FontendActivity 在廣告更新廣播回呼中因 Activity 已銷毀而觸發 Glide IllegalArgumentException 的問題。
2. 在 VideoImageMixedPlayer 中增加 Activity 生命週期安全檢查,確保不會在非法 Context 下執行 UI 操作。
3. 優化 VideoImageMixedPlayer 的清空邏輯:當特定 Flag 下無廣告可播放時,主動隱藏影片與圖片視圖並清除殘留影像。
4. 強化 stop() 方法,確保切換頁面時能完整釋放播放器資源並重設視圖狀態。
5. 增加詳細的廣告過濾與播放日誌,並支援廣告動態播放時間 (Duration) 控制。
This commit is contained in:
sky121113 2026-05-13 17:06:17 +08:00
parent 3044314856
commit 8ae4ae9c14
17 changed files with 457 additions and 29 deletions

View File

@ -12,13 +12,14 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
```bash
// turbo
# 終止殘留的 Gradle/Java 程序,避免多個 Daemon 互搶資源
pkill -9 -f "gradle" 2>/dev/null; pkill -9 -f "GradleDaemon" 2>/dev/null; sleep 1
pkill -9 -f "gradle" 2>/dev/null; pkill -9 -f "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
# 使用 clean 確保完整重新編譯,避免舊快取遮蔽 import 缺失等錯誤
./gradlew clean :app:assembleS_12_XY_U_Standard_Debug
# --no-daemon強制使用單次 JVM 進程,避免 Daemon 初始化期間被殘留訊號中斷
./gradlew clean :app:assembleS_12_XY_U_Standard_Debug --no-daemon
```
## 步驟二:導出 APK 到 outputs 資料夾

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View File

@ -1235,6 +1235,18 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="samsung" />
<option name="codename" value="m1q" />
<option name="id" value="m1q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S26" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="samsung" />
@ -1537,6 +1549,18 @@
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="samsung" />
<option name="codename" value="q7mq" />
<option name="id" value="q7mq" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z TriFold" />
<option name="screenDensity" value="320" />
<option name="screenX" value="2160" />
<option name="screenY" value="1584" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="samsung" />

12
.idea/gradle.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -2,11 +2,14 @@ package com.unibuy.smartdevice;
import static android.view.View.VISIBLE;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.view.View;
@ -76,6 +79,25 @@ public class HomeActivity extends AppCompatActivityAbstract {
private ActivityHomeBinding binding;
private static VideoEoxPlayer videoEoxPlayer;
// 廣告更新監聽器MQTT 廣告同步完成後自動刷新待機廣告播放器
private final BroadcastReceiver adUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
getLogs().info("🔔 [HomeActivity] 收到 ACTION_AD_UPDATED 廣播,準備刷新待機頁廣告播放器...");
// 🔹 清除 Glide 記憶體快取避免拿到舊的圖片 Bitmap
Glide.get(context).clearMemory();
// 使用 postDelayed 確保 DB 寫入完成後再刷新
new android.os.Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
startOnSwitchFragment();
getLogs().info("✅ [HomeActivity] 待機頁廣告播放器已重建");
} else {
getLogs().info("⚠️ [HomeActivity] Activity 已銷毀,取消刷新廣告播放器");
}
}, 500);
}
};
private long lastReportTime = 0;
private static ReportShotTimeByScheduler reportOnScheduler;
@ -227,6 +249,14 @@ public class HomeActivity extends AppCompatActivityAbstract {
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
startOnSwitchFragment();
// 註冊廣告更新監聽
IntentFilter adFilter = new IntentFilter("com.unibuy.smartdevice.ACTION_AD_UPDATED");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(adUpdateReceiver, adFilter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(adUpdateReceiver, adFilter);
}
}
public void startOnSwitchFragment() {
@ -236,6 +266,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
videoEoxPlayer = new VideoEoxPlayer(getCtx());
getHandlerMain().getHandler().postDelayed(() -> {
if (isFinishing() || isDestroyed()) return;
try {
// videoEoxPlayer.playMedia(binding.videoView, binding.imageAd); // 同時支援影片與圖片輪播
videoEoxPlayer.playMediaByFlag("3", binding.videoView, binding.imageAd); // 同時支援影片與圖片輪播
@ -579,5 +610,11 @@ public class HomeActivity extends AppCompatActivityAbstract {
protected void onDestroy() {
super.onDestroy();
stopOnSwitchFragment();
// 取消註冊廣告更新監聽
try {
unregisterReceiver(adUpdateReceiver);
} catch (Exception e) {
getLogs().info("unregisterReceiver adUpdateReceiver: " + e.getMessage());
}
}
}

View File

@ -417,17 +417,16 @@ public class MyApp extends Application {
serviceIntent.putExtra("machineId", machineId);
serviceIntent.putExtra("apiToken", apiKey);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} catch (Exception e) {
// Android 12+ AlarmManager 喚醒時不允許直接啟動 ForegroundService
// 改用普通 startService Service 先啟動 Activity 進前台後再提升
logs.info("ForegroundService start blocked (background state), falling back to startService");
} else {
startService(serviceIntent);
}
} else {
startService(serviceIntent);
} catch (Exception e) {
// Android 12+ 在背景狀態下如安裝後自動重啟不允許啟動前台/背景服務
// 這裡只記錄日誌不再嘗試 fallback等使用者開啟 App 進前台後再由 Activity 觸發
logs.info("MQTT Service start blocked (app in background): " + e.getMessage());
}
}
}

View File

@ -20,10 +20,14 @@ import com.unibuy.smartdevice.structure.ReportSlotListStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.structure.SettingStructure;
import com.unibuy.smartdevice.ui.SettingActivity;
import com.unibuy.smartdevice.ui.tools.VideoEoxPlayer;
import com.unibuy.smartdevice.ui.tools.VideoImageMixedPlayer;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
/**
* 專門對接 Star-Cloud 後台的 API 類別
@ -671,11 +675,140 @@ public class StarCloudAPI {
}
Log.i(TAG, "Updated product references in all slots.");
// 🔹 發送廣播通知所有監如銷售平台商品與貨道資料已更新
// 🔹 發送廣播通知所有監如銷售平台商品與貨道資料已更新
android.content.Intent intent = new android.content.Intent("com.unibuy.smartdevice.ACTION_SLOT_UPDATED");
intent.setPackage(handlerMain.getContext().getPackageName());
handlerMain.getContext().sendBroadcast(intent);
}
/**
* B005: 下載廣告清單並同步媒體檔案
*/
public void downloadAdvertisement() {
downloadAdvertisement(null);
}
public void downloadAdvertisement(final Runnable onComplete) {
final String url = BASE_URL + "machine/ad/B005";
Log.i(TAG, "Starting B005 request to: " + url);
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = -1;
String receive = "";
try {
java.net.URL reqUrl = new java.net.URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
String machineToken = MyApp.getInstance().getMachine().getApiKey();
if (machineToken != null && !machineToken.isEmpty()) {
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
}
conn.setConnectTimeout(15000);
conn.setReadTimeout(30000); // 廣告清單可能較大
responseCode = conn.getResponseCode();
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
receive = sb.toString();
}
conn.disconnect();
} catch (Exception e) {
Log.e(TAG, "B005 Network Error", e);
handlerMain.start("StarCloudAPI", 500, "廣告同步連線失敗");
if (onComplete != null) onComplete.run();
return;
}
final int finalCode = responseCode;
final String finalReceive = receive;
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
JSONArray ads = res.getJSONArray("data");
saveAds(ads);
handlerMain.start("StarCloudAPI", 200, "廣告同步完成");
} else {
handlerMain.start("StarCloudAPI", 401, "廣告同步失敗: " + res.optString("message"));
}
} catch (Exception e) {
Log.e(TAG, "B005 Parse Error", e);
handlerMain.start("StarCloudAPI", 500, "廣告解析錯誤");
}
} else {
handlerMain.start("StarCloudAPI", finalCode, "廣告同步網路錯誤: " + finalCode);
}
if (onComplete != null) onComplete.run();
}
}).start();
}
private void saveAds(JSONArray ads) throws JSONException {
SettingsDao settingsDao = new SettingsDao(handlerMain.getContext());
// 清除舊廣告設定
settingsDao.deleteWhere(VideoEoxPlayer.class.getSimpleName());
// 🔹 清除 MediaCacheHelper 本地快取避免 URL 相同但內容變更時播放器讀到過期快取
VideoImageMixedPlayer.clearAdsCache(handlerMain.getContext());
Log.i(TAG, "Cleared local ads media cache before sync.");
MyApp.getInstance().getFontendAdUrlString().clear();
for (int i = 0; i < ads.length(); i++) {
JSONObject ad = ads.getJSONObject(i);
String url = ad.optString("t070v04", "");
String flag = String.valueOf(ad.optInt("t070v03", 0));
String duration = String.valueOf(ad.optInt("t070v02", 10));
if (url.isEmpty()) continue;
MyApp.getInstance().getFontendAdUrlString().add(url);
// 下載檔案
try {
Log.i(TAG, "Downloading Ad (" + (i+1) + "/" + ads.length() + "): " + url);
HttpConnect httpConnect = new HttpConnect();
httpConnect.download(url, true);
} catch (Exception e) {
Log.e(TAG, "Download Ad Failed: " + url, e);
}
// 儲存至資料庫 [URL, Flag, Duration]
settingsDao.insertOne(new SettingStructure(
VideoEoxPlayer.class.getSimpleName(),
new String[]{url, flag, duration}
));
// 更新 UI 進度
final int percent = (i + 1) * 100 / ads.length();
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
SettingActivity.updateProgressUI(percent, "廣告下載中...");
}
});
}
settingsDao.close();
Log.i(TAG, "Synced " + ads.length() + " ads from star-cloud.");
// 發送廣播通知前台 ActivityHomeActivity / FontendActivity刷新廣告播放器
// 🔹 必須設定 setPackage 才能被 RECEIVER_NOT_EXPORTED 的接收器收到Android 13+
android.content.Intent adIntent = new android.content.Intent("com.unibuy.smartdevice.ACTION_AD_UPDATED");
adIntent.setPackage(handlerMain.getContext().getPackageName());
handlerMain.getContext().sendBroadcast(adIntent);
Log.i(TAG, "ACTION_AD_UPDATED broadcast sent.");
}
/**
* 取得設備真實 IP 地址
*/

View File

@ -240,6 +240,40 @@ public class MqttService extends Service {
logs.info("Failed to parse or start change command: " + e.getMessage());
mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage());
}
} else if ("update_ads".equals(cmd)) {
logs.info("Executing remote advertisement sync command...");
// 📢 在主執行緒顯示 Toast 提示使用者
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
android.widget.Toast.makeText(getApplicationContext(),
"📢 廣告更新中...", android.widget.Toast.LENGTH_SHORT).show());
com.unibuy.smartdevice.tools.HandlerMain adHandler = new com.unibuy.smartdevice.tools.HandlerMain(
getApplicationContext(), logs) {
@Override
protected void execute(Context context, int code, String message) {
logs.info("Ad sync event: [" + code + "] " + message);
if (code == 200 && "廣告同步完成".equals(message)) {
mqttManager.publishCommandAck(cmdId, "success",
"Advertisement sync completed successfully");
// Toast 提示完成
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
android.widget.Toast.makeText(context,
"✅ 廣告更新完成", android.widget.Toast.LENGTH_SHORT).show());
// 注意廣播 ACTION_AD_UPDATED 已在 StarCloudAPI.saveAds() 中統一發送
} else if (code >= 400) {
mqttManager.publishCommandAck(cmdId, "failed",
"Advertisement sync failed: " + message);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
android.widget.Toast.makeText(context,
"❌ 廣告更新失敗: " + message, android.widget.Toast.LENGTH_LONG).show());
}
}
};
// 使用新架構 StarCloudAPI 執行同步
new com.unibuy.smartdevice.external.StarCloudAPI(adHandler, adHandler).downloadAdvertisement();
} else if ("update_inventory".equals(cmd)) {
logs.info("Executing update_inventory command...");
try {

View File

@ -141,6 +141,22 @@ public class FontendActivity extends AppCompatActivityAbstract {
}
};
// 廣告更新監聽器MQTT 廣告同步完成後自動刷新販賣頁廣告播放器
private final BroadcastReceiver adUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
getLogs().info("🔔 [FontendActivity] 收到 ACTION_AD_UPDATED 廣播,準備刷新販賣頁廣告播放器...");
// 🔹 清除 Glide 記憶體快取避免拿到舊的圖片 Bitmap
Glide.get(context).clearMemory();
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
startOnSwitchFragment();
getLogs().info("✅ [FontendActivity] 販賣頁廣告播放器已重建");
}
}, 500);
}
};
private static VideoEoxPlayer videoEoxPlayer;
@Override
@ -423,7 +439,9 @@ public class FontendActivity extends AppCompatActivityAbstract {
// 確保前一個 ExoPlayer 完整釋放後再初始化新的
new Handler(Looper.getMainLooper()).postDelayed(() -> {
initVideoPlayer();
if (!isFinishing() && !isDestroyed()) {
initVideoPlayer();
}
}, 100);
} catch (Exception e) {
getLogs().warning(e);
@ -434,6 +452,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
videoEoxPlayer = new VideoEoxPlayer(getCtx()); // 用新版建構子
getHandlerMain().getHandler().postDelayed(() -> {
if (isFinishing() || isDestroyed()) return;
if (videoEoxPlayer != null) {
// videoEoxPlayer.playMedia(binding.videoView, binding.imageAd); // 傳入 view
// 開始播放
@ -1100,6 +1119,14 @@ public class FontendActivity extends AppCompatActivityAbstract {
registerReceiver(slotUpdateReceiver, filter);
}
// 註冊廣告更新監聽
IntentFilter adFilter = new IntentFilter("com.unibuy.smartdevice.ACTION_AD_UPDATED");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(adUpdateReceiver, adFilter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(adUpdateReceiver, adFilter);
}
// 🔹 從設定頁面或其他頁面回到銷售平台時強制刷新商品列表確保新圖片能顯示
// 先清除 Glide 記憶體快取避免拿到舊的 Bitmap
Glide.get(this).clearMemory();
@ -1120,6 +1147,12 @@ public class FontendActivity extends AppCompatActivityAbstract {
} catch (Exception e) {
getLogs().info("unregisterReceiver slotUpdateReceiver: " + e.getMessage());
}
// 取消註冊廣告更新監聽
try {
unregisterReceiver(adUpdateReceiver);
} catch (Exception e) {
getLogs().info("unregisterReceiver adUpdateReceiver: " + e.getMessage());
}
}
@Override

View File

@ -58,6 +58,7 @@ public class RestartHostActivity extends AppCompatActivityAbstract {
private RecyclerSettingListAdpter recyclerSettingListAdpter;
private HttpAPI httpAPI;
private com.unibuy.smartdevice.external.StarCloudAPI starCloudAPI;
@Override
protected Context setCtx() {
@ -88,11 +89,7 @@ public class RestartHostActivity extends AppCompatActivityAbstract {
new ImageReload(this).start();
break;
case AD_RELOAD:
try {
httpAPI.downloadAdvertisement();
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
starCloudAPI.downloadAdvertisement();
break;
}
}
@ -107,6 +104,7 @@ public class RestartHostActivity extends AppCompatActivityAbstract {
setContentView(binding.getRoot());
httpAPI = new HttpAPI(getHandlerMain());
starCloudAPI = new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain());
recyclerSettingListAdpter = new RecyclerSettingListAdpter(statusList);
binding.recyclerSettingList.setAdapter(recyclerSettingListAdpter);

View File

@ -114,6 +114,9 @@ public class SettingActivity extends AppCompatActivityAbstract {
if ("綁定成功".equals(message)) {
binding.progressLayout.setVisibility(View.VISIBLE); // 顯示進度條因為要同步商品
starCloudAPI.downloadProductInfo();
} else if ("商品下載成功".equals(message)) {
binding.progressLayout.setVisibility(View.VISIBLE); // 顯示進度條同步廣告
starCloudAPI.downloadAdvertisement();
}
checkSetting();
@ -207,6 +210,7 @@ public class SettingActivity extends AppCompatActivityAbstract {
}
public static void updateProgressUI(int percent, String message) {
if (instance == null) return;
SettingActivity activity = instance.get();
if (activity == null)
return;

View File

@ -75,18 +75,26 @@ public class VideoEoxPlayer {
SettingsDao settingsDao = new SettingsDao(context);
List<SettingStructure> dataList = settingsDao.getWhere(VideoEoxPlayer.class.getSimpleName());
List<String> targetUrls = new ArrayList<>();
List<VideoImageMixedPlayer.AdItem> targetAds = new ArrayList<>();
for (SettingStructure data : dataList) {
String url = data.getData(0);
String flag = data.getData(1);
int duration = 10;
try {
if (data.getDataCount() > 2) {
duration = Integer.parseInt(data.getData(2));
}
} catch (Exception ignored) {}
if (flag != null && flag.equals(targetFlag)) {
targetUrls.add(url);
targetAds.add(new VideoImageMixedPlayer.AdItem(url, duration));
}
}
settingsDao.close();
logs.info("Ads [Flag: " + targetFlag + "] 過濾後數量: " + targetAds.size());
mixedPlayer = new VideoImageMixedPlayer(context, targetUrls);
mixedPlayer = new VideoImageMixedPlayer(context, targetAds);
mixedPlayer.start(videoView, imageView);
}

View File

@ -29,9 +29,19 @@ import java.util.concurrent.Executors;
public class VideoImageMixedPlayer {
public static class AdItem {
public String url;
public int duration; //
public AdItem(String url, int duration) {
this.url = url;
this.duration = duration;
}
}
private final Logs logs;
private final Context context;
private final List<String> mediaUrls;
private final List<AdItem> adItems;
private ExoPlayer exoPlayer;
private PlayerView playerView;
@ -39,9 +49,9 @@ public class VideoImageMixedPlayer {
private Handler handler;
private int currentIndex = 0;
public VideoImageMixedPlayer(Context context, List<String> mediaUrls) {
public VideoImageMixedPlayer(Context context, List<AdItem> adItems) {
this.context = context;
this.mediaUrls = mediaUrls;
this.adItems = adItems;
this.logs = new Logs(getClass());
this.handler = new Handler(Looper.getMainLooper());
}
@ -54,16 +64,28 @@ public class VideoImageMixedPlayer {
}
private void playNext() {
if (mediaUrls == null || mediaUrls.isEmpty()) {
logs.info("Ads沒有任何媒體可以播放");
if (adItems == null || adItems.isEmpty()) {
logs.info("Ads沒有任何媒體可以播放清空視圖 (flag-based)...");
if (playerView != null) {
playerView.setVisibility(android.view.View.GONE);
playerView.setPlayer(null);
}
if (imageView != null) {
imageView.setVisibility(android.view.View.GONE);
imageView.setImageDrawable(null); // 清除舊圖片
}
releaseExoPlayer();
return;
}
if (currentIndex >= mediaUrls.size()) {
logs.info("Ads 目前播放索引: " + currentIndex + " / 總數: " + adItems.size());
if (currentIndex >= adItems.size()) {
currentIndex = 0;
}
String url = mediaUrls.get(currentIndex);
AdItem item = adItems.get(currentIndex);
String url = item.url;
// 先檢查快取
String cachedPath = MediaCacheHelper.getCachedPath(context, url);
@ -132,10 +154,23 @@ public class VideoImageMixedPlayer {
exoPlayer.prepare();
exoPlayer.setPlayWhenReady(true);
// 使用設定的秒數控制影片播放時間到時間就強制切下一個
int duration = adItems.get(currentIndex).duration;
if (duration <= 0) duration = 10;
final Runnable forceNextRunnable = () -> {
logs.info("影片播放到設定秒數 (" + adItems.get(currentIndex).duration + "s),強制切下一個廣告");
releaseExoPlayer();
currentIndex = (currentIndex + 1) % adItems.size();
playNext();
};
handler.postDelayed(forceNextRunnable, duration * 1000L);
exoPlayer.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_ENDED) {
// 影片自然播放結束取消強制計時器直接切下一個
handler.removeCallbacks(forceNextRunnable);
nextSchedule(1000);
}
}
@ -143,6 +178,7 @@ public class VideoImageMixedPlayer {
@Override
public void onPlayerError(PlaybackException error) {
logs.info("影片播放錯誤: " + error.getMessage());
handler.removeCallbacks(forceNextRunnable);
nextSchedule(1000);
}
});
@ -155,17 +191,29 @@ public class VideoImageMixedPlayer {
Object source = path.startsWith("/") ? new File(path) : path;
// 增加生命週期檢查避免 Glide Activity 銷毀後嘗試載入導致崩潰
if (context instanceof android.app.Activity) {
android.app.Activity activity = (android.app.Activity) context;
if (activity.isFinishing() || (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed())) {
logs.info("⚠️ Activity 已銷毀,跳過圖片載入: " + path);
return;
}
}
Glide.with(context)
.load(source)
.fitCenter()
.into(imageView);
nextSchedule(10000); // 圖片輪播 10
// 使用動態秒數若為 0 則預設 10
int duration = adItems.get(currentIndex).duration;
if (duration <= 0) duration = 10;
nextSchedule(duration * 1000L);
}
private void nextSchedule(long delayMs) {
handler.postDelayed(() -> {
currentIndex = (currentIndex + 1) % mediaUrls.size();
currentIndex = (currentIndex + 1) % adItems.size();
playNext();
}, delayMs);
}
@ -180,7 +228,16 @@ public class VideoImageMixedPlayer {
}
public void stop() {
logs.info("Ads 播放器停止,清空視圖");
releaseExoPlayer();
if (playerView != null) {
playerView.setVisibility(android.view.View.GONE);
playerView.setPlayer(null);
}
if (imageView != null) {
imageView.setVisibility(android.view.View.GONE);
imageView.setImageDrawable(null);
}
if (handler != null) {
handler.removeCallbacksAndMessages(null);
}

57
build_log.txt Normal file
View File

@ -0,0 +1,57 @@
Starting a Gradle Daemon, 1 busy and 3 incompatible and 1 stopped Daemons could not be reused, use --status for details
> Task :app:preBuild UP-TO-DATE
> Task :app:preS_12_XY_U_Standard_DebugBuild UP-TO-DATE
> Task :app:mergeS_12_XY_U_Standard_DebugNativeDebugMetadata NO-SOURCE
> Task :app:generateS_12_XY_U_Standard_DebugResValues
> Task :app:dataBindingMergeDependencyArtifactsS_12_XY_U_Standard_Debug
> Task :app:generateS_12_XY_U_Standard_DebugResources
> Task :app:packageS_12_XY_U_Standard_DebugResources
> Task :app:javaPreCompileS_12_XY_U_Standard_Debug
> Task :app:mergeS_12_XY_U_Standard_DebugResources
> Task :app:checkS_12_XY_U_Standard_DebugAarMetadata
> Task :app:mapS_12_XY_U_Standard_DebugSourceSetPaths
> Task :app:createS_12_XY_U_Standard_DebugCompatibleScreenManifests
> Task :app:extractDeepLinksS_12_XY_U_Standard_Debug
> Task :app:parseS_12_XY_U_Standard_DebugLocalResources
> Task :app:processS_12_XY_U_Standard_DebugMainManifest
> Task :app:dataBindingGenBaseClassesS_12_XY_U_Standard_Debug
> Task :app:processS_12_XY_U_Standard_DebugManifest
> Task :app:mergeS_12_XY_U_Standard_DebugShaders
> Task :app:compileS_12_XY_U_Standard_DebugShaders NO-SOURCE
> Task :app:generateS_12_XY_U_Standard_DebugAssets UP-TO-DATE
> Task :app:mergeS_12_XY_U_Standard_DebugAssets
> Task :app:compressS_12_XY_U_Standard_DebugAssets
> Task :app:desugarS_12_XY_U_Standard_DebugFileDependencies
> Task :app:mergeS_12_XY_U_Standard_DebugJniLibFolders
> Task :app:checkS_12_XY_U_Standard_DebugDuplicateClasses
> Task :app:mergeS_12_XY_U_Standard_DebugNativeLibs
> Task :app:processS_12_XY_U_Standard_DebugManifestForPackage
> Task :app:mergeLibDexS_12_XY_U_Standard_Debug
> Task :app:stripS_12_XY_U_Standard_DebugDebugSymbols
Unable to strip the following libraries, packaging them as they are: libserial_port.so.
> Task :app:validateSigningS_12_XY_U_Standard_Debug
> Task :app:writeS_12_XY_U_Standard_DebugAppMetadata
> Task :app:writeS_12_XY_U_Standard_DebugSigningConfigVersions
> Task :app:mergeExtDexS_12_XY_U_Standard_Debug
warn: removing resource com.unibuy.smartdevice:string/reload_product without required default value.
warn: removing resource com.unibuy.smartdevice:string/reload_product_warning without required default value.
> Task :app:processS_12_XY_U_Standard_DebugResources
> Task :app:compileS_12_XY_U_Standard_DebugJavaWithJavac
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
> Task :app:dexBuilderS_12_XY_U_Standard_Debug
> Task :app:processS_12_XY_U_Standard_DebugJavaRes
> Task :app:mergeProjectDexS_12_XY_U_Standard_Debug
> Task :app:mergeS_12_XY_U_Standard_DebugJavaResource
> Task :app:packageS_12_XY_U_Standard_Debug
> Task :app:createS_12_XY_U_Standard_DebugApkListingFileRedirect
> Task :app:assembleS_12_XY_U_Standard_Debug
[Incubating] Problems report is available at: file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/build/reports/problems/problems-report.html
BUILD SUCCESSFUL in 56s
36 actionable tasks: 36 executed