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.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) {
|
||||
|
||||
@@ -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<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) {
|
||||
switch (networkType) {
|
||||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||
@@ -504,11 +638,11 @@ public class SystemUtils {
|
||||
}
|
||||
|
||||
public static String getBuildId() {
|
||||
return getProperty(Build.ID, Build.UNKNOWN);
|
||||
return getProperty("ro.build.id", Build.ID);
|
||||
}
|
||||
|
||||
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() {
|
||||
return getProperty(Build.MANUFACTURER, Build.UNKNOWN);
|
||||
return getProperty("ro.product.manufacturer", Build.MANUFACTURER);
|
||||
}
|
||||
|
||||
public static String getHardware() {
|
||||
@@ -535,7 +669,7 @@ public class SystemUtils {
|
||||
}
|
||||
|
||||
public static String getHost() {
|
||||
return getProperty(Build.HOST, Build.UNKNOWN);
|
||||
return getProperty("ro.build.host", Build.HOST);
|
||||
}
|
||||
|
||||
public static String getAndroidId(Context context) {
|
||||
@@ -547,23 +681,23 @@ public class SystemUtils {
|
||||
}
|
||||
|
||||
public static String getBuildFingerprint() {
|
||||
return getProperty(Build.FINGERPRINT, Build.UNKNOWN);
|
||||
return getProperty("ro.build.fingerprint", Build.FINGERPRINT);
|
||||
}
|
||||
|
||||
public static String getBuildType() {
|
||||
return getProperty(Build.TYPE, Build.UNKNOWN);
|
||||
return getProperty("ro.build.type", Build.TYPE);
|
||||
}
|
||||
|
||||
public static String getBuildUser() {
|
||||
return getProperty(Build.USER, Build.UNKNOWN);
|
||||
return getProperty("ro.build.user", Build.USER);
|
||||
}
|
||||
|
||||
public static String getBuildHost() {
|
||||
return getProperty(Build.HOST, Build.UNKNOWN);
|
||||
return getProperty("ro.build.host", Build.HOST);
|
||||
}
|
||||
|
||||
public static String getBuildTags() {
|
||||
return getProperty(Build.TAGS, Build.UNKNOWN);
|
||||
return getProperty("ro.build.tags", Build.TAGS);
|
||||
}
|
||||
|
||||
public static long getBuildTime() {
|
||||
@@ -600,13 +734,30 @@ public class SystemUtils {
|
||||
|
||||
public static String getScreenResolution(Context context) {
|
||||
try {
|
||||
android.util.DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||||
return dm.widthPixels + "x" + dm.heightPixels;
|
||||
} catch (Throwable t) {
|
||||
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) {
|
||||
try {
|
||||
return (int) Math.round(context.getResources().getDisplayMetrics().density);
|
||||
@@ -635,7 +786,7 @@ public class SystemUtils {
|
||||
}
|
||||
|
||||
public static String getProductName() {
|
||||
return getProperty(Build.PRODUCT, Build.UNKNOWN);
|
||||
return getProperty("ro.product.name", Build.PRODUCT);
|
||||
}
|
||||
|
||||
public static String getProductModel() {
|
||||
@@ -682,7 +833,7 @@ public class SystemUtils {
|
||||
}
|
||||
|
||||
public static String getBootloader() {
|
||||
return getProperty(Build.BOOTLOADER, Build.UNKNOWN);
|
||||
return getProperty("ro.bootloader", Build.BOOTLOADER);
|
||||
}
|
||||
|
||||
public static String getBasebandVersion() {
|
||||
@@ -699,7 +850,7 @@ public class SystemUtils {
|
||||
|
||||
public static String getScreenSize(Context context) {
|
||||
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)
|
||||
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
||||
return String.format(Locale.US, "%.1f", diagonal);
|
||||
@@ -710,7 +861,7 @@ public class SystemUtils {
|
||||
|
||||
public static double getScreenInch(Context context) {
|
||||
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)
|
||||
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
||||
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].
|
||||
# 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
|
||||
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 "," .
|
||||
# For example: 811,961,1365
|
||||
ignore_dp=
|
||||
|
||||
Reference in New Issue
Block a user