diff --git a/app/build.gradle b/app/build.gradle index dea2577..775915c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdkVersion 24 targetSdkVersion 30 versionCode 6 - versionName "V2.0.8.32" + versionName "V2.0.8.34" //V2.0.8.22 删除ai背景视频切换代码 //V2.0.8.23 节目切换图片覆盖问题 //V2.0.8.30 4个视频 diff --git a/app/src/main/java/qianmu/container/activity/BaseActivity.java b/app/src/main/java/qianmu/container/activity/BaseActivity.java index 27f8f95..01836b5 100644 --- a/app/src/main/java/qianmu/container/activity/BaseActivity.java +++ b/app/src/main/java/qianmu/container/activity/BaseActivity.java @@ -151,10 +151,9 @@ public abstract class BaseActivity extends AppCompatActivity { // 重启app restartApp(); } else if (Constant.ACTION_RESTART_MQTT.equals(message.getCode())) { - // 重启mqtt服务 - LoggerUtil.e("BaseActivity", "通知关闭mqtt服务"); - stopService(new Intent(this, MQTTService.class));// 关闭Mqtt服务 - Constant.mqttState = "off"; + // 服务内部断开重连,避免 stopService/startService 与 static client 的竞态 + LoggerUtil.e("BaseActivity", "通知重连mqtt服务"); + MQTTService.restartConnection(); } diff --git a/app/src/main/java/qianmu/container/activity/H5/WebViewActivity.java b/app/src/main/java/qianmu/container/activity/H5/WebViewActivity.java index 1ba90c0..51c63e2 100644 --- a/app/src/main/java/qianmu/container/activity/H5/WebViewActivity.java +++ b/app/src/main/java/qianmu/container/activity/H5/WebViewActivity.java @@ -312,7 +312,11 @@ public class WebViewActivity extends BaseActivity { wv.setWebChromeClient(new WebChromeClient() { public boolean onConsoleMessage(ConsoleMessage cm) { if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { - LoggerUtil.e("WebView_ConsoleError", "lineNumber: " + cm.lineNumber()); + String msg = cm.message(); + if (msg != null && msg.length() > 120) { + msg = msg.substring(0, 120); + } + LoggerUtil.e("WebView_ConsoleError", msg); } else if (cm.message().contains("THREE.WebGLRenderer:") || cm.message().contains("Uncaught (in promise) AbortError")) { LoggerUtil.e("WebView日志", cm.message()); } diff --git a/app/src/main/java/qianmu/container/activity/program/MyPresentation.java b/app/src/main/java/qianmu/container/activity/program/MyPresentation.java index 248b318..ca5a23f 100644 --- a/app/src/main/java/qianmu/container/activity/program/MyPresentation.java +++ b/app/src/main/java/qianmu/container/activity/program/MyPresentation.java @@ -2651,7 +2651,10 @@ class MyPresentation extends Presentation { videoView2.setOnErrorListener(new CustomerVideoView.OnErrorListener() { @Override public boolean onError() { - //视频播放失败 + //视频播放失败:切下一个素材,避免出错后永久卡在当前帧 + LoggerUtil.e("MyPresentation", "videoView2播放出错,切换下一个素材"); + handler.removeMessages(TYPE_UPDATE_VIDEO0); + handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDEO0, 1000); return true; } }); @@ -2697,7 +2700,10 @@ class MyPresentation extends Presentation { videoView3.setOnErrorListener(new CustomerVideoView.OnErrorListener() { @Override public boolean onError() { - //视频播放失败 + //视频播放失败:切下一个素材,避免出错后永久卡在当前帧 + LoggerUtil.e("MyPresentation", "videoView3播放出错,切换下一个素材"); + handler.removeMessages(TYPE_UPDATE_VIDEO1); + handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDEO1, 1000); return true; } }); diff --git a/app/src/main/java/qianmu/container/activity/program/ScreenSaverActivity.java b/app/src/main/java/qianmu/container/activity/program/ScreenSaverActivity.java index 77e7cbc..d65a985 100644 --- a/app/src/main/java/qianmu/container/activity/program/ScreenSaverActivity.java +++ b/app/src/main/java/qianmu/container/activity/program/ScreenSaverActivity.java @@ -542,9 +542,14 @@ public class ScreenSaverActivity extends BaseActivity { public void dataCallBack(int code, String data) { LoggerUtil.e("QHT: 开关屏:", "code:" + code + ",data:" + data); if("CLOSE".equals(state)){ - MyApplication.getInstance().stopService(new Intent(MyApplication.getInstance(),MQTTService.class)); + if(Constant.mqttState.equals("on")){ + MQTTService.stopAndDisableReconnect(MyApplication.getInstance()); + } }else{ - MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); + if(Constant.mqttState.equals("off")){ + MQTTService.manualStopped = false; + MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); + } } if(code == 65350){ login(state); diff --git a/app/src/main/java/qianmu/container/activity/program/ViewScreenSaver.java b/app/src/main/java/qianmu/container/activity/program/ViewScreenSaver.java index f253449..28a4f31 100644 --- a/app/src/main/java/qianmu/container/activity/program/ViewScreenSaver.java +++ b/app/src/main/java/qianmu/container/activity/program/ViewScreenSaver.java @@ -21,6 +21,7 @@ import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.animation.Animation; import android.view.animation.TranslateAnimation; import android.webkit.ConsoleMessage; @@ -540,8 +541,8 @@ public class ViewScreenSaver extends ViewBase { binding.videoView0.setVideoPath(localPath); } else { // 图片 - Drawable drawable = new BitmapDrawable(localPath); - binding.videoView0.setBackground(drawable); + setSampledBackground(binding.videoView0, localPath, + components.getWidth(), components.getHeight()); } } } else { @@ -551,8 +552,8 @@ public class ViewScreenSaver extends ViewBase { binding.videoView0.setVideoPath(localPath); } else { // 图片 - Drawable drawable = new BitmapDrawable(localPath); - binding.videoView0.setBackground(drawable); + setSampledBackground(binding.videoView0, localPath, + components.getWidth(), components.getHeight()); } } @@ -1710,7 +1711,17 @@ public class ViewScreenSaver extends ViewBase { } if (webList != null && webList.size() > 0) { for (WebView webView : webList) { - binding.relativeLayoutMax.removeView(webView); + if (webView == null) continue; + try { + webView.stopLoading(); + webView.loadUrl("about:blank"); + webView.removeAllViews(); + ViewParent p = webView.getParent(); + if (p instanceof ViewGroup) ((ViewGroup) p).removeView(webView); + webView.destroy(); + } catch (Throwable t) { + LoggerUtil.e("deleteView()销毁WebView", StringUtil.getThrowableStr(t)); + } } webList.clear(); } @@ -2867,7 +2878,10 @@ public class ViewScreenSaver extends ViewBase { binding.videoView0.setOnErrorListener(new CustomerVideoView.OnErrorListener() { @Override public boolean onError() { - // 视频播放失败 + // 视频播放失败:切下一个素材,避免出错后永久卡在当前帧 + LoggerUtil.e("ViewScreenSaver", "videoView0播放出错,切换下一个素材"); + handler.removeMessages(TYPE_UPDATE_VIDEO0); + handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDEO0, 1000); return true; } }); @@ -2913,7 +2927,10 @@ public class ViewScreenSaver extends ViewBase { binding.videoView1.setOnErrorListener(new CustomerVideoView.OnErrorListener() { @Override public boolean onError() { - // 视频播放失败 + // 视频播放失败:切下一个素材,避免出错后永久卡在当前帧 + LoggerUtil.e("ViewScreenSaver", "videoView1播放出错,切换下一个素材"); + handler.removeMessages(TYPE_UPDATE_VIDEO1); + handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDEO1, 1000); return true; } }); @@ -2956,7 +2973,10 @@ public class ViewScreenSaver extends ViewBase { binding.videoView2.setOnErrorListener(new CustomerVideoView.OnErrorListener() { @Override public boolean onError() { - // 视频播放失败 + // 视频播放失败:切下一个素材,避免出错后永久卡在当前帧 + LoggerUtil.e("ViewScreenSaver", "videoView2播放出错,切换下一个素材"); + handler.removeMessages(TYPE_UPDATE_VIDEO2); + handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDEO2, 1000); return true; } }); @@ -2999,7 +3019,10 @@ public class ViewScreenSaver extends ViewBase { binding.videoView3.setOnErrorListener(new CustomerVideoView.OnErrorListener() { @Override public boolean onError() { - // 视频播放失败 + // 视频播放失败:切下一个素材,避免出错后永久卡在当前帧 + LoggerUtil.e("ViewScreenSaver", "videoView3播放出错,切换下一个素材"); + handler.removeMessages(TYPE_UPDATE_VIDEO3); + handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDEO3, 1000); return true; } }); @@ -3105,10 +3128,53 @@ public class ViewScreenSaver extends ViewBase { } handler.sendEmptyMessageDelayed(TYPE_UPDATE_VIDE, videoComponents.getConfig().getTransitionPeriod() * 1000); - Drawable drawable = new BitmapDrawable(localPath); - videoView.setBackground(drawable); + // 按目标尺寸降采样解码并回收旧背景,避免循环切换时全尺寸Bitmap累积导致native内存OOM静默重启 + setSampledBackground(videoView, localPath, videoComponents.getWidth(), videoComponents.getHeight()); + } + + } + + /** + * 给视频控件设置静态图背景:按目标尺寸降采样解码,并回收上一张背景Bitmap。 + * 替代废弃的 new BitmapDrawable(String) 全尺寸解码,防止内存泄漏导致播放过程中OOM重启。 + */ + private void setSampledBackground(View targetView, String localPath, int reqWidth, int reqHeight) { + try { + Drawable old = targetView.getBackground(); + + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(localPath, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + opts.inJustDecodeBounds = false; + Bitmap bitmap = BitmapFactory.decodeFile(localPath, opts); + if (bitmap == null) return; + + targetView.setBackground(new BitmapDrawable(context.getResources(), bitmap)); + + // 回收旧背景的Bitmap + if (old instanceof BitmapDrawable) { + Bitmap oldBmp = ((BitmapDrawable) old).getBitmap(); + if (oldBmp != null && !oldBmp.isRecycled()) oldBmp.recycle(); + } + } catch (Throwable t) { + LoggerUtil.e("setSampledBackground", StringUtil.getThrowableStr(t)); } + } + private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + if (reqWidth <= 0 || reqHeight <= 0) return inSampleSize; + if (height > reqHeight || width > reqWidth) { + int halfHeight = height / 2; + int halfWidth = width / 2; + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + return inSampleSize; } /** diff --git a/app/src/main/java/qianmu/container/app/Constant.java b/app/src/main/java/qianmu/container/app/Constant.java index 6136bc6..049dddc 100644 --- a/app/src/main/java/qianmu/container/app/Constant.java +++ b/app/src/main/java/qianmu/container/app/Constant.java @@ -24,12 +24,12 @@ public class Constant { public static String TTSHome="sbc"; // sbc-思必驰 kdxf-科大讯飞 (有语音的项目需要配置) // public static String androidBoardType = ""; //设备板子型号 无固定版 // public static String androidBoardType = "ys"; // 设备板子型号 ys(亿晟) 北京颐堤港定制touch - // public static String androidBoardType = "xwst"; //设备板子型号 xwst(欣威视通3399) + public static String androidBoardType = "xwst"; //设备板子型号 xwst(欣威视通3399) // public static String androidBoardType = "xwst2"; //设备板子型号 xwst2(欣威视通3588、T982、3576) // public static String androidBoardType = "zc"; //设备板子型号 zc(卓策主板——王府井喜悦、杨浦中心医院) // public static String androidBoardType = "sx"; //设备板子型号 sx(视想) // public static String androidBoardType = "nova"; //设备板子型号 诺瓦盒子 华贸LED - public static String androidBoardType = "huidu"; //设备板子型号 huidu(灰度主板) 罗湖寻车机 + // public static String androidBoardType = "huidu"; //设备板子型号 huidu(灰度主板) 罗湖寻车机 // public static String androidBoardType = "bv"; //设备板子型号 Bv-3588M // public static String androidBoardType = "smt"; //设备板子型号 视美泰 // public static String androidBoardType = "ctf"; //创泰丰 diff --git a/app/src/main/java/qianmu/container/data/PowerData.java b/app/src/main/java/qianmu/container/data/PowerData.java index d1edd97..3feff04 100644 --- a/app/src/main/java/qianmu/container/data/PowerData.java +++ b/app/src/main/java/qianmu/container/data/PowerData.java @@ -141,6 +141,21 @@ public class PowerData extends BaseData { } } + //欣威视通:定时开机时间前后40秒内重启一次设备(防止定时开机后黑屏/异常),每天仅重启一次 + if(Constant.androidBoardType.equals("xwst") && !bootTime.isEmpty()){ + long currentTime = System.currentTimeMillis(); + String s = TimeUtil.stampToDate(currentTime); + long bootLong = TimeUtil.pareTLong2(s + " " + bootTime);//开机时间 + String today = new SimpleDateFormat("yyyy-MM-dd").format(currentTime); + if(Math.abs(currentTime-bootLong)<300000 + && !DeviceData.getDeviceInfo(DeviceData.DEVICE_RESTART_TIME).equals(today)){ + LoggerUtil.e("PowerData","xwst开机时间后5分钟内,重启设备"); + DeviceData.saveDeviceInfo(DeviceData.DEVICE_RESTART_TIME, today); + SignWayUtil.reboot(); + return; + } + } + String dataJson = getDataJson(NAME, POWER_INFO, "{}"); //跳过开机的第一次设置 if(newTimeInfo.equals(dataJson)){ @@ -222,7 +237,7 @@ public class PowerData extends BaseData { long bootLong2 = bootLong+24*60*60*1000;//第二天开机时间 if(Constant.androidBoardType.equals("xwst")){ - + SignWayUtil.clearPowerOnOffTime(); if(parameterLong > bootLong) { //当天关机,第二天开机 SignWayUtil.setPowerOnTime("1", date2[0], date2[1], date2[2], on2[0], on2[1]); @@ -241,7 +256,6 @@ public class PowerData extends BaseData { LoggerUtil.e("PowerData()", "关机时间:"+s + " " + parameter+",开机时间:"+s + " " + bootTime); } } - }else if (Constant.androidBoardType.equals("xwst2")){ SdkApi.getInstance().TimerSwitch().setTimerSwitchOnoff(true); SdkApi.getInstance().TimerSwitch().setTimerType(true); diff --git a/app/src/main/java/qianmu/container/handler/ContainerHandler.java b/app/src/main/java/qianmu/container/handler/ContainerHandler.java index 5d843f5..a0e2723 100644 --- a/app/src/main/java/qianmu/container/handler/ContainerHandler.java +++ b/app/src/main/java/qianmu/container/handler/ContainerHandler.java @@ -78,6 +78,7 @@ public class ContainerHandler extends Handler { public static final int INIT_JXB2 = 11; //设置机械臂 public int goMemoryTime =0; private boolean isSetOver = false; //是否设置过开机时间了,默认没有设置过 + private boolean lastHdmiEnabled = true;//上次HDMI信号状态,用于检测off→on恢复以解除MQTT主动停止标志 private WeakReference weakReference; @@ -242,23 +243,85 @@ public class ContainerHandler extends Handler { // 获取 ActivityManager 服务 ActivityManager activityManager = (ActivityManager) MyApplication.getInstance().getSystemService(Context.ACTIVITY_SERVICE); - final Debug.MemoryInfo[] memInfo = activityManager.getProcessMemoryInfo(new int[]{android.os.Process.myPid()}); - final int totalPss = memInfo[0].getTotalPss(); + //部分定制ROM(本机rk3399 eng)下getTotalPss恒返回0,改用/proc/self/status的VmRSS作为可靠的主进程内存来源 + int rssMb = readProcRssMb(); + LoggerUtil.e("getMemory()","运行内存(RSS):"+rssMb+"MB"); - LoggerUtil.e("getMemory()","运行内存:"+totalPss/1024); - -// Long cpuUsage = getCpuUsage(); -// if(cpuUsage>50){ -// LoggerUtil.e("ContainerHandler","cpu使用率:"+cpuUsage+"%"); -// } + //检测WebView渲染进程的内存和显存(WebView渲染器运行在独立进程,OOM会导致渲染进程崩溃) + String deviceType = DeviceData.getDeviceInfo(DeviceData.HINT_DEVICE_TYPE); + if ("导视".equals(deviceType) || "水牌".equals(deviceType)) { + checkWebViewMemory(activityManager); + } - if(totalPss/1024>1200){ - //内存超过了1G会出现卡顿,内存溢出问题。重启设备。 + if(rssMb>2000){ + //内存超过了2.4G会出现卡顿,内存溢出问题。重启软件。 LoggerUtil.e("getMemory()","内存溢出重启软件"); EventBus.getDefault().post(new MessageEvent(Constant.ACTION_RESTART_APP)); } } + /** + * 读取本进程 /proc/self/status 的 VmRSS(物理内存占用),单位MB,失败返回0。 + * 用于替代部分定制ROM下恒返回0的 getProcessMemoryInfo().getTotalPss()。 + */ + private int readProcRssMb(){ + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/status"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("VmRSS:")) { + //形如: VmRSS: 123456 kB + String[] parts = line.trim().split("\\s+"); + return Integer.parseInt(parts[1]) / 1024; + } + } + } catch (Throwable t) { + LoggerUtil.e("readProcRssMb", StringUtil.getThrowableStr(t)); + } + return 0; + } + + /** + * 检测WebView渲染进程的内存和显存 + * WebView渲染器运行在独立的沙箱进程(sandboxed_process)中,其内存不计入主进程PSS, + * 渲染进程显存/内存过高会导致渲染进程被系统杀死(onRenderProcessGone),表现为app莫名重启。 + */ + private void checkWebViewMemory(ActivityManager activityManager){ + try { + List processes = activityManager.getRunningAppProcesses(); + if(processes == null) return; + String pkg = MyApplication.getInstance().getPackageName(); + for(ActivityManager.RunningAppProcessInfo info : processes){ + if(info.processName == null) continue; + //只统计本应用的WebView渲染/沙箱进程 + boolean isWebViewProcess = info.processName.contains("sandboxed_process") + || info.processName.contains("webview"); + if(!isWebViewProcess || !info.processName.startsWith(pkg)) continue; + + Debug.MemoryInfo[] wvMem = activityManager.getProcessMemoryInfo(new int[]{info.pid}); + if(wvMem == null || wvMem.length == 0) continue; + int wvPss = wvMem[0].getTotalPss(); + int wvGraphics = parseMemoryStat(wvMem[0], "summary.graphics"); + LoggerUtil.e("getMemory()","WebView渲染进程["+info.processName+"]内存:" + + wvPss/1024 + "MB,显存:" + wvGraphics/1024 + "MB"); + } + } catch (Throwable t){ + LoggerUtil.e("checkWebViewMemory", StringUtil.getThrowableStr(t)); + } + } + + /** + * 读取MemoryInfo的指定统计项(如summary.graphics),单位KB,解析失败返回0 + */ + private int parseMemoryStat(Debug.MemoryInfo memInfo, String key){ + try { + String value = memInfo.getMemoryStat(key); + if(value == null) return 0; + return Integer.parseInt(value); + } catch (Throwable t){ + return 0; + } + } + private long mLastCpuTime; private long mLastAppCpuTime; @@ -315,25 +378,33 @@ public class ContainerHandler extends Handler { try { if(Constant.androidBoardType.equals("xwst") && Constant.screenType.equals("HDMI")){ //欣威视通假关机 - LoggerUtil.e("mqttState","HDMI结果:"+RootCmdUtil.HDMIEnabled()+",mqttState结果:"+Constant.mqttState); - if(!RootCmdUtil.HDMIEnabled()){ + boolean hdmiOn = RootCmdUtil.HDMIEnabled(); + LoggerUtil.e("mqttState","HDMI结果:"+hdmiOn+",mqttState结果:"+Constant.mqttState); + boolean isService = DeviceUtil.isServiceRunning(MyApplication.getInstance(), "MQTTService"); + if(!hdmiOn){ //HDMI无信号 - if(Constant.mqttState.equals("on")){ + if(isService){ LoggerUtil.e("mqttState","HDMI无信号关闭MQTTService"); - MyApplication.getInstance().stopService(new Intent(MyApplication.getInstance(),MQTTService.class)); + MQTTService.stopAndDisableReconnect(MyApplication.getInstance()); } - }else { //HDMI有信号 - boolean isService = DeviceUtil.isServiceRunning(MyApplication.getInstance(), "MQTTService"); + if(!lastHdmiEnabled){ + //HDMI信号off→on恢复,解除主动停止标志,允许重新拉起MQTT + MQTTService.manualStopped = false; + } LoggerUtil.e("mqttState","MQTTService:"+isService); - if(Constant.mqttState.equals("off") || !isService){ - LoggerUtil.e("mqttState","HDMI有信号开启MQTTService"); - MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); - }else { - EventBus.getDefault().post(new MessageEvent(Constant.ACTION_MQTT_STATE));//通知mqtt状态判断 + //主动停止(stopService)后不自动拉起,直到HDMI/开屏恢复或重新启动服务 + if(!MQTTService.manualStopped){ + if(Constant.mqttState.equals("off") || !isService){ + LoggerUtil.e("mqttState","HDMI有信号开启MQTTService"); + MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); + }else { + EventBus.getDefault().post(new MessageEvent(Constant.ACTION_MQTT_STATE));//通知mqtt状态判断 + } } } + lastHdmiEnabled = hdmiOn; }else if(Constant.androidBoardType.equals("xwst2") && Constant.screenType.equals("HDMI")){ RootCmdUtil.checkHDMIEnabled(new HdmiStatusCallback() { @@ -344,27 +415,38 @@ public class ContainerHandler extends Handler { //HDMI无信号 if(Constant.mqttState.equals("on")){ LoggerUtil.e("mqttState","HDMI无信号关闭MQTTService"); - MyApplication.getInstance().stopService(new Intent(MyApplication.getInstance(),MQTTService.class)); + MQTTService.stopAndDisableReconnect(MyApplication.getInstance()); } }else { //HDMI有信号 + if(!lastHdmiEnabled){ + //HDMI信号off→on恢复,解除主动停止标志,允许重新拉起MQTT + MQTTService.manualStopped = false; + } boolean isService = DeviceUtil.isServiceRunning(MyApplication.getInstance(), "MQTTService"); LoggerUtil.e("mqttState","MQTTService:"+isService); - if(Constant.mqttState.equals("off") || !isService){ - LoggerUtil.e("mqttState","HDMI有信号开启MQTTService"); - MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); - }else { - EventBus.getDefault().post(new MessageEvent(Constant.ACTION_MQTT_STATE));//通知mqtt状态判断 + //主动停止(stopService)后不自动拉起,直到HDMI/开屏恢复或重新启动服务 + if(!MQTTService.manualStopped){ + if(Constant.mqttState.equals("off") || !isService){ + LoggerUtil.e("mqttState","HDMI有信号开启MQTTService"); + MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); + }else { + EventBus.getDefault().post(new MessageEvent(Constant.ACTION_MQTT_STATE));//通知mqtt状态判断 + } } } + lastHdmiEnabled = isEnabled; } }); }else { - if(Constant.mqttState.equals("off")){ - LoggerUtil.e("mqttState","MQTT被关闭了,开启MQTTService"); - MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); - }else { - EventBus.getDefault().post(new MessageEvent(Constant.ACTION_MQTT_STATE));//通知mqtt状态判断 + //非HDMI板:主动停止(stopService)后不自动拉起,待开屏/重新启动服务(onCreate清除标志)时恢复 + if(!MQTTService.manualStopped){ + if(Constant.mqttState.equals("off")){ + LoggerUtil.e("mqttState","MQTT被关闭了,开启MQTTService"); + MyApplication.getInstance().startService(new Intent(MyApplication.getInstance(),MQTTService.class)); + }else { + EventBus.getDefault().post(new MessageEvent(Constant.ACTION_MQTT_STATE));//通知mqtt状态判断 + } } } diff --git a/app/src/main/java/qianmu/container/mqtt/MQTTService.java b/app/src/main/java/qianmu/container/mqtt/MQTTService.java index 768ac71..d34226f 100644 --- a/app/src/main/java/qianmu/container/mqtt/MQTTService.java +++ b/app/src/main/java/qianmu/container/mqtt/MQTTService.java @@ -124,6 +124,7 @@ public class MQTTService extends Service { public static final String TAG = "MQTTService"; private static MqttAndroidClient client; + private static volatile MQTTService instance;//当前服务实例,用于内部重连 private MqttConnectOptions conOpt; static boolean isDownloadFile = false; private long programTime =0;//防止短时间接收多条信息 @@ -137,6 +138,9 @@ public class MQTTService extends Service { private int connectionLostNumb = 0;//失去连接次数 private long connectionLostTime = 0;//失去连接时间 private long connectTime = 0;//连接时间 + //主动停止标志:经stopService停止(onDestroy)时置true,抑制ContainerHandler.mqttState自动拉起与重连; + //仅在 服务重新启动(onCreate)/HDMI信号恢复/开屏 等"真正需要MQTT"时清除 + public static volatile boolean manualStopped = false; public MQTTService() { } @@ -150,7 +154,9 @@ public class MQTTService extends Service { @Override public void onCreate() { super.onCreate(); + instance = this; Constant.mqttState="on"; + manualStopped = false;//服务已(重新)启动,解除主动停止标志 LoggerUtil.e("MQTTService","onCreate"); // host = StringUtil.strSplice("tcp://",MqttData.getMqttInfo().getServer(), ":", MqttData.getMqttInfo().getPort()); host = StringUtil.strSplice("ssl://",MqttData.getMqttInfo().getServer(), ":", MqttData.getMqttInfo().getPort()); @@ -244,12 +250,47 @@ public class MQTTService extends Service { sendOffline(); disconnectMqtt(); EventBus.getDefault().unregister(this); + if(instance == this){ + instance = null; + } super.onDestroy(); } + /** + * 服务内部重连:断开旧连接后重新建连,不经过 stopService/startService 生命周期, + * 避免 static client 被旧服务实例 onDestroy 析构置空的竞态。 + */ + public static void restartConnection(){ + MQTTService s = instance; + if(s == null){ + LoggerUtil.e(TAG, "restartConnection: 服务实例为空,忽略重连"); + return; + } + LoggerUtil.e(TAG, "restartConnection: 服务内部重连"); + s.connectionLostNumb = 0; + s.isConnected = false; + s.disconnectMqtt(); + s.init(); + } + + /** + * 主动停止MQTT并抑制自动重连:先置manualStopped再stopService。 + * 与系统杀进程区分(onDestroy不置标志,仍可被mqttState自愈); + * 停止后仅在 HDMI信号恢复/开屏/重新启动服务(onCreate) 时恢复连接。 + */ + public static void stopAndDisableReconnect(Context context){ + manualStopped = true; + LoggerUtil.e(TAG, "主动停止MQTT并抑制自动重连"); + context.stopService(new Intent(context, MQTTService.class)); + } + /** 连接MQTT服务器 */ public void doClientConnection() { try { + if(manualStopped){ + LoggerUtil.e(TAG,"MQTT已被主动停止,跳过重连"); + return; + } if(connectionLostNumb>10){ LoggerUtil.e(TAG,"出现多次断线重连,通知重启mqtt服务"); EventBus.getDefault().post(new MessageEvent(Constant.ACTION_RESTART_MQTT)); @@ -282,19 +323,21 @@ public class MQTTService extends Service { client.subscribe(myTopic,2); sendOnline(); connectTime = System.currentTimeMillis(); - if(Math.abs(connectTime-connectionLostTime)>3600000){ + //首次连接(connectionLostTime=0)或断连超60分钟才视为"需要刷新",短时抖动重连不刷新 + boolean longGap = Math.abs(connectTime-connectionLostTime)>3600000; + if(longGap){ //连接时间与连接失败时间相差60分钟,连接上时自动拉取最新节目 LoggerUtil.e("MQTTService","连接成功与连接失败时间相差60分钟,连接成功主动拉取最新节目"); programPublish(); } - + String deviceType = DeviceData.getDeviceInfo(DeviceData.HINT_DEVICE_TYPE); + if (longGap && ("信发".equals(deviceType) || "双面屏".equals(deviceType))){ + //仅在首次连接/断连超60分钟时刷新WebView;避免MQTT短时抖动重连反复reload扰动播放导致卡屏 + EventBus.getDefault().post(new MessageEvent(Constant.ACTION_UPDATE_WEBVIEW)); + } } catch (MqttException e) { e.printStackTrace(); } - if ("信发".equals(DeviceData.getDeviceInfo(DeviceData.HINT_DEVICE_TYPE)) - || "双面屏".equals(DeviceData.getDeviceInfo(DeviceData.HINT_DEVICE_TYPE))){ - EventBus.getDefault().post(new MessageEvent(Constant.ACTION_UPDATE_WEBVIEW)); - } } @Override @@ -343,7 +386,6 @@ public class MQTTService extends Service { } connectionLostTime = time; LoggerUtil.e(TAG, " 失去连接: 时间:"+connectionLostTime+",次数:"+connectionLostNumb); - } }; diff --git a/app/src/main/java/qianmu/container/util/LoggerUtil.java b/app/src/main/java/qianmu/container/util/LoggerUtil.java index 666ca51..d270d87 100644 --- a/app/src/main/java/qianmu/container/util/LoggerUtil.java +++ b/app/src/main/java/qianmu/container/util/LoggerUtil.java @@ -14,6 +14,7 @@ import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import okhttp3.MediaType; @@ -105,9 +106,12 @@ public class LoggerUtil { } + //写日志的共享线程池(全局复用,避免每条日志都新建线程导致线程泄漏OOM) + private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor(); + //将日志信息保存至SD卡(异步,正常日志使用) public static synchronized void storeLog(String strModule, String strErrMsg) { - Executors.newSingleThreadExecutor().execute(() -> writeLogToFile(strModule, strErrMsg)); + LOG_EXECUTOR.execute(() -> writeLogToFile(strModule, strErrMsg)); } //将日志信息同步保存至SD卡(崩溃时使用,确保进程退出前写入完成) diff --git a/app/src/main/java/qianmu/container/util/SignWayUtil.java b/app/src/main/java/qianmu/container/util/SignWayUtil.java index 4930001..beb7628 100644 --- a/app/src/main/java/qianmu/container/util/SignWayUtil.java +++ b/app/src/main/java/qianmu/container/util/SignWayUtil.java @@ -297,6 +297,8 @@ public class SignWayUtil { try{ if(Constant.androidBoardType.equals("xwst")){ + setPowerOffTime("0","1970","1","1","0","0"); + setPowerOnTime("0","1970","1","1","0","0"); setPowerOffTime("1","1970","1","1","0","0"); setPowerOnTime("1","1970","1","1","0","0"); diff --git a/app/src/main/java/qianmu/container/view/CustomerVideoView.java b/app/src/main/java/qianmu/container/view/CustomerVideoView.java index 082d8f0..54a9d11 100644 --- a/app/src/main/java/qianmu/container/view/CustomerVideoView.java +++ b/app/src/main/java/qianmu/container/view/CustomerVideoView.java @@ -6,12 +6,15 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; +import android.view.LayoutInflater; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import qianmu.container.R; + import androidx.annotation.NonNull; import com.google.android.exoplayer2.DefaultRenderersFactory; @@ -58,8 +61,10 @@ public class CustomerVideoView extends FrameLayout { } private void init(Context context) { - // 1. 创建 StyledPlayerView - playerView = new StyledPlayerView(context); + // 1. 创建 StyledPlayerView —— 从XML inflate以启用 surface_type=texture_view + // (用TextureView替代默认SurfaceView,避免多路视频同播时争抢硬件overlay平面导致黑屏) + playerView = (StyledPlayerView) LayoutInflater.from(context) + .inflate(R.layout.view_exo_texture_player, this, false); addView(playerView); // 2. 初始化播放器 DefaultRenderersFactory factory = diff --git a/app/src/main/res/layout/view_exo_texture_player.xml b/app/src/main/res/layout/view_exo_texture_player.xml new file mode 100644 index 0000000..06da0d2 --- /dev/null +++ b/app/src/main/res/layout/view_exo_texture_player.xml @@ -0,0 +1,16 @@ + + +