diff --git a/app/src/main/java/com/ttstd/dialer/bean/DeviceStatus.java b/app/src/main/java/com/ttstd/dialer/bean/DeviceStatus.java new file mode 100644 index 0000000..720e39d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/DeviceStatus.java @@ -0,0 +1,73 @@ +package com.ttstd.dialer.bean; + +import com.google.gson.annotations.SerializedName; + +/** + * MQTT 设备状态上报实体(心跳 / 屏幕亮灭)。 + *
+ * 序列化为 JSON:{"sn":"...","status":"online","ip":"...","screenOn":true|false}
+ */
+public class DeviceStatus {
+ @SerializedName("sn")
+ private String sn;
+
+ @SerializedName("online")
+ private boolean online;
+
+ @SerializedName("ip")
+ private String ip;
+
+ @SerializedName("rssi")
+ private Integer rssi;
+
+ @SerializedName("screenOn")
+ private boolean screenOn;
+
+ public DeviceStatus(String sn, boolean online, String ip, Integer rssi, boolean screenOn) {
+ this.sn = sn;
+ this.online = online;
+ this.ip = ip;
+ this.rssi = rssi;
+ this.screenOn = screenOn;
+ }
+
+ public String getSn() {
+ return sn;
+ }
+
+ public void setSn(String sn) {
+ this.sn = sn;
+ }
+
+ public boolean isOnline() {
+ return online;
+ }
+
+ public void setOnline(boolean online) {
+ this.online = online;
+ }
+
+ public String getIp() {
+ return ip;
+ }
+
+ public void setIp(String ip) {
+ this.ip = ip;
+ }
+
+ public Integer getRssi() {
+ return rssi;
+ }
+
+ public void setRssi(Integer rssi) {
+ this.rssi = rssi;
+ }
+
+ public boolean isScreenOn() {
+ return screenOn;
+ }
+
+ public void setScreenOn(boolean screenOn) {
+ this.screenOn = screenOn;
+ }
+}
diff --git a/app/src/main/java/com/ttstd/dialer/manager/MqttManager.java b/app/src/main/java/com/ttstd/dialer/manager/MqttManager.java
index a7d556e..3194d25 100644
--- a/app/src/main/java/com/ttstd/dialer/manager/MqttManager.java
+++ b/app/src/main/java/com/ttstd/dialer/manager/MqttManager.java
@@ -9,8 +9,15 @@ import android.text.TextUtils;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import com.hivemq.client.mqtt.mqtt5.Mqtt5ClientBuilder;
+import com.hivemq.client.mqtt.lifecycle.MqttClientConnectedContext;
+import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedContext;
+import com.hivemq.client.mqtt.lifecycle.MqttDisconnectSource;
+import com.hivemq.client.mqtt.mqtt5.lifecycle.Mqtt5ClientConnectedContext;
+import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck;
import com.jeremyliao.liveeventbus.LiveEventBus;
+import com.ttstd.dialer.bean.DeviceStatus;
import com.ttstd.dialer.config.CommonConfig;
+import com.ttstd.dialer.gson.GsonUtils;
import com.ttstd.dialer.mdm.DeviceManagerService;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.utils.SystemUtils;
@@ -24,6 +31,7 @@ import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
/**
* MQTT 消息管理
@@ -39,9 +47,9 @@ public class MqttManager {
/*MQTT 配置*/
public static final String MQTT_URL = "mqtt://175.178.213.60:1883";
- public static final String MQTT_CLIENT_ID = "ttstd-dialer-android";
- public static final String MQTT_USERNAME = "";
- public static final String MQTT_PASSWORD = "";
+ // public static final String MQTT_CLIENT_ID = "ttstd-dialer-android";
+ public static final String MQTT_USERNAME = "tt";
+ public static final String MQTT_PASSWORD = "fanhuitong";
/*订阅主题*/
public static final String MQTT_TOPIC_COMMAND = "ttstd/device/command";
/*发布主题*/
@@ -62,16 +70,6 @@ public class MqttManager {
*/
private static final long HEARTBEAT_INTERVAL_MS = 30_000L;
- /**
- * 重连退避初始间隔(毫秒)
- */
- private static final long RECONNECT_DELAY_INITIAL_MS = 1_000L;
-
- /**
- * 重连退避间隔上限(毫秒),超过此值不再翻倍
- */
- private static final long RECONNECT_DELAY_MAX_MS = 64_000L;
-
@SuppressLint("StaticFieldLeak")
private static volatile MqttManager INSTANCE;
@@ -79,6 +77,11 @@ public class MqttManager {
private Mqtt5AsyncClient mMqttClient;
private volatile boolean mConnected;
+ /**
+ * 正在连接标记,防止并发调用 connect() 时重复 build 多个相同 clientId 的 client 互相踢
+ */
+ private volatile boolean mConnecting;
+
/**
* 主动断开标记,避免主动断开时触发自动重连
*/
@@ -89,16 +92,6 @@ public class MqttManager {
*/
private final Handler mHeartbeatHandler = new Handler(Looper.getMainLooper());
- /**
- * 重连定时器(主线程 Handler)
- */
- private final Handler mReconnectHandler = new Handler(Looper.getMainLooper());
-
- /**
- * 重连退避间隔(毫秒),随失败次数指数增长
- */
- private long mReconnectDelayMs = RECONNECT_DELAY_INITIAL_MS;
-
private boolean mHeartbeatRunning;
/**
@@ -146,7 +139,8 @@ public class MqttManager {
if (TextUtils.isEmpty(sn)) {
sn = "";
}
- return ("{\"sn\":\"" + sn + "\",\"status\":\"offline\"}").getBytes(StandardCharsets.UTF_8);
+ return GsonUtils.toJSONString(new DeviceStatus(sn, true, getLocalIpAddress(), SystemUtils.getCurrentNetworkRssi(mContext), false))
+ .getBytes(StandardCharsets.UTF_8);
}
/**
@@ -157,8 +151,14 @@ public class MqttManager {
if (mMqttClient != null && mConnected) {
return;
}
+ // 已在连接中(上一次 connect() 的异步回调尚未返回),避免并发重复建连导致相同 clientId 互踢
+ if (mConnecting) {
+ Logger.e(TAG, "connect: 已有连接在进行中,跳过重复 connect");
+ return;
+ }
// 主动发起连接,清除主动断开标记,允许后续自动重连
mManualDisconnect = false;
+ mConnecting = true;
if (mMqttClient != null) {
try {
mMqttClient.disconnect();
@@ -172,46 +172,43 @@ public class MqttManager {
int port = uri.getPort() > 0 ? uri.getPort() : 1883;
Mqtt5ClientBuilder builder = Mqtt5Client.builder()
- .identifier(MQTT_CLIENT_ID)
+ .identifier(SystemUtils.getSerial())
.serverHost(host)
.serverPort(port)
- .addDisconnectedListener(context -> onDisconnected());
- if (!TextUtils.isEmpty(MQTT_USERNAME)) {
- builder.simpleAuth()
- .username(MQTT_USERNAME)
- .password(MQTT_PASSWORD.getBytes(StandardCharsets.UTF_8))
- .applySimpleAuth();
- }
+ // HiveMQ 自带断线自动重连(内置指数退避),避免与自定义重连叠加导致重复连接
+ .automaticReconnectWithDefaultConfig()
+ .addDisconnectedListener(this::onDisconnected)
+ .addConnectedListener(this::onConnected);
+
+ builder.simpleAuth()
+ .username(MQTT_USERNAME)
+ .password(MQTT_PASSWORD.getBytes(StandardCharsets.UTF_8))
+ .applySimpleAuth();
mMqttClient = builder.buildAsync();
mMqttClient.connectWith()
.keepAlive(30)
+ .cleanStart(false) // 复用历史会话
+ .sessionExpiryInterval(TimeUnit.DAYS.toSeconds(1)) // 断开后会话保留
.willPublish()
- .topic(MQTT_TOPIC_STATUS)
- .payload(buildWillPayload())
- .qos(com.hivemq.client.mqtt.datatypes.MqttQos.AT_LEAST_ONCE)
- .retain(false)
- .applyWillPublish()
+ .topic(MQTT_TOPIC_STATUS)
+ .payload(buildWillPayload())
+ .qos(com.hivemq.client.mqtt.datatypes.MqttQos.AT_LEAST_ONCE)
+ .retain(false)
+ .applyWillPublish()
.send()
.whenComplete((connAck, throwable) -> {
+ // 首次 connect 异步结果返回,复位连接中标记
+ mConnecting = false;
if (throwable != null) {
String msg = throwable.getMessage();
- Logger.e(TAG, "connect: 连接失败 " + msg
- + ",将自动重试连接 " + host + ":" + port);
+ Logger.e(TAG, "connect: 首次连接失败 " + msg
+ + ",HiveMQ 将自动重连 " + host + ":" + port);
setConnected(false);
- // 首次连接失败也安排重连(网络不可达等场景)
- scheduleReconnect();
} else {
- Logger.e(TAG, "connect: 连接成功");
- mReconnectDelayMs = RECONNECT_DELAY_INITIAL_MS;
- setConnected(true);
- // 连接成功后重新订阅之前订阅的主题
- reSubscribeAll();
- // 连接成功后启动心跳上报
- startHeartbeat();
- // 连接成功后上报一次当前屏幕开关状态
- reportScreenStateOnConnect();
+ // 首次连接成功(后续重连成功会走 onConnected 回调)
+ onConnected();
}
});
} catch (Throwable e) {
@@ -221,55 +218,71 @@ public class MqttManager {
}
/**
- * 断线回调:若非主动断开,则按指数退避安排重连
+ * 连接成功回调(首次 connect 异步结果返回时调用,无 context 信息)
*/
- private void onDisconnected() {
+ private void onConnected() {
+ onConnected(null);
+ }
+
+ /**
+ * 连接成功回调(首次连接与 HiveMQ 自动重连成功都会触发)
+ */
+ private void onConnected(MqttClientConnectedContext context) {
+ boolean sessionPresent = false;
+ if (context instanceof Mqtt5ClientConnectedContext) {
+ Mqtt5ConnAck connAck = ((Mqtt5ClientConnectedContext) context).getConnAck();
+ sessionPresent = connAck != null && connAck.isSessionPresent();
+ }
+ Logger.e(TAG, "onConnected: 连接成功 sessionPresent=" + sessionPresent);
+ setConnected(true);
+ // 连接成功后重新订阅之前订阅的主题
+ reSubscribeAll();
+ // 连接成功后启动心跳上报
+ startHeartbeat();
+ // 连接成功后上报一次当前屏幕开关状态
+ reportScreenStateOnConnect();
+ }
+
+ /**
+ * 断线回调:仅做状态清理与心跳停止,重连交由 HiveMQ 自动重连处理,
+ * 不再自行调用 connect(),避免与 HiveMQ 的重连叠加导致重复连接。
+ * 同时打印 Broker 返回的断开原因,便于定位反复断开的根因。
+ */
+ private void onDisconnected(MqttClientDisconnectedContext context) {
if (mManualDisconnect) {
Logger.e(TAG, "onDisconnected: 主动断开,不重连");
return;
}
- Logger.e(TAG, "onDisconnected: 连接断开,准备自动重连");
+ // 解析断开来源与原因,区分"服务端踢"与"网络/客户端问题"
+ String source = context.getSource() != null ? context.getSource().name() : "UNKNOWN";
+ String cause = context.getCause() != null ? context.getCause().toString() : "null";
+ if (context.getSource() == MqttDisconnectSource.SERVER) {
+ // 服务端主动断开(如 clientId 冲突、鉴权失败、协议违规),reason code 有价值
+ Logger.e(TAG, "onDisconnected: 服务端主动断开 source=" + source
+ + " cause=" + cause
+ + ",等待 HiveMQ 自动重连");
+ } else {
+ Logger.e(TAG, "onDisconnected: 连接断开 source=" + source
+ + " cause=" + cause
+ + ",等待 HiveMQ 自动重连");
+ }
setConnected(false);
stopHeartbeat();
- scheduleReconnect();
}
- /**
- * 安排一次延迟重连(指数退避,上限 30s)
- */
- private void scheduleReconnect() {
- mReconnectHandler.removeCallbacks(mReconnectRunnable);
- Logger.e(TAG, "scheduleReconnect: " + (mReconnectDelayMs / 1000) + "s 后重连");
- mReconnectHandler.postDelayed(mReconnectRunnable, mReconnectDelayMs);
- // 下次退避间隔翻倍,封顶 30s
- mReconnectDelayMs = Math.min(mReconnectDelayMs * 2, RECONNECT_DELAY_MAX_MS);
- }
-
- private final Runnable mReconnectRunnable = new Runnable() {
- @Override
- public void run() {
- if (mManualDisconnect) {
- return;
- }
- Logger.e(TAG, "reconnect: 尝试重新连接...");
- connect();
- }
- };
-
/**
* 断开 MQTT 连接(主动断开,先发离线消息再断连,不会触发遗言)
*/
public void disconnect() {
- // 标记主动断开,重连任务与断线监听均据此跳过重连
+ // 标记主动断开,断线监听据此跳过重连
mManualDisconnect = true;
- mReconnectHandler.removeCallbacks(mReconnectRunnable);
stopHeartbeat();
try {
if (mMqttClient != null && mConnected) {
// 主动上报离线消息,确保服务端即时感知设备下线
String sn = DeviceManagerService.getInstance().getSerial();
if (!TextUtils.isEmpty(sn)) {
- String offlinePayload = "{\"sn\":\"" + sn + "\",\"status\":\"offline\"}";
+ String offlinePayload = GsonUtils.toJSONString(new DeviceStatus(sn, false, getLocalIpAddress(), SystemUtils.getCurrentNetworkRssi(mContext), false));
publish(MQTT_TOPIC_STATUS, offlinePayload);
}
mMqttClient.disconnect();
@@ -427,12 +440,11 @@ public class MqttManager {
}
/**
- * 构建设备状态上报 JSON:{"sn":"...","status":"online","ip":"...","screenOn":true|false}
+ * 构建设备状态上报 JSON:{"sn":"...","online":"online","ip":"...","screenOn":true|false}
*/
private String buildStatusPayload(String sn, boolean screenOn) {
- String ip = getLocalIpAddress();
- return "{\"sn\":\"" + sn + "\",\"status\":\"online\",\"ip\":\"" + (ip == null ? "" : ip)
- + "\",\"screenOn\":" + screenOn + "}";
+ DeviceStatus status = new DeviceStatus(sn, true, getLocalIpAddress(), SystemUtils.getCurrentNetworkRssi(mContext), screenOn);
+ return GsonUtils.toJSONString(status);
}
/**
@@ -452,7 +464,7 @@ public class MqttManager {
} catch (Throwable e) {
Logger.e(TAG, "getLocalIpAddress: " + e.getMessage());
}
- return "";
+ return "127.0.0.1";
}
private void onMessageReceived(com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish publish) {
diff --git a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java
index 3bcccc4..4c4bdce 100644
--- a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java
+++ b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java
@@ -18,6 +18,8 @@ import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.graphics.Bitmap;
import android.graphics.Rect;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
@@ -26,9 +28,13 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.MediaStore;
import android.provider.Settings;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrength;
+import android.telephony.SignalStrength;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -78,15 +84,14 @@ public class SystemUtils {
* @return
*/
public static String getProperty(String key, String defaultValue) {
- String value = defaultValue;
+// SystemProperties.get();
try {
Class> c = Class.forName("android.os.SystemProperties");
Method get = c.getMethod("get", String.class, String.class);
- value = (String) (get.invoke(c, key, Build.UNKNOWN));
+ return (String) (get.invoke(c, key, defaultValue));
} catch (Exception e) {
e.printStackTrace();
- } finally {
- return value;
+ return defaultValue;
}
}
@@ -366,6 +371,135 @@ public class SystemUtils {
return "";
}
+ /**
+ * 获取当前蜂窝信号强度等级(0~4,对应 ASU 的粗略分级,4 最强)。
+ * 该值可用于状态栏信号格显示,与 {@link android.telephony.SignalStrength#getLevel()} 一致。
+ *
+ * @param context 上下文
+ * @return 信号等级 0~4;无 SIM / 非蜂窝网络 / 获取失败返回 -1
+ */
+ @SuppressLint("MissingPermission")
+ public static int getSignalLevel(Context context) {
+ try {
+ TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm == null) {
+ return -1;
+ }
+ // Android 9 (API 28) 及以下无 getAllCellInfo/SignalStrength 的公开 getLevel,
+ // 这里统一用 getAllCellInfo 取最强小区的 level,覆盖绝大多数系统版本。
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ List