refactor(mqtt): 优化MQTT连接管理与设备状态上报
- 使用DeviceStatus对象替代手动拼接JSON字符串 - 启用HiveMQ自动重连机制,移除自定义重连逻辑 - 添加连接状态标记防止并发重复建连 - 优化断线回调,记录服务端断开原因 - 使用设备序列号作为MQTT客户端标识 - 新增信号强度和真实屏幕分辨率获取方法 - 修复SystemUtils中系统属性获取方式 - 添加DeviceStatus单元测试
This commit is contained in:
73
app/src/main/java/com/ttstd/dialer/bean/DeviceStatus.java
Normal file
73
app/src/main/java/com/ttstd/dialer/bean/DeviceStatus.java
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ttstd.dialer.bean;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT 设备状态上报实体(心跳 / 屏幕亮灭)。
|
||||||
|
* <p>
|
||||||
|
* 序列化为 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,15 @@ import android.text.TextUtils;
|
|||||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
|
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
|
||||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
|
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
|
||||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5ClientBuilder;
|
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.jeremyliao.liveeventbus.LiveEventBus;
|
||||||
|
import com.ttstd.dialer.bean.DeviceStatus;
|
||||||
import com.ttstd.dialer.config.CommonConfig;
|
import com.ttstd.dialer.config.CommonConfig;
|
||||||
|
import com.ttstd.dialer.gson.GsonUtils;
|
||||||
import com.ttstd.dialer.mdm.DeviceManagerService;
|
import com.ttstd.dialer.mdm.DeviceManagerService;
|
||||||
import com.ttstd.dialer.utils.Logger;
|
import com.ttstd.dialer.utils.Logger;
|
||||||
import com.ttstd.dialer.utils.SystemUtils;
|
import com.ttstd.dialer.utils.SystemUtils;
|
||||||
@@ -24,6 +31,7 @@ import java.util.Collections;
|
|||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MQTT 消息管理
|
* MQTT 消息管理
|
||||||
@@ -39,9 +47,9 @@ public class MqttManager {
|
|||||||
|
|
||||||
/*MQTT 配置*/
|
/*MQTT 配置*/
|
||||||
public static final String MQTT_URL = "mqtt://175.178.213.60:1883";
|
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_CLIENT_ID = "ttstd-dialer-android";
|
||||||
public static final String MQTT_USERNAME = "";
|
public static final String MQTT_USERNAME = "tt";
|
||||||
public static final String MQTT_PASSWORD = "";
|
public static final String MQTT_PASSWORD = "fanhuitong";
|
||||||
/*订阅主题*/
|
/*订阅主题*/
|
||||||
public static final String MQTT_TOPIC_COMMAND = "ttstd/device/command";
|
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 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")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private static volatile MqttManager INSTANCE;
|
private static volatile MqttManager INSTANCE;
|
||||||
|
|
||||||
@@ -79,6 +77,11 @@ public class MqttManager {
|
|||||||
private Mqtt5AsyncClient mMqttClient;
|
private Mqtt5AsyncClient mMqttClient;
|
||||||
private volatile boolean mConnected;
|
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());
|
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;
|
private boolean mHeartbeatRunning;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,7 +139,8 @@ public class MqttManager {
|
|||||||
if (TextUtils.isEmpty(sn)) {
|
if (TextUtils.isEmpty(sn)) {
|
||||||
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) {
|
if (mMqttClient != null && mConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 已在连接中(上一次 connect() 的异步回调尚未返回),避免并发重复建连导致相同 clientId 互踢
|
||||||
|
if (mConnecting) {
|
||||||
|
Logger.e(TAG, "connect: 已有连接在进行中,跳过重复 connect");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 主动发起连接,清除主动断开标记,允许后续自动重连
|
// 主动发起连接,清除主动断开标记,允许后续自动重连
|
||||||
mManualDisconnect = false;
|
mManualDisconnect = false;
|
||||||
|
mConnecting = true;
|
||||||
if (mMqttClient != null) {
|
if (mMqttClient != null) {
|
||||||
try {
|
try {
|
||||||
mMqttClient.disconnect();
|
mMqttClient.disconnect();
|
||||||
@@ -172,21 +172,25 @@ public class MqttManager {
|
|||||||
int port = uri.getPort() > 0 ? uri.getPort() : 1883;
|
int port = uri.getPort() > 0 ? uri.getPort() : 1883;
|
||||||
|
|
||||||
Mqtt5ClientBuilder builder = Mqtt5Client.builder()
|
Mqtt5ClientBuilder builder = Mqtt5Client.builder()
|
||||||
.identifier(MQTT_CLIENT_ID)
|
.identifier(SystemUtils.getSerial())
|
||||||
.serverHost(host)
|
.serverHost(host)
|
||||||
.serverPort(port)
|
.serverPort(port)
|
||||||
.addDisconnectedListener(context -> onDisconnected());
|
// HiveMQ 自带断线自动重连(内置指数退避),避免与自定义重连叠加导致重复连接
|
||||||
if (!TextUtils.isEmpty(MQTT_USERNAME)) {
|
.automaticReconnectWithDefaultConfig()
|
||||||
|
.addDisconnectedListener(this::onDisconnected)
|
||||||
|
.addConnectedListener(this::onConnected);
|
||||||
|
|
||||||
builder.simpleAuth()
|
builder.simpleAuth()
|
||||||
.username(MQTT_USERNAME)
|
.username(MQTT_USERNAME)
|
||||||
.password(MQTT_PASSWORD.getBytes(StandardCharsets.UTF_8))
|
.password(MQTT_PASSWORD.getBytes(StandardCharsets.UTF_8))
|
||||||
.applySimpleAuth();
|
.applySimpleAuth();
|
||||||
}
|
|
||||||
|
|
||||||
mMqttClient = builder.buildAsync();
|
mMqttClient = builder.buildAsync();
|
||||||
|
|
||||||
mMqttClient.connectWith()
|
mMqttClient.connectWith()
|
||||||
.keepAlive(30)
|
.keepAlive(30)
|
||||||
|
.cleanStart(false) // 复用历史会话
|
||||||
|
.sessionExpiryInterval(TimeUnit.DAYS.toSeconds(1)) // 断开后会话保留
|
||||||
.willPublish()
|
.willPublish()
|
||||||
.topic(MQTT_TOPIC_STATUS)
|
.topic(MQTT_TOPIC_STATUS)
|
||||||
.payload(buildWillPayload())
|
.payload(buildWillPayload())
|
||||||
@@ -195,23 +199,16 @@ public class MqttManager {
|
|||||||
.applyWillPublish()
|
.applyWillPublish()
|
||||||
.send()
|
.send()
|
||||||
.whenComplete((connAck, throwable) -> {
|
.whenComplete((connAck, throwable) -> {
|
||||||
|
// 首次 connect 异步结果返回,复位连接中标记
|
||||||
|
mConnecting = false;
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
String msg = throwable.getMessage();
|
String msg = throwable.getMessage();
|
||||||
Logger.e(TAG, "connect: 连接失败 " + msg
|
Logger.e(TAG, "connect: 首次连接失败 " + msg
|
||||||
+ ",将自动重试连接 " + host + ":" + port);
|
+ ",HiveMQ 将自动重连 " + host + ":" + port);
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
// 首次连接失败也安排重连(网络不可达等场景)
|
|
||||||
scheduleReconnect();
|
|
||||||
} else {
|
} else {
|
||||||
Logger.e(TAG, "connect: 连接成功");
|
// 首次连接成功(后续重连成功会走 onConnected 回调)
|
||||||
mReconnectDelayMs = RECONNECT_DELAY_INITIAL_MS;
|
onConnected();
|
||||||
setConnected(true);
|
|
||||||
// 连接成功后重新订阅之前订阅的主题
|
|
||||||
reSubscribeAll();
|
|
||||||
// 连接成功后启动心跳上报
|
|
||||||
startHeartbeat();
|
|
||||||
// 连接成功后上报一次当前屏幕开关状态
|
|
||||||
reportScreenStateOnConnect();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Throwable e) {
|
} 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) {
|
if (mManualDisconnect) {
|
||||||
Logger.e(TAG, "onDisconnected: 主动断开,不重连");
|
Logger.e(TAG, "onDisconnected: 主动断开,不重连");
|
||||||
return;
|
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);
|
setConnected(false);
|
||||||
stopHeartbeat();
|
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 连接(主动断开,先发离线消息再断连,不会触发遗言)
|
* 断开 MQTT 连接(主动断开,先发离线消息再断连,不会触发遗言)
|
||||||
*/
|
*/
|
||||||
public void disconnect() {
|
public void disconnect() {
|
||||||
// 标记主动断开,重连任务与断线监听均据此跳过重连
|
// 标记主动断开,断线监听据此跳过重连
|
||||||
mManualDisconnect = true;
|
mManualDisconnect = true;
|
||||||
mReconnectHandler.removeCallbacks(mReconnectRunnable);
|
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
try {
|
try {
|
||||||
if (mMqttClient != null && mConnected) {
|
if (mMqttClient != null && mConnected) {
|
||||||
// 主动上报离线消息,确保服务端即时感知设备下线
|
// 主动上报离线消息,确保服务端即时感知设备下线
|
||||||
String sn = DeviceManagerService.getInstance().getSerial();
|
String sn = DeviceManagerService.getInstance().getSerial();
|
||||||
if (!TextUtils.isEmpty(sn)) {
|
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);
|
publish(MQTT_TOPIC_STATUS, offlinePayload);
|
||||||
}
|
}
|
||||||
mMqttClient.disconnect();
|
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) {
|
private String buildStatusPayload(String sn, boolean screenOn) {
|
||||||
String ip = getLocalIpAddress();
|
DeviceStatus status = new DeviceStatus(sn, true, getLocalIpAddress(), SystemUtils.getCurrentNetworkRssi(mContext), screenOn);
|
||||||
return "{\"sn\":\"" + sn + "\",\"status\":\"online\",\"ip\":\"" + (ip == null ? "" : ip)
|
return GsonUtils.toJSONString(status);
|
||||||
+ "\",\"screenOn\":" + screenOn + "}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -452,7 +464,7 @@ public class MqttManager {
|
|||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Logger.e(TAG, "getLocalIpAddress: " + e.getMessage());
|
Logger.e(TAG, "getLocalIpAddress: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return "";
|
return "127.0.0.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMessageReceived(com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish publish) {
|
private void onMessageReceived(com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish publish) {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import android.content.pm.ResolveInfo;
|
|||||||
import android.content.pm.UserInfo;
|
import android.content.pm.UserInfo;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
import android.net.wifi.WifiInfo;
|
import android.net.wifi.WifiInfo;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -26,9 +28,13 @@ import android.os.IBinder;
|
|||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import android.os.SystemProperties;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.telephony.CellInfo;
|
||||||
|
import android.telephony.CellSignalStrength;
|
||||||
|
import android.telephony.SignalStrength;
|
||||||
import android.telephony.SubscriptionInfo;
|
import android.telephony.SubscriptionInfo;
|
||||||
import android.telephony.SubscriptionManager;
|
import android.telephony.SubscriptionManager;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
@@ -78,15 +84,14 @@ public class SystemUtils {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String getProperty(String key, String defaultValue) {
|
public static String getProperty(String key, String defaultValue) {
|
||||||
String value = defaultValue;
|
// SystemProperties.get();
|
||||||
try {
|
try {
|
||||||
Class<?> c = Class.forName("android.os.SystemProperties");
|
Class<?> c = Class.forName("android.os.SystemProperties");
|
||||||
Method get = c.getMethod("get", String.class, String.class);
|
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) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} finally {
|
return defaultValue;
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +371,135 @@ public class SystemUtils {
|
|||||||
return "";
|
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<CellInfo> cellInfos = tm.getAllCellInfo();
|
||||||
|
if (cellInfos != null) {
|
||||||
|
int maxLevel = -1;
|
||||||
|
for (CellInfo info : cellInfos) {
|
||||||
|
if (info == null || !info.isRegistered()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CellSignalStrength strength = info.getCellSignalStrength();
|
||||||
|
if (strength != null) {
|
||||||
|
maxLevel = Math.max(maxLevel, strength.getLevel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxLevel >= 0) {
|
||||||
|
return maxLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 兜底:API 29+ 可直接取 SignalStrength 的 level
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
SignalStrength signalStrength = tm.getSignalStrength();
|
||||||
|
if (signalStrength != null) {
|
||||||
|
return signalStrength.getLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前蜂窝信号强度(dBm,如 -75 表示较强,-113 接近无信号)。
|
||||||
|
* 优先使用各制式 CellInfo 的 dBm,找不到则回退到 ASU 换算(asu - 113 - 2 * (asu & 1))。
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @return 信号强度 dBm;无信号 / 获取失败返回 {@code null}
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
public static Integer getSignalDbm(Context context) {
|
||||||
|
try {
|
||||||
|
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
|
if (tm == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
List<CellInfo> cellInfos = tm.getAllCellInfo();
|
||||||
|
if (cellInfos != null) {
|
||||||
|
int bestDbm = Integer.MIN_VALUE;
|
||||||
|
for (CellInfo info : cellInfos) {
|
||||||
|
if (info == null || !info.isRegistered()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CellSignalStrength strength = info.getCellSignalStrength();
|
||||||
|
if (strength != null) {
|
||||||
|
int dbm = strength.getDbm();
|
||||||
|
if (dbm != Integer.MAX_VALUE && dbm > bestDbm) {
|
||||||
|
bestDbm = dbm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestDbm != Integer.MIN_VALUE) {
|
||||||
|
return bestDbm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
SignalStrength signalStrength = tm.getSignalStrength();
|
||||||
|
if (signalStrength != null) {
|
||||||
|
int dbm = signalStrength.getDbm();
|
||||||
|
if (dbm != Integer.MAX_VALUE && dbm != Integer.MIN_VALUE) {
|
||||||
|
return dbm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前活跃网络获取信号强度 (RSSI/dBm)
|
||||||
|
* 优先返回 Wi-Fi 信号强度,如果未连接 Wi-Fi 则返回移动网络信号强度
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @return 信号强度 (dBm);获取失败返回 null
|
||||||
|
*/
|
||||||
|
public static Integer getCurrentNetworkRssi(Context context) {
|
||||||
|
try {
|
||||||
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (cm == null) return null;
|
||||||
|
|
||||||
|
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||||
|
if (activeNetwork == null || !activeNetwork.isConnected()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||||
|
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
|
WifiInfo info = wifiManager.getConnectionInfo();
|
||||||
|
if (info != null) {
|
||||||
|
return info.getRssi();
|
||||||
|
}
|
||||||
|
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
|
||||||
|
return getSignalDbm(context);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String getNetworkTypeName(int networkType) {
|
private static String getNetworkTypeName(int networkType) {
|
||||||
switch (networkType) {
|
switch (networkType) {
|
||||||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||||
@@ -504,11 +638,11 @@ public class SystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildId() {
|
public static String getBuildId() {
|
||||||
return getProperty(Build.ID, Build.UNKNOWN);
|
return getProperty("ro.build.id", Build.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildDisplayId() {
|
public static String getBuildDisplayId() {
|
||||||
return getProperty(Build.DISPLAY, Build.UNKNOWN);
|
return getProperty("ro.build.display.id", Build.DISPLAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
@@ -527,7 +661,7 @@ public class SystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getDeviceManufacturer() {
|
public static String getDeviceManufacturer() {
|
||||||
return getProperty(Build.MANUFACTURER, Build.UNKNOWN);
|
return getProperty("ro.product.manufacturer", Build.MANUFACTURER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getHardware() {
|
public static String getHardware() {
|
||||||
@@ -535,7 +669,7 @@ public class SystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getHost() {
|
public static String getHost() {
|
||||||
return getProperty(Build.HOST, Build.UNKNOWN);
|
return getProperty("ro.build.host", Build.HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getAndroidId(Context context) {
|
public static String getAndroidId(Context context) {
|
||||||
@@ -547,23 +681,23 @@ public class SystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildFingerprint() {
|
public static String getBuildFingerprint() {
|
||||||
return getProperty(Build.FINGERPRINT, Build.UNKNOWN);
|
return getProperty("ro.build.fingerprint", Build.FINGERPRINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildType() {
|
public static String getBuildType() {
|
||||||
return getProperty(Build.TYPE, Build.UNKNOWN);
|
return getProperty("ro.build.type", Build.TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildUser() {
|
public static String getBuildUser() {
|
||||||
return getProperty(Build.USER, Build.UNKNOWN);
|
return getProperty("ro.build.user", Build.USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildHost() {
|
public static String getBuildHost() {
|
||||||
return getProperty(Build.HOST, Build.UNKNOWN);
|
return getProperty("ro.build.host", Build.HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBuildTags() {
|
public static String getBuildTags() {
|
||||||
return getProperty(Build.TAGS, Build.UNKNOWN);
|
return getProperty("ro.build.tags", Build.TAGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getBuildTime() {
|
public static long getBuildTime() {
|
||||||
@@ -600,13 +734,30 @@ public class SystemUtils {
|
|||||||
|
|
||||||
public static String getScreenResolution(Context context) {
|
public static String getScreenResolution(Context context) {
|
||||||
try {
|
try {
|
||||||
android.util.DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||||||
return dm.widthPixels + "x" + dm.heightPixels;
|
return dm.widthPixels + "x" + dm.heightPixels;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备真实屏幕显示参数(包含状态栏、导航栏,不被窗口裁切)。
|
||||||
|
* 优先使用 getRealMetrics(显示设备真实分辨率),低版本无此 API 时回退 getMetrics。
|
||||||
|
*/
|
||||||
|
private static android.util.DisplayMetrics getRealDisplayMetrics(Context context) {
|
||||||
|
android.util.DisplayMetrics dm = new android.util.DisplayMetrics();
|
||||||
|
android.view.WindowManager wm =
|
||||||
|
(android.view.WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
android.view.Display display = wm.getDefaultDisplay();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
display.getRealMetrics(dm);
|
||||||
|
} else {
|
||||||
|
display.getMetrics(dm);
|
||||||
|
}
|
||||||
|
return dm;
|
||||||
|
}
|
||||||
|
|
||||||
public static int getScreenDensity(Context context) {
|
public static int getScreenDensity(Context context) {
|
||||||
try {
|
try {
|
||||||
return (int) Math.round(context.getResources().getDisplayMetrics().density);
|
return (int) Math.round(context.getResources().getDisplayMetrics().density);
|
||||||
@@ -635,7 +786,7 @@ public class SystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getProductName() {
|
public static String getProductName() {
|
||||||
return getProperty(Build.PRODUCT, Build.UNKNOWN);
|
return getProperty("ro.product.name", Build.PRODUCT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getProductModel() {
|
public static String getProductModel() {
|
||||||
@@ -682,7 +833,7 @@ public class SystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getBootloader() {
|
public static String getBootloader() {
|
||||||
return getProperty(Build.BOOTLOADER, Build.UNKNOWN);
|
return getProperty("ro.bootloader", Build.BOOTLOADER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBasebandVersion() {
|
public static String getBasebandVersion() {
|
||||||
@@ -699,7 +850,7 @@ public class SystemUtils {
|
|||||||
|
|
||||||
public static String getScreenSize(Context context) {
|
public static String getScreenSize(Context context) {
|
||||||
try {
|
try {
|
||||||
android.util.DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||||||
double diagonal = Math.sqrt(Math.pow(dm.widthPixels / dm.xdpi, 2)
|
double diagonal = Math.sqrt(Math.pow(dm.widthPixels / dm.xdpi, 2)
|
||||||
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
||||||
return String.format(Locale.US, "%.1f", diagonal);
|
return String.format(Locale.US, "%.1f", diagonal);
|
||||||
@@ -710,7 +861,7 @@ public class SystemUtils {
|
|||||||
|
|
||||||
public static double getScreenInch(Context context) {
|
public static double getScreenInch(Context context) {
|
||||||
try {
|
try {
|
||||||
android.util.DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||||||
double diagonal = Math.sqrt(Math.pow(dm.widthPixels / dm.xdpi, 2)
|
double diagonal = Math.sqrt(Math.pow(dm.widthPixels / dm.xdpi, 2)
|
||||||
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
||||||
return Math.round(diagonal * 10.0) / 10.0;
|
return Math.round(diagonal * 10.0) / 10.0;
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.ttstd.dialer.bean;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class DeviceStatusTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConstructorAndGetters() {
|
||||||
|
String sn = "SN123456";
|
||||||
|
boolean online = true;
|
||||||
|
String ip = "192.168.1.100";
|
||||||
|
Integer rssi = -65;
|
||||||
|
boolean screenOn = true;
|
||||||
|
|
||||||
|
DeviceStatus status = new DeviceStatus(sn, online, ip, rssi, screenOn);
|
||||||
|
|
||||||
|
Assert.assertEquals(sn, status.getSn());
|
||||||
|
Assert.assertEquals(online, status.isOnline());
|
||||||
|
Assert.assertEquals(ip, status.getIp());
|
||||||
|
Assert.assertEquals(rssi, status.getRssi());
|
||||||
|
Assert.assertEquals(screenOn, status.isScreenOn());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetters() {
|
||||||
|
DeviceStatus status = new DeviceStatus(null, false, null, null, false);
|
||||||
|
|
||||||
|
status.setSn("NEW_SN");
|
||||||
|
status.setOnline(true);
|
||||||
|
status.setIp("10.0.0.1");
|
||||||
|
status.setRssi(-70);
|
||||||
|
status.setScreenOn(true);
|
||||||
|
|
||||||
|
Assert.assertEquals("NEW_SN", status.getSn());
|
||||||
|
Assert.assertTrue(status.isOnline());
|
||||||
|
Assert.assertEquals("10.0.0.1", status.getIp());
|
||||||
|
Assert.assertEquals(Integer.valueOf(-70), status.getRssi());
|
||||||
|
Assert.assertTrue(status.isScreenOn());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJsonSerialization() {
|
||||||
|
DeviceStatus status = new DeviceStatus("SN123", true, "1.1.1.1", -50, true);
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String json = gson.toJson(status);
|
||||||
|
|
||||||
|
// 验证 JSON 字符串中包含正确的字段和值
|
||||||
|
Assert.assertTrue(json.contains("\"sn\":\"SN123\""));
|
||||||
|
Assert.assertTrue(json.contains("\"online\":true"));
|
||||||
|
Assert.assertTrue(json.contains("\"ip\":\"1.1.1.1\""));
|
||||||
|
Assert.assertTrue(json.contains("\"rssi\":-50"));
|
||||||
|
Assert.assertTrue(json.contains("\"screenOn\":true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJsonDeserialization() {
|
||||||
|
String json = "{\"sn\":\"SN999\",\"online\":false,\"ip\":\"8.8.8.8\",\"rssi\":-80,\"screenOn\":false}";
|
||||||
|
Gson gson = new Gson();
|
||||||
|
DeviceStatus status = gson.fromJson(json, DeviceStatus.class);
|
||||||
|
|
||||||
|
Assert.assertEquals("SN999", status.getSn());
|
||||||
|
Assert.assertFalse(status.isOnline());
|
||||||
|
Assert.assertEquals("8.8.8.8", status.getIp());
|
||||||
|
Assert.assertEquals(Integer.valueOf(-80), status.getRssi());
|
||||||
|
Assert.assertFalse(status.isScreenOn());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ base_dp=360
|
|||||||
# Also need to match the phone screen of [match_dp].
|
# Also need to match the phone screen of [match_dp].
|
||||||
# If you have another dp values.
|
# If you have another dp values.
|
||||||
# System default values is 240,320,384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
|
# System default values is 240,320,384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
|
||||||
match_dp=
|
match_dp=240,260,280,300,320,360,362.2641,380,384,390,392.7272,400,410,411.4285,430,432,451.7647,480,533,540,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
|
||||||
# If you not wanna to match dp values above. Write some above values here, append value with "," .
|
# If you not wanna to match dp values above. Write some above values here, append value with "," .
|
||||||
# For example: 811,961,1365
|
# For example: 811,961,1365
|
||||||
ignore_dp=
|
ignore_dp=
|
||||||
|
|||||||
Reference in New Issue
Block a user