From 06bb744a79887ebe89a5d4fa6d395f513a9267f9 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Wed, 5 Aug 2026 02:09:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor(push):=20=E9=87=8D=E6=9E=84=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=BF=A1=E6=81=AF=E4=B8=8A=E6=8A=A5=E4=B8=BA=E4=BA=94?= =?UTF-8?q?=E7=B1=BB=E5=B9=B6=E5=8F=91=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原来的单一硬件信息上报拆分为系统、硬件、网络、安全、其他五类信息,通过 mergeDelayError 并发上传,避免单点失败阻塞,并新增 DeviceInfoCollector 统一采集逻辑。 --- .../dialer/bean/req/SnHardwareInfoReq.java | 298 ++++-- .../dialer/bean/req/SnNetworkInfoReq.java | 293 ++++++ .../ttstd/dialer/bean/req/SnOtherInfoReq.java | 261 ++++++ .../dialer/bean/req/SnSecurityInfoReq.java | 246 +++++ .../dialer/bean/req/SnSystemInfoReq.java | 151 +++ .../ttstd/dialer/mdm/DeviceInfoCollector.java | 876 ++++++++++++++++++ .../ttstd/dialer/network/OkHttpManager.java | 43 +- .../ttstd/dialer/network/UrlConstants.java | 12 +- .../com/ttstd/dialer/network/api/SnApi.java | 35 + .../com/ttstd/dialer/push/PushExecutor.java | 68 +- 10 files changed, 2185 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/com/ttstd/dialer/bean/req/SnNetworkInfoReq.java create mode 100644 app/src/main/java/com/ttstd/dialer/bean/req/SnOtherInfoReq.java create mode 100644 app/src/main/java/com/ttstd/dialer/bean/req/SnSecurityInfoReq.java create mode 100644 app/src/main/java/com/ttstd/dialer/bean/req/SnSystemInfoReq.java create mode 100644 app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java index 3070395..407c964 100644 --- a/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java @@ -4,138 +4,290 @@ import com.google.gson.annotations.SerializedName; import java.io.Serializable; +/** + * 设备硬件信息上传请求。 + *

+ * 对应后端 {@code POST /api/v1/sn/update_hardware_info}, + * 字段与后端 {@code SnHardwareInfoReq} 一一对应。 + *

+ * 使用包装类型(Integer)而非基本类型,未采集到的字段保持 null, + * Gson 默认不序列化 null,可避免用 0 覆盖服务端已有数据。 + */ public class SnHardwareInfoReq implements Serializable { - private static final long serialVersionUID = 2728925136862797858L; + private static final long serialVersionUID = 1L; - @SerializedName("sn_imei") - private String imei; + @SerializedName("cpu_abi") + private String cpuAbi; - @SerializedName("sn_imsi") - private String imsi; + @SerializedName("cpu_abi2") + private String cpuAbi2; - @SerializedName("sn_wlan_mac") - private String wlanMac; + @SerializedName("cpu_min") + private String cpuMin; - @SerializedName("sn_device_mac") - private String deviceMac; + @SerializedName("cpu_model") + private String cpuModel; - @SerializedName("sn_bluetooth_mac") - private String bluetoothMac; + @SerializedName("cpu_cores") + private Integer cpuCores; - @SerializedName("sn_model") - private String model; + @SerializedName("cpu_max_freq") + private String cpuMaxFreq; - @SerializedName("sn_brand") - private String brand; + @SerializedName("cpu_min_freq") + private String cpuMinFreq; - @SerializedName("sn_board") - private String board; + @SerializedName("total_ram") + private String totalRam; - @SerializedName("sn_android_version") - private String androidVersion; + @SerializedName("available_ram") + private String availableRam; - @SerializedName("sn_android_api") - private int androidApi; + @SerializedName("internal_storage") + private String internalStorage; - @SerializedName("sn_build_id") - private String buildId; + @SerializedName("available_storage") + private String availableStorage; - @SerializedName("sn_build_display_id") - private String buildDisplayId; + @SerializedName("external_storage") + private String externalStorage; - public String getImei() { - return imei; + @SerializedName("battery_capacity") + private String batteryCapacity; + + @SerializedName("battery_level") + private Integer batteryLevel; + + @SerializedName("battery_status") + private String batteryStatus; + + @SerializedName("battery_health") + private String batteryHealth; + + @SerializedName("battery_temp") + private String batteryTemp; + + @SerializedName("battery_voltage") + private String batteryVoltage; + + @SerializedName("battery_technology") + private String batteryTechnology; + + @SerializedName("gpu_renderer") + private String gpuRenderer; + + @SerializedName("gpu_vendor") + private String gpuVendor; + + @SerializedName("gpu_version") + private String gpuVersion; + + @SerializedName("sensors") + private String sensors; + + @SerializedName("support_abi") + private String supportAbi; + + @SerializedName("support_abi64") + private String supportAbi64; + + public String getCpuAbi() { + return cpuAbi; } - public void setImei(String imei) { - this.imei = imei; + public void setCpuAbi(String cpuAbi) { + this.cpuAbi = cpuAbi; } - public String getImsi() { - return imsi; + public String getCpuAbi2() { + return cpuAbi2; } - public void setImsi(String imsi) { - this.imsi = imsi; + public void setCpuAbi2(String cpuAbi2) { + this.cpuAbi2 = cpuAbi2; } - public String getWlanMac() { - return wlanMac; + public String getCpuMin() { + return cpuMin; } - public void setWlanMac(String wlanMac) { - this.wlanMac = wlanMac; + public void setCpuMin(String cpuMin) { + this.cpuMin = cpuMin; } - public String getDeviceMac() { - return deviceMac; + public String getCpuModel() { + return cpuModel; } - public void setDeviceMac(String deviceMac) { - this.deviceMac = deviceMac; + public void setCpuModel(String cpuModel) { + this.cpuModel = cpuModel; } - public String getBluetoothMac() { - return bluetoothMac; + public Integer getCpuCores() { + return cpuCores; } - public void setBluetoothMac(String bluetoothMac) { - this.bluetoothMac = bluetoothMac; + public void setCpuCores(Integer cpuCores) { + this.cpuCores = cpuCores; } - public String getModel() { - return model; + public String getCpuMaxFreq() { + return cpuMaxFreq; } - public void setModel(String model) { - this.model = model; + public void setCpuMaxFreq(String cpuMaxFreq) { + this.cpuMaxFreq = cpuMaxFreq; } - public String getBrand() { - return brand; + public String getCpuMinFreq() { + return cpuMinFreq; } - public void setBrand(String brand) { - this.brand = brand; + public void setCpuMinFreq(String cpuMinFreq) { + this.cpuMinFreq = cpuMinFreq; } - public String getBoard() { - return board; + public String getTotalRam() { + return totalRam; } - public void setBoard(String board) { - this.board = board; + public void setTotalRam(String totalRam) { + this.totalRam = totalRam; } - public String getAndroidVersion() { - return androidVersion; + public String getAvailableRam() { + return availableRam; } - public void setAndroidVersion(String androidVersion) { - this.androidVersion = androidVersion; + public void setAvailableRam(String availableRam) { + this.availableRam = availableRam; } - public int getAndroidApi() { - return androidApi; + public String getInternalStorage() { + return internalStorage; } - public void setAndroidApi(int androidApi) { - this.androidApi = androidApi; + public void setInternalStorage(String internalStorage) { + this.internalStorage = internalStorage; } - public String getBuildId() { - return buildId; + public String getAvailableStorage() { + return availableStorage; } - public void setBuildId(String buildId) { - this.buildId = buildId; + public void setAvailableStorage(String availableStorage) { + this.availableStorage = availableStorage; } - public String getBuildDisplayId() { - return buildDisplayId; + public String getExternalStorage() { + return externalStorage; } - public void setBuildDisplayId(String buildDisplayId) { - this.buildDisplayId = buildDisplayId; + public void setExternalStorage(String externalStorage) { + this.externalStorage = externalStorage; + } + + public String getBatteryCapacity() { + return batteryCapacity; + } + + public void setBatteryCapacity(String batteryCapacity) { + this.batteryCapacity = batteryCapacity; + } + + public Integer getBatteryLevel() { + return batteryLevel; + } + + public void setBatteryLevel(Integer batteryLevel) { + this.batteryLevel = batteryLevel; + } + + public String getBatteryStatus() { + return batteryStatus; + } + + public void setBatteryStatus(String batteryStatus) { + this.batteryStatus = batteryStatus; + } + + public String getBatteryHealth() { + return batteryHealth; + } + + public void setBatteryHealth(String batteryHealth) { + this.batteryHealth = batteryHealth; + } + + public String getBatteryTemp() { + return batteryTemp; + } + + public void setBatteryTemp(String batteryTemp) { + this.batteryTemp = batteryTemp; + } + + public String getBatteryVoltage() { + return batteryVoltage; + } + + public void setBatteryVoltage(String batteryVoltage) { + this.batteryVoltage = batteryVoltage; + } + + public String getBatteryTechnology() { + return batteryTechnology; + } + + public void setBatteryTechnology(String batteryTechnology) { + this.batteryTechnology = batteryTechnology; + } + + public String getGpuRenderer() { + return gpuRenderer; + } + + public void setGpuRenderer(String gpuRenderer) { + this.gpuRenderer = gpuRenderer; + } + + public String getGpuVendor() { + return gpuVendor; + } + + public void setGpuVendor(String gpuVendor) { + this.gpuVendor = gpuVendor; + } + + public String getGpuVersion() { + return gpuVersion; + } + + public void setGpuVersion(String gpuVersion) { + this.gpuVersion = gpuVersion; + } + + public String getSensors() { + return sensors; + } + + public void setSensors(String sensors) { + this.sensors = sensors; + } + + public String getSupportAbi() { + return supportAbi; + } + + public void setSupportAbi(String supportAbi) { + this.supportAbi = supportAbi; + } + + public String getSupportAbi64() { + return supportAbi64; + } + + public void setSupportAbi64(String supportAbi64) { + this.supportAbi64 = supportAbi64; } } diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnNetworkInfoReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnNetworkInfoReq.java new file mode 100644 index 0000000..dc5dabe --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnNetworkInfoReq.java @@ -0,0 +1,293 @@ +package com.ttstd.dialer.bean.req; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * 设备网络信息上传请求。 + *

+ * 对应后端 {@code POST /api/v1/sn/update_network_info}, + * 字段与后端 {@code SnNetworkInfoReq} 一一对应。 + *

+ * 布尔语义字段(is_connected/is_vpn/is_proxy)后端为 Integer, + * 这里同样用 Integer 表示 0/1,未知时为 null。 + */ +public class SnNetworkInfoReq implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("network_type") + private String networkType; + + @SerializedName("network_subtype") + private String networkSubtype; + + @SerializedName("is_connected") + private Integer isConnected; + + @SerializedName("wifi_ssid") + private String wifiSsid; + + @SerializedName("wifi_bssid") + private String wifiBssid; + + @SerializedName("wifi_ip") + private String wifiIp; + + @SerializedName("wifi_mac") + private String wifiMac; + + @SerializedName("wifi_gateway") + private String wifiGateway; + + @SerializedName("wifi_netmask") + private String wifiNetmask; + + @SerializedName("wifi_dns") + private String wifiDns; + + @SerializedName("wifi_rssi") + private Integer wifiRssi; + + @SerializedName("wifi_speed") + private String wifiSpeed; + + @SerializedName("mobile_ip") + private String mobileIp; + + @SerializedName("mobile_mac") + private String mobileMac; + + @SerializedName("mobile_imei") + private String mobileImei; + + @SerializedName("mobile_imsi") + private String mobileImsi; + + @SerializedName("mobile_operator") + private String mobileOperator; + + @SerializedName("mobile_sim_serial") + private String mobileSimSerial; + + @SerializedName("mobile_network_type") + private String mobileNetworkType; + + @SerializedName("bluetooth_mac") + private String bluetoothMac; + + @SerializedName("bluetooth_name") + private String bluetoothName; + + @SerializedName("is_vpn") + private Integer isVpn; + + @SerializedName("is_proxy") + private Integer isProxy; + + @SerializedName("proxy_host") + private String proxyHost; + + @SerializedName("proxy_port") + private String proxyPort; + + public String getNetworkType() { + return networkType; + } + + public void setNetworkType(String networkType) { + this.networkType = networkType; + } + + public String getNetworkSubtype() { + return networkSubtype; + } + + public void setNetworkSubtype(String networkSubtype) { + this.networkSubtype = networkSubtype; + } + + public Integer getIsConnected() { + return isConnected; + } + + public void setIsConnected(Integer isConnected) { + this.isConnected = isConnected; + } + + public String getWifiSsid() { + return wifiSsid; + } + + public void setWifiSsid(String wifiSsid) { + this.wifiSsid = wifiSsid; + } + + public String getWifiBssid() { + return wifiBssid; + } + + public void setWifiBssid(String wifiBssid) { + this.wifiBssid = wifiBssid; + } + + public String getWifiIp() { + return wifiIp; + } + + public void setWifiIp(String wifiIp) { + this.wifiIp = wifiIp; + } + + public String getWifiMac() { + return wifiMac; + } + + public void setWifiMac(String wifiMac) { + this.wifiMac = wifiMac; + } + + public String getWifiGateway() { + return wifiGateway; + } + + public void setWifiGateway(String wifiGateway) { + this.wifiGateway = wifiGateway; + } + + public String getWifiNetmask() { + return wifiNetmask; + } + + public void setWifiNetmask(String wifiNetmask) { + this.wifiNetmask = wifiNetmask; + } + + public String getWifiDns() { + return wifiDns; + } + + public void setWifiDns(String wifiDns) { + this.wifiDns = wifiDns; + } + + public Integer getWifiRssi() { + return wifiRssi; + } + + public void setWifiRssi(Integer wifiRssi) { + this.wifiRssi = wifiRssi; + } + + public String getWifiSpeed() { + return wifiSpeed; + } + + public void setWifiSpeed(String wifiSpeed) { + this.wifiSpeed = wifiSpeed; + } + + public String getMobileIp() { + return mobileIp; + } + + public void setMobileIp(String mobileIp) { + this.mobileIp = mobileIp; + } + + public String getMobileMac() { + return mobileMac; + } + + public void setMobileMac(String mobileMac) { + this.mobileMac = mobileMac; + } + + public String getMobileImei() { + return mobileImei; + } + + public void setMobileImei(String mobileImei) { + this.mobileImei = mobileImei; + } + + public String getMobileImsi() { + return mobileImsi; + } + + public void setMobileImsi(String mobileImsi) { + this.mobileImsi = mobileImsi; + } + + public String getMobileOperator() { + return mobileOperator; + } + + public void setMobileOperator(String mobileOperator) { + this.mobileOperator = mobileOperator; + } + + public String getMobileSimSerial() { + return mobileSimSerial; + } + + public void setMobileSimSerial(String mobileSimSerial) { + this.mobileSimSerial = mobileSimSerial; + } + + public String getMobileNetworkType() { + return mobileNetworkType; + } + + public void setMobileNetworkType(String mobileNetworkType) { + this.mobileNetworkType = mobileNetworkType; + } + + public String getBluetoothMac() { + return bluetoothMac; + } + + public void setBluetoothMac(String bluetoothMac) { + this.bluetoothMac = bluetoothMac; + } + + public String getBluetoothName() { + return bluetoothName; + } + + public void setBluetoothName(String bluetoothName) { + this.bluetoothName = bluetoothName; + } + + public Integer getIsVpn() { + return isVpn; + } + + public void setIsVpn(Integer isVpn) { + this.isVpn = isVpn; + } + + public Integer getIsProxy() { + return isProxy; + } + + public void setIsProxy(Integer isProxy) { + this.isProxy = isProxy; + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public String getProxyPort() { + return proxyPort; + } + + public void setProxyPort(String proxyPort) { + this.proxyPort = proxyPort; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnOtherInfoReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnOtherInfoReq.java new file mode 100644 index 0000000..b44f86b --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnOtherInfoReq.java @@ -0,0 +1,261 @@ +package com.ttstd.dialer.bean.req; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * 设备其他信息上传请求。 + *

+ * 对应后端 {@code POST /api/v1/sn/update_other_info}, + * 字段与后端 {@code SnOtherInfoReq} 一一对应。 + *

+ * 注意:后端 install_time / update_time_app 为 {@code LocalDateTime}, + * Jackson 默认接受 "yyyy-MM-dd'T'HH:mm:ss" 格式的字符串, + * 因此这里用 String 承载,由采集器负责格式化,避免时间戳数字反序列化失败。 + */ +public class SnOtherInfoReq implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("app_version") + private String appVersion; + + @SerializedName("app_version_code") + private Integer appVersionCode; + + @SerializedName("install_time") + private String installTime; + + @SerializedName("update_time_app") + private String updateTimeApp; + + @SerializedName("package_name") + private String packageName; + + @SerializedName("channel") + private String channel; + + @SerializedName("device_id") + private String deviceId; + + @SerializedName("gaid") + private String gaid; + + @SerializedName("oaid") + private String oaid; + + @SerializedName("phone_number") + private String phoneNumber; + + @SerializedName("iccid") + private String iccid; + + @SerializedName("imsi") + private String imsi; + + @SerializedName("camera_info") + private String cameraInfo; + + @SerializedName("sensor_count") + private Integer sensorCount; + + @SerializedName("app_list_md5") + private String appListMd5; + + @SerializedName("is_charging") + private Integer isCharging; + + @SerializedName("charging_type") + private String chargingType; + + @SerializedName("temperature") + private String temperature; + + @SerializedName("free_storage") + private String freeStorage; + + @SerializedName("total_storage") + private String totalStorage; + + @SerializedName("free_ram") + private String freeRam; + + @SerializedName("total_ram") + private String totalRam; + + public String getAppVersion() { + return appVersion; + } + + public void setAppVersion(String appVersion) { + this.appVersion = appVersion; + } + + public Integer getAppVersionCode() { + return appVersionCode; + } + + public void setAppVersionCode(Integer appVersionCode) { + this.appVersionCode = appVersionCode; + } + + public String getInstallTime() { + return installTime; + } + + public void setInstallTime(String installTime) { + this.installTime = installTime; + } + + public String getUpdateTimeApp() { + return updateTimeApp; + } + + public void setUpdateTimeApp(String updateTimeApp) { + this.updateTimeApp = updateTimeApp; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getGaid() { + return gaid; + } + + public void setGaid(String gaid) { + this.gaid = gaid; + } + + public String getOaid() { + return oaid; + } + + public void setOaid(String oaid) { + this.oaid = oaid; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getIccid() { + return iccid; + } + + public void setIccid(String iccid) { + this.iccid = iccid; + } + + public String getImsi() { + return imsi; + } + + public void setImsi(String imsi) { + this.imsi = imsi; + } + + public String getCameraInfo() { + return cameraInfo; + } + + public void setCameraInfo(String cameraInfo) { + this.cameraInfo = cameraInfo; + } + + public Integer getSensorCount() { + return sensorCount; + } + + public void setSensorCount(Integer sensorCount) { + this.sensorCount = sensorCount; + } + + public String getAppListMd5() { + return appListMd5; + } + + public void setAppListMd5(String appListMd5) { + this.appListMd5 = appListMd5; + } + + public Integer getIsCharging() { + return isCharging; + } + + public void setIsCharging(Integer isCharging) { + this.isCharging = isCharging; + } + + public String getChargingType() { + return chargingType; + } + + public void setChargingType(String chargingType) { + this.chargingType = chargingType; + } + + public String getTemperature() { + return temperature; + } + + public void setTemperature(String temperature) { + this.temperature = temperature; + } + + public String getFreeStorage() { + return freeStorage; + } + + public void setFreeStorage(String freeStorage) { + this.freeStorage = freeStorage; + } + + public String getTotalStorage() { + return totalStorage; + } + + public void setTotalStorage(String totalStorage) { + this.totalStorage = totalStorage; + } + + public String getFreeRam() { + return freeRam; + } + + public void setFreeRam(String freeRam) { + this.freeRam = freeRam; + } + + public String getTotalRam() { + return totalRam; + } + + public void setTotalRam(String totalRam) { + this.totalRam = totalRam; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnSecurityInfoReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnSecurityInfoReq.java new file mode 100644 index 0000000..3546567 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnSecurityInfoReq.java @@ -0,0 +1,246 @@ +package com.ttstd.dialer.bean.req; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * 设备安全信息上传请求。 + *

+ * 对应后端 {@code POST /api/v1/sn/update_security_info}, + * 字段与后端 {@code SnSecurityInfoReq} 一一对应。 + */ +public class SnSecurityInfoReq implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("is_root") + private Integer isRoot; + + @SerializedName("is_jailbreak") + private Integer isJailbreak; + + @SerializedName("root_status") + private Integer rootStatus; + + @SerializedName("has_su_binary") + private Integer hasSuBinary; + + @SerializedName("has_busybox") + private Integer hasBusybox; + + @SerializedName("is_emulator") + private Integer isEmulator; + + @SerializedName("is_debuggable") + private Integer isDebuggable; + + @SerializedName("is_usb_debug") + private Integer isUsbDebug; + + @SerializedName("is_unknown_sources") + private Integer isUnknownSources; + + @SerializedName("is_verify_apps") + private Integer isVerifyApps; + + @SerializedName("is_encrypted") + private Integer isEncrypted; + + @SerializedName("lock_screen_type") + private String lockScreenType; + + @SerializedName("has_password") + private Integer hasPassword; + + @SerializedName("password_type") + private String passwordType; + + @SerializedName("security_patch") + private String securityPatch; + + @SerializedName("google_play_protect") + private Integer googlePlayProtect; + + @SerializedName("adb_status") + private Integer adbStatus; + + @SerializedName("is_vpn_active") + private Integer isVpnActive; + + @SerializedName("is_device_owner") + private Integer isDeviceOwner; + + @SerializedName("android_version") + private String androidVersion; + + @SerializedName("kernel_version") + private String kernelVersion; + + public Integer getIsRoot() { + return isRoot; + } + + public void setIsRoot(Integer isRoot) { + this.isRoot = isRoot; + } + + public Integer getIsJailbreak() { + return isJailbreak; + } + + public void setIsJailbreak(Integer isJailbreak) { + this.isJailbreak = isJailbreak; + } + + public Integer getRootStatus() { + return rootStatus; + } + + public void setRootStatus(Integer rootStatus) { + this.rootStatus = rootStatus; + } + + public Integer getHasSuBinary() { + return hasSuBinary; + } + + public void setHasSuBinary(Integer hasSuBinary) { + this.hasSuBinary = hasSuBinary; + } + + public Integer getHasBusybox() { + return hasBusybox; + } + + public void setHasBusybox(Integer hasBusybox) { + this.hasBusybox = hasBusybox; + } + + public Integer getIsEmulator() { + return isEmulator; + } + + public void setIsEmulator(Integer isEmulator) { + this.isEmulator = isEmulator; + } + + public Integer getIsDebuggable() { + return isDebuggable; + } + + public void setIsDebuggable(Integer isDebuggable) { + this.isDebuggable = isDebuggable; + } + + public Integer getIsUsbDebug() { + return isUsbDebug; + } + + public void setIsUsbDebug(Integer isUsbDebug) { + this.isUsbDebug = isUsbDebug; + } + + public Integer getIsUnknownSources() { + return isUnknownSources; + } + + public void setIsUnknownSources(Integer isUnknownSources) { + this.isUnknownSources = isUnknownSources; + } + + public Integer getIsVerifyApps() { + return isVerifyApps; + } + + public void setIsVerifyApps(Integer isVerifyApps) { + this.isVerifyApps = isVerifyApps; + } + + public Integer getIsEncrypted() { + return isEncrypted; + } + + public void setIsEncrypted(Integer isEncrypted) { + this.isEncrypted = isEncrypted; + } + + public String getLockScreenType() { + return lockScreenType; + } + + public void setLockScreenType(String lockScreenType) { + this.lockScreenType = lockScreenType; + } + + public Integer getHasPassword() { + return hasPassword; + } + + public void setHasPassword(Integer hasPassword) { + this.hasPassword = hasPassword; + } + + public String getPasswordType() { + return passwordType; + } + + public void setPasswordType(String passwordType) { + this.passwordType = passwordType; + } + + public String getSecurityPatch() { + return securityPatch; + } + + public void setSecurityPatch(String securityPatch) { + this.securityPatch = securityPatch; + } + + public Integer getGooglePlayProtect() { + return googlePlayProtect; + } + + public void setGooglePlayProtect(Integer googlePlayProtect) { + this.googlePlayProtect = googlePlayProtect; + } + + public Integer getAdbStatus() { + return adbStatus; + } + + public void setAdbStatus(Integer adbStatus) { + this.adbStatus = adbStatus; + } + + public Integer getIsVpnActive() { + return isVpnActive; + } + + public void setIsVpnActive(Integer isVpnActive) { + this.isVpnActive = isVpnActive; + } + + public Integer getIsDeviceOwner() { + return isDeviceOwner; + } + + public void setIsDeviceOwner(Integer isDeviceOwner) { + this.isDeviceOwner = isDeviceOwner; + } + + public String getAndroidVersion() { + return androidVersion; + } + + public void setAndroidVersion(String androidVersion) { + this.androidVersion = androidVersion; + } + + public String getKernelVersion() { + return kernelVersion; + } + + public void setKernelVersion(String kernelVersion) { + this.kernelVersion = kernelVersion; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnSystemInfoReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnSystemInfoReq.java new file mode 100644 index 0000000..f2571e1 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnSystemInfoReq.java @@ -0,0 +1,151 @@ +package com.ttstd.dialer.bean.req; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * 设备基本信息上传请求。 + *

+ * 对应后端 {@code POST /api/v1/sn/update_system_info}, + * 字段与后端 {@code SnSystemInfoReq} 一一对应。 + *

+ * 注意:该类原名为 SnHardwareInfoReq,但其字段(sn_imei/sn_model/sn_brand 等) + * 实际对应的是后端的「基本信息」而非「硬件信息」,故更名以避免与新增的 + * {@link SnHardwareInfoReq}(CPU/内存/电池/GPU)混淆。 + */ +public class SnSystemInfoReq implements Serializable { + private static final long serialVersionUID = 2728925136862797858L; + + @SerializedName("sn_imei") + private String imei; + + @SerializedName("sn_imsi") + private String imsi; + + @SerializedName("sn_wlan_mac") + private String wlanMac; + + @SerializedName("sn_device_mac") + private String deviceMac; + + @SerializedName("sn_bluetooth_mac") + private String bluetoothMac; + + @SerializedName("sn_model") + private String model; + + @SerializedName("sn_brand") + private String brand; + + @SerializedName("sn_board") + private String board; + + @SerializedName("sn_android_version") + private String androidVersion; + + @SerializedName("sn_android_api") + private int androidApi; + + @SerializedName("sn_build_id") + private String buildId; + + @SerializedName("sn_build_display_id") + private String buildDisplayId; + + public String getImei() { + return imei; + } + + public void setImei(String imei) { + this.imei = imei; + } + + public String getImsi() { + return imsi; + } + + public void setImsi(String imsi) { + this.imsi = imsi; + } + + public String getWlanMac() { + return wlanMac; + } + + public void setWlanMac(String wlanMac) { + this.wlanMac = wlanMac; + } + + public String getDeviceMac() { + return deviceMac; + } + + public void setDeviceMac(String deviceMac) { + this.deviceMac = deviceMac; + } + + public String getBluetoothMac() { + return bluetoothMac; + } + + public void setBluetoothMac(String bluetoothMac) { + this.bluetoothMac = bluetoothMac; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } + + public String getBoard() { + return board; + } + + public void setBoard(String board) { + this.board = board; + } + + public String getAndroidVersion() { + return androidVersion; + } + + public void setAndroidVersion(String androidVersion) { + this.androidVersion = androidVersion; + } + + public int getAndroidApi() { + return androidApi; + } + + public void setAndroidApi(int androidApi) { + this.androidApi = androidApi; + } + + public String getBuildId() { + return buildId; + } + + public void setBuildId(String buildId) { + this.buildId = buildId; + } + + public String getBuildDisplayId() { + return buildDisplayId; + } + + public void setBuildDisplayId(String buildDisplayId) { + this.buildDisplayId = buildDisplayId; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java b/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java new file mode 100644 index 0000000..64cc502 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java @@ -0,0 +1,876 @@ +package com.ttstd.dialer.mdm; + +import android.Manifest; +import android.app.ActivityManager; +import android.app.admin.DevicePolicyManager; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ApplicationInfo; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.BatteryManager; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import androidx.core.content.ContextCompat; + +import com.ttstd.dialer.bean.req.SnHardwareInfoReq; +import com.ttstd.dialer.bean.req.SnNetworkInfoReq; +import com.ttstd.dialer.bean.req.SnOtherInfoReq; +import com.ttstd.dialer.bean.req.SnSecurityInfoReq; +import com.ttstd.dialer.bean.req.SnSystemInfoReq; +import com.ttstd.dialer.utils.Logger; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * 设备信息采集器。 + *

+ * 负责采集后端 5 个上传接口所需的全部信息: + *

+ *

+ * 设计要点: + *

    + *
  1. 每个字段的采集都独立 try/catch,单个字段失败不影响整体上报;
  2. + *
  3. 采集不到的字段保持 null,Gson 默认不序列化 null,避免覆盖服务端已有数据;
  4. + *
  5. 涉及权限的调用(IMEI/IMSI/ICCID/手机号)均做权限检查,无权限时静默跳过;
  6. + *
  7. 存在文件读取与 shell 探测,耗时较长,务必在子线程调用。
  8. + *
+ */ +public class DeviceInfoCollector { + + private static final String TAG = "DeviceInfoCollector"; + + /** + * 后端 LocalDateTime 默认接受的 ISO 格式。 + */ + private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + + /** + * 常见的 su 二进制路径,用于 Root 检测。 + */ + private static final String[] SU_PATHS = { + "/system/bin/su", + "/system/xbin/su", + "/sbin/su", + "/system/su", + "/vendor/bin/su", + "/su/bin/su", + "/data/local/xbin/su", + "/data/local/bin/su", + "/system/sd/xbin/su" + }; + + /** + * 常见的 busybox 路径。 + */ + private static final String[] BUSYBOX_PATHS = { + "/system/bin/busybox", + "/system/xbin/busybox", + "/sbin/busybox", + "/vendor/bin/busybox", + "/data/local/xbin/busybox" + }; + + private final Context mContext; + + public DeviceInfoCollector(Context context) { + this.mContext = context.getApplicationContext(); + } + + // ================================================================== + // 基本信息 + // ================================================================== + + /** + * 采集设备基本信息。 + */ + public SnSystemInfoReq collectSystemInfo() { + SnSystemInfoReq req = new SnSystemInfoReq(); + DeviceManagerService dms = DeviceManagerService.getInstance(); + + req.setImei(safeString(dms::getImei)); + req.setImsi(safeString(dms::getImsi)); + req.setWlanMac(safeString(dms::getWlanMac)); + req.setDeviceMac(safeString(dms::getDeviceMac)); + req.setBluetoothMac(safeString(dms::getBluetoothMac)); + + req.setModel(Build.MODEL); + req.setBrand(Build.BRAND); + req.setBoard(Build.BOARD); + req.setAndroidVersion(Build.VERSION.RELEASE); + req.setAndroidApi(Build.VERSION.SDK_INT); + req.setBuildId(Build.ID); + req.setBuildDisplayId(Build.DISPLAY); + return req; + } + + // ================================================================== + // 硬件信息 + // ================================================================== + + /** + * 采集设备硬件信息。 + */ + public SnHardwareInfoReq collectHardwareInfo() { + SnHardwareInfoReq req = new SnHardwareInfoReq(); + + // ---- CPU ---- + try { + req.setCpuAbi(Build.SUPPORTED_ABIS.length > 0 ? Build.SUPPORTED_ABIS[0] : null); + req.setCpuAbi2(Build.SUPPORTED_ABIS.length > 1 ? Build.SUPPORTED_ABIS[1] : null); + req.setSupportAbi(join(Build.SUPPORTED_32_BIT_ABIS)); + req.setSupportAbi64(join(Build.SUPPORTED_64_BIT_ABIS)); + } catch (Throwable t) { + Logger.e(TAG, "collect cpu abi failed: " + t.getMessage()); + } + + try { + req.setCpuCores(Runtime.getRuntime().availableProcessors()); + } catch (Throwable t) { + Logger.e(TAG, "collect cpu cores failed: " + t.getMessage()); + } + + req.setCpuModel(readCpuModel()); + req.setCpuMin(Build.HARDWARE); + req.setCpuMaxFreq(readCpuFreq("cpuinfo_max_freq")); + req.setCpuMinFreq(readCpuFreq("cpuinfo_min_freq")); + + // ---- 内存 ---- + try { + ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); + am.getMemoryInfo(mi); + req.setTotalRam(formatSize(mi.totalMem)); + req.setAvailableRam(formatSize(mi.availMem)); + } + } catch (Throwable t) { + Logger.e(TAG, "collect ram failed: " + t.getMessage()); + } + + // ---- 存储 ---- + try { + File data = Environment.getDataDirectory(); + StatFs stat = new StatFs(data.getPath()); + long total = stat.getBlockCountLong() * stat.getBlockSizeLong(); + long available = stat.getAvailableBlocksLong() * stat.getBlockSizeLong(); + req.setInternalStorage(formatSize(total)); + req.setAvailableStorage(formatSize(available)); + } catch (Throwable t) { + Logger.e(TAG, "collect storage failed: " + t.getMessage()); + } + + try { + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + File ext = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(ext.getPath()); + long total = stat.getBlockCountLong() * stat.getBlockSizeLong(); + req.setExternalStorage(formatSize(total)); + } + } catch (Throwable t) { + Logger.e(TAG, "collect external storage failed: " + t.getMessage()); + } + + // ---- 电池 ---- + try { + Intent battery = mContext.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (battery != null) { + int level = battery.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = battery.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + if (level >= 0 && scale > 0) { + req.setBatteryLevel(level * 100 / scale); + } + req.setBatteryStatus(batteryStatusName( + battery.getIntExtra(BatteryManager.EXTRA_STATUS, -1))); + req.setBatteryHealth(batteryHealthName( + battery.getIntExtra(BatteryManager.EXTRA_HEALTH, -1))); + + int temp = battery.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1); + if (temp > 0) { + // 系统返回的是 0.1℃ 为单位的整数 + req.setBatteryTemp(String.valueOf(temp / 10.0f)); + } + int voltage = battery.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1); + if (voltage > 0) { + req.setBatteryVoltage(voltage + "mV"); + } + req.setBatteryTechnology( + battery.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY)); + } + req.setBatteryCapacity(readBatteryCapacity()); + } catch (Throwable t) { + Logger.e(TAG, "collect battery failed: " + t.getMessage()); + } + + // ---- 传感器 ---- + try { + SensorManager sm = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + if (sm != null) { + List sensors = sm.getSensorList(Sensor.TYPE_ALL); + StringBuilder sb = new StringBuilder(); + for (Sensor s : sensors) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(s.getName()); + } + req.setSensors(sb.length() > 0 ? sb.toString() : null); + } + } catch (Throwable t) { + Logger.e(TAG, "collect sensors failed: " + t.getMessage()); + } + + // GPU 信息需要 GL 上下文,普通 Service 中无法可靠获取,此处留空由服务端容错。 + return req; + } + + // ================================================================== + // 网络信息 + // ================================================================== + + /** + * 采集设备网络信息。 + */ + public SnNetworkInfoReq collectNetworkInfo() { + SnNetworkInfoReq req = new SnNetworkInfoReq(); + + ConnectivityManager cm = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + + // ---- 连接状态与网络类型 ---- + try { + if (cm != null) { + Network active = cm.getActiveNetwork(); + NetworkCapabilities caps = active == null ? null : cm.getNetworkCapabilities(active); + boolean connected = caps != null + && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + req.setIsConnected(connected ? 1 : 0); + + if (caps != null) { + if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + req.setNetworkType("WIFI"); + } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + req.setNetworkType("MOBILE"); + } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + req.setNetworkType("ETHERNET"); + } else { + req.setNetworkType("OTHER"); + } + req.setIsVpn(caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ? 1 : 0); + } + + // DNS 与网关 + LinkProperties lp = active == null ? null : cm.getLinkProperties(active); + if (lp != null) { + StringBuilder dns = new StringBuilder(); + for (InetAddress addr : lp.getDnsServers()) { + if (dns.length() > 0) { + dns.append(","); + } + dns.append(addr.getHostAddress()); + } + req.setWifiDns(dns.length() > 0 ? dns.toString() : null); + + for (LinkAddress la : lp.getLinkAddresses()) { + if (la.getAddress() instanceof Inet4Address) { + req.setWifiNetmask(String.valueOf(la.getPrefixLength())); + break; + } + } + } + } + } catch (Throwable t) { + Logger.e(TAG, "collect network state failed: " + t.getMessage()); + } + + // ---- WiFi ---- + try { + WifiManager wm = (WifiManager) mContext.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + if (wm != null) { + WifiInfo info = wm.getConnectionInfo(); + if (info != null) { + String ssid = info.getSSID(); + if (!TextUtils.isEmpty(ssid)) { + // 系统返回的 SSID 常带引号 + req.setWifiSsid(ssid.replace("\"", "")); + } + req.setWifiBssid(info.getBSSID()); + req.setWifiRssi(info.getRssi()); + req.setWifiSpeed(info.getLinkSpeed() + "Mbps"); + + int ip = info.getIpAddress(); + if (ip != 0) { + req.setWifiIp(intToIp(ip)); + } + } + // Android 6.0+ 出于隐私限制,getMacAddress 返回固定值,改用 MDM 接口 + req.setWifiMac(safeString(() -> DeviceManagerService.getInstance().getWlanMac())); + + android.net.DhcpInfo dhcp = wm.getDhcpInfo(); + if (dhcp != null && dhcp.gateway != 0) { + req.setWifiGateway(intToIp(dhcp.gateway)); + } + } + } catch (Throwable t) { + Logger.e(TAG, "collect wifi failed: " + t.getMessage()); + } + + // ---- 移动网络 ---- + try { + TelephonyManager tm = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + if (tm != null) { + req.setMobileOperator(tm.getNetworkOperatorName()); + req.setMobileNetworkType(networkTypeName(tm)); + if (hasPermission(Manifest.permission.READ_PHONE_STATE)) { + req.setMobileSimSerial(safeString(tm::getSimSerialNumber)); + } + } + // IMEI/IMSI 走 MDM 接口,规避高版本系统的权限限制 + req.setMobileImei(safeString(() -> DeviceManagerService.getInstance().getImei())); + req.setMobileImsi(safeString(() -> DeviceManagerService.getInstance().getImsi())); + } catch (Throwable t) { + Logger.e(TAG, "collect mobile failed: " + t.getMessage()); + } + + // ---- 蓝牙 ---- + try { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter != null) { + req.setBluetoothName(safeString(adapter::getName)); + } + req.setBluetoothMac(safeString(() -> DeviceManagerService.getInstance().getBluetoothMac())); + } catch (Throwable t) { + Logger.e(TAG, "collect bluetooth failed: " + t.getMessage()); + } + + // ---- 代理 ---- + try { + String host = System.getProperty("http.proxyHost"); + String port = System.getProperty("http.proxyPort"); + boolean hasProxy = !TextUtils.isEmpty(host); + req.setIsProxy(hasProxy ? 1 : 0); + if (hasProxy) { + req.setProxyHost(host); + req.setProxyPort(port); + } + } catch (Throwable t) { + Logger.e(TAG, "collect proxy failed: " + t.getMessage()); + } + + return req; + } + + // ================================================================== + // 安全信息 + // ================================================================== + + /** + * 采集设备安全信息。 + */ + public SnSecurityInfoReq collectSecurityInfo() { + SnSecurityInfoReq req = new SnSecurityInfoReq(); + + // ---- Root 检测 ---- + int hasSu = anyFileExists(SU_PATHS) ? 1 : 0; + int hasBusybox = anyFileExists(BUSYBOX_PATHS) ? 1 : 0; + int testKeys = Build.TAGS != null && Build.TAGS.contains("test-keys") ? 1 : 0; + int rooted = (hasSu == 1 || hasBusybox == 1 || testKeys == 1) ? 1 : 0; + + req.setHasSuBinary(hasSu); + req.setHasBusybox(hasBusybox); + req.setIsRoot(rooted); + req.setIsJailbreak(rooted); + req.setRootStatus(rooted); + + // ---- 模拟器检测 ---- + req.setIsEmulator(isEmulator() ? 1 : 0); + + // ---- 调试相关 ---- + try { + ApplicationInfo ai = mContext.getApplicationInfo(); + req.setIsDebuggable((ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 ? 1 : 0); + } catch (Throwable t) { + Logger.e(TAG, "collect debuggable failed: " + t.getMessage()); + } + + try { + int adb = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ADB_ENABLED, 0); + req.setIsUsbDebug(adb); + req.setAdbStatus(adb); + } catch (Throwable t) { + Logger.e(TAG, "collect adb failed: " + t.getMessage()); + } + + try { + // INSTALL_NON_MARKET_APPS 在 Android 8.0 后被弃用,改为按应用授权 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + req.setIsUnknownSources(Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.INSTALL_NON_MARKET_APPS, 0)); + } else { + req.setIsUnknownSources( + mContext.getPackageManager().canRequestPackageInstalls() ? 1 : 0); + } + } catch (Throwable t) { + Logger.e(TAG, "collect unknown sources failed: " + t.getMessage()); + } + + try { + req.setIsVerifyApps(Settings.Global.getInt(mContext.getContentResolver(), + "package_verifier_enable", 0)); + req.setGooglePlayProtect(req.getIsVerifyApps()); + } catch (Throwable t) { + Logger.e(TAG, "collect verify apps failed: " + t.getMessage()); + } + + // ---- 加密与锁屏 ---- + try { + DevicePolicyManager dpm = + (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + if (dpm != null) { + int status = dpm.getStorageEncryptionStatus(); + boolean encrypted = + status == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE + || status == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER + || status == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY; + req.setIsEncrypted(encrypted ? 1 : 0); + req.setIsDeviceOwner(dpm.isDeviceOwnerApp(mContext.getPackageName()) ? 1 : 0); + } + } catch (Throwable t) { + Logger.e(TAG, "collect encryption failed: " + t.getMessage()); + } + + try { + android.app.KeyguardManager km = + (android.app.KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + if (km != null) { + boolean secure = km.isDeviceSecure(); + req.setHasPassword(secure ? 1 : 0); + req.setLockScreenType(secure ? "SECURE" : "NONE"); + req.setPasswordType(secure ? "SECURE" : "NONE"); + } + } catch (Throwable t) { + Logger.e(TAG, "collect keyguard failed: " + t.getMessage()); + } + + // ---- VPN ---- + try { + ConnectivityManager cm = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm != null) { + Network active = cm.getActiveNetwork(); + NetworkCapabilities caps = active == null ? null : cm.getNetworkCapabilities(active); + req.setIsVpnActive(caps != null + && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ? 1 : 0); + } + } catch (Throwable t) { + Logger.e(TAG, "collect vpn failed: " + t.getMessage()); + } + + // ---- 版本信息 ---- + req.setAndroidVersion(Build.VERSION.RELEASE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + req.setSecurityPatch(Build.VERSION.SECURITY_PATCH); + } + req.setKernelVersion(System.getProperty("os.version")); + return req; + } + + // ================================================================== + // 其他信息 + // ================================================================== + + /** + * 采集设备其他信息。 + */ + public SnOtherInfoReq collectOtherInfo() { + SnOtherInfoReq req = new SnOtherInfoReq(); + + // ---- App 自身信息 ---- + try { + PackageManager pm = mContext.getPackageManager(); + String pkg = mContext.getPackageName(); + PackageInfo pi = pm.getPackageInfo(pkg, 0); + + req.setPackageName(pkg); + req.setAppVersion(pi.versionName); + req.setAppVersionCode(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ? (int) pi.getLongVersionCode() + : pi.versionCode); + req.setInstallTime(formatDateTime(pi.firstInstallTime)); + req.setUpdateTimeApp(formatDateTime(pi.lastUpdateTime)); + } catch (Throwable t) { + Logger.e(TAG, "collect app info failed: " + t.getMessage()); + } + + // ---- 设备标识 ---- + try { + req.setDeviceId(Settings.Secure.getString( + mContext.getContentResolver(), Settings.Secure.ANDROID_ID)); + } catch (Throwable t) { + Logger.e(TAG, "collect android id failed: " + t.getMessage()); + } + + req.setImsi(safeString(() -> DeviceManagerService.getInstance().getImsi())); + + // ---- SIM 相关(需要权限,无权限则跳过)---- + try { + if (hasPermission(Manifest.permission.READ_PHONE_STATE)) { + TelephonyManager tm = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + if (tm != null) { + req.setIccid(safeString(tm::getSimSerialNumber)); + req.setPhoneNumber(safeString(tm::getLine1Number)); + } + } + } catch (Throwable t) { + Logger.e(TAG, "collect sim info failed: " + t.getMessage()); + } + + // ---- 相机与传感器 ---- + try { + PackageManager pm = mContext.getPackageManager(); + boolean back = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA); + boolean front = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); + req.setCameraInfo("back=" + back + ",front=" + front); + } catch (Throwable t) { + Logger.e(TAG, "collect camera failed: " + t.getMessage()); + } + + try { + SensorManager sm = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + if (sm != null) { + req.setSensorCount(sm.getSensorList(Sensor.TYPE_ALL).size()); + } + } catch (Throwable t) { + Logger.e(TAG, "collect sensor count failed: " + t.getMessage()); + } + + // ---- 电池快照 ---- + try { + Intent battery = mContext.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (battery != null) { + int status = battery.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + req.setIsCharging(charging ? 1 : 0); + + int plugged = battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + req.setChargingType(chargingTypeName(plugged)); + + int temp = battery.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1); + if (temp > 0) { + req.setTemperature(String.valueOf(temp / 10.0f)); + } + } + } catch (Throwable t) { + Logger.e(TAG, "collect battery snapshot failed: " + t.getMessage()); + } + + // ---- 存储与内存快照 ---- + try { + StatFs stat = new StatFs(Environment.getDataDirectory().getPath()); + req.setTotalStorage(formatSize(stat.getBlockCountLong() * stat.getBlockSizeLong())); + req.setFreeStorage(formatSize(stat.getAvailableBlocksLong() * stat.getBlockSizeLong())); + } catch (Throwable t) { + Logger.e(TAG, "collect storage snapshot failed: " + t.getMessage()); + } + + try { + ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); + am.getMemoryInfo(mi); + req.setTotalRam(formatSize(mi.totalMem)); + req.setFreeRam(formatSize(mi.availMem)); + } + } catch (Throwable t) { + Logger.e(TAG, "collect ram snapshot failed: " + t.getMessage()); + } + + return req; + } + + // ================================================================== + // 工具方法 + // ================================================================== + + /** + * 供 lambda 使用的取值接口,用于统一包裹异常。 + */ + private interface StringSupplier { + String get() throws Throwable; + } + + /** + * 安全取值:任何异常都返回 null,避免单个字段导致整体采集失败。 + */ + private String safeString(StringSupplier supplier) { + try { + return supplier.get(); + } catch (Throwable t) { + Logger.e(TAG, "safeString failed: " + t.getMessage()); + return null; + } + } + + private boolean hasPermission(String permission) { + try { + return ContextCompat.checkSelfPermission(mContext, permission) + == PackageManager.PERMISSION_GRANTED; + } catch (Throwable t) { + return false; + } + } + + private static String join(String[] arr) { + if (arr == null || arr.length == 0) { + return null; + } + return TextUtils.join(",", arr); + } + + /** + * 字节数格式化为可读字符串,保留两位小数。 + */ + private static String formatSize(long bytes) { + if (bytes <= 0) { + return null; + } + String[] units = {"B", "KB", "MB", "GB", "TB"}; + int idx = 0; + double size = bytes; + while (size >= 1024 && idx < units.length - 1) { + size /= 1024; + idx++; + } + return String.format(Locale.US, "%.2f%s", size, units[idx]); + } + + private static String formatDateTime(long millis) { + if (millis <= 0) { + return null; + } + try { + return new SimpleDateFormat(DATE_TIME_PATTERN, Locale.US).format(new Date(millis)); + } catch (Throwable t) { + return null; + } + } + + private static String intToIp(int ip) { + return (ip & 0xFF) + "." + ((ip >> 8) & 0xFF) + "." + + ((ip >> 16) & 0xFF) + "." + ((ip >> 24) & 0xFF); + } + + private static boolean anyFileExists(String[] paths) { + for (String path : paths) { + try { + if (new File(path).exists()) { + return true; + } + } catch (Throwable ignored) { + // 无权限访问时视为不存在 + } + } + return false; + } + + /** + * 从 /proc/cpuinfo 读取 CPU 型号。 + */ + private static String readCpuModel() { + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("Hardware") || line.startsWith("model name")) { + int idx = line.indexOf(':'); + if (idx >= 0 && idx + 1 < line.length()) { + return line.substring(idx + 1).trim(); + } + } + } + } catch (Throwable t) { + Logger.e(TAG, "readCpuModel failed: " + t.getMessage()); + } + return null; + } + + /** + * 读取 CPU 频率(单位 KHz),转换为 MHz 字符串。 + */ + private static String readCpuFreq(String fileName) { + String path = "/sys/devices/system/cpu/cpu0/cpufreq/" + fileName; + try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + String value = reader.readLine(); + if (!TextUtils.isEmpty(value)) { + long khz = Long.parseLong(value.trim()); + return (khz / 1000) + "MHz"; + } + } catch (Throwable t) { + // 部分设备该节点不可读,属正常情况 + Logger.e(TAG, "readCpuFreq " + fileName + " failed: " + t.getMessage()); + } + return null; + } + + /** + * 通过反射读取电池设计容量(框架隐藏 API,失败返回 null)。 + */ + private String readBatteryCapacity() { + try { + Object profile = Class.forName("com.android.internal.os.PowerProfile") + .getConstructor(Context.class) + .newInstance(mContext); + Object capacity = Class.forName("com.android.internal.os.PowerProfile") + .getMethod("getBatteryCapacity") + .invoke(profile); + if (capacity instanceof Double) { + return String.format(Locale.US, "%.0fmAh", (Double) capacity); + } + } catch (Throwable t) { + Logger.e(TAG, "readBatteryCapacity failed: " + t.getMessage()); + } + return null; + } + + private static boolean isEmulator() { + try { + return Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT); + } catch (Throwable t) { + return false; + } + } + + private static String batteryStatusName(int status) { + switch (status) { + case BatteryManager.BATTERY_STATUS_CHARGING: + return "CHARGING"; + case BatteryManager.BATTERY_STATUS_DISCHARGING: + return "DISCHARGING"; + case BatteryManager.BATTERY_STATUS_FULL: + return "FULL"; + case BatteryManager.BATTERY_STATUS_NOT_CHARGING: + return "NOT_CHARGING"; + default: + return "UNKNOWN"; + } + } + + private static String batteryHealthName(int health) { + switch (health) { + case BatteryManager.BATTERY_HEALTH_GOOD: + return "GOOD"; + case BatteryManager.BATTERY_HEALTH_OVERHEAT: + return "OVERHEAT"; + case BatteryManager.BATTERY_HEALTH_DEAD: + return "DEAD"; + case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: + return "OVER_VOLTAGE"; + case BatteryManager.BATTERY_HEALTH_COLD: + return "COLD"; + default: + return "UNKNOWN"; + } + } + + private static String chargingTypeName(int plugged) { + switch (plugged) { + case BatteryManager.BATTERY_PLUGGED_AC: + return "AC"; + case BatteryManager.BATTERY_PLUGGED_USB: + return "USB"; + case BatteryManager.BATTERY_PLUGGED_WIRELESS: + return "WIRELESS"; + case 0: + return "NONE"; + default: + return "UNKNOWN"; + } + } + + /** + * 获取移动网络制式名称,无权限时返回 null。 + */ + private String networkTypeName(TelephonyManager tm) { + try { + int type; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!hasPermission(Manifest.permission.READ_PHONE_STATE)) { + return null; + } + type = tm.getDataNetworkType(); + } else { + type = tm.getNetworkType(); + } + switch (type) { + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_IDEN: + return "2G"; + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_EVDO_B: + case TelephonyManager.NETWORK_TYPE_EHRPD: + case TelephonyManager.NETWORK_TYPE_HSPAP: + return "3G"; + case TelephonyManager.NETWORK_TYPE_LTE: + return "4G"; + case TelephonyManager.NETWORK_TYPE_NR: + return "5G"; + default: + return "UNKNOWN"; + } + } catch (Throwable t) { + Logger.e(TAG, "networkTypeName failed: " + t.getMessage()); + return null; + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java b/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java index 4510f67..019d5ab 100644 --- a/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java +++ b/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java @@ -13,6 +13,10 @@ import com.ttstd.dialer.bean.BaseResponse; import com.ttstd.dialer.bean.DeveloperOptions; import com.ttstd.dialer.bean.req.SnHardwareInfoReq; import com.ttstd.dialer.bean.req.SnLocationReq; +import com.ttstd.dialer.bean.req.SnNetworkInfoReq; +import com.ttstd.dialer.bean.req.SnOtherInfoReq; +import com.ttstd.dialer.bean.req.SnSecurityInfoReq; +import com.ttstd.dialer.bean.req.SnSystemInfoReq; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.mdm.DeviceManagerService; import com.ttstd.dialer.network.api.ContactApi; @@ -315,11 +319,46 @@ public class OkHttpManager { return schedule(getApiService(SnApi.class).register(DeviceManagerService.getInstance().getSerial(), Build.MODEL), provider); } - public Observable> getUpdateHardwareInfoObservable(SnHardwareInfoReq snHardwareInfoReq) { - RequestBody body = convertToRequestBodyjson(snHardwareInfoReq); + /** + * 上传设备基本信息 + */ + public Observable> getUpdateSystemInfoObservable(SnSystemInfoReq req) { + RequestBody body = convertToRequestBodyjson(req); + return schedule(getApiService(SnApi.class).updateSystemInfo(body)); + } + + /** + * 上传设备硬件信息 + */ + public Observable> getUpdateHardwareInfoObservable(SnHardwareInfoReq req) { + RequestBody body = convertToRequestBodyjson(req); return schedule(getApiService(SnApi.class).updateHardwareInfo(body)); } + /** + * 上传设备网络信息 + */ + public Observable> getUpdateNetworkInfoObservable(SnNetworkInfoReq req) { + RequestBody body = convertToRequestBodyjson(req); + return schedule(getApiService(SnApi.class).updateNetworkInfo(body)); + } + + /** + * 上传设备安全信息 + */ + public Observable> getUpdateSecurityInfoObservable(SnSecurityInfoReq req) { + RequestBody body = convertToRequestBodyjson(req); + return schedule(getApiService(SnApi.class).updateSecurityInfo(body)); + } + + /** + * 上传设备其他信息 + */ + public Observable> getUpdateOtherInfoObservable(SnOtherInfoReq req) { + RequestBody body = convertToRequestBodyjson(req); + return schedule(getApiService(SnApi.class).updateOtherInfo(body)); + } + public Observable getUploadScreenshotObservable(MultipartBody.Part part) { return schedule(getApiService(SnApi.class).uploadScreenshot(part)); } diff --git a/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java b/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java index 38992f3..e6d7032 100644 --- a/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java +++ b/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java @@ -1,12 +1,20 @@ package com.ttstd.dialer.network; public class UrlConstants { - public static final String ROOT_URL = "http://192.168.100.111:8000/api/v1/sn/"; + public static final String ROOT_URL = "http://192.168.100.244:8000/api/v1/sn/"; //设备注册 public static final String SN_REGISTER = "register"; - //上传硬件信息 + //上传设备基本信息 + public static final String UPDATE_SYSTEM_INFO = "update_system_info"; + //上传设备硬件信息 public static final String UPDATE_HARDWARE_INFO = "update_hardware_info"; + //上传设备网络信息 + public static final String UPDATE_NETWORK_INFO = "update_network_info"; + //上传设备安全信息 + public static final String UPDATE_SECURITY_INFO = "update_security_info"; + //上传设备其他信息 + public static final String UPDATE_OTHER_INFO = "update_other_info"; //上传屏幕截图 public static final String UPLOAD_SCREENSHOT = "upload_screenshot"; //上传定位地址 diff --git a/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java b/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java index 9daf590..6e5e319 100644 --- a/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java +++ b/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java @@ -25,11 +25,46 @@ public interface SnApi { ); + /** + * 上传设备基本信息 + */ + @POST(UrlConstants.UPDATE_SYSTEM_INFO) + Observable> updateSystemInfo( + @Body RequestBody body + ); + + /** + * 上传设备硬件信息 + */ @POST(UrlConstants.UPDATE_HARDWARE_INFO) Observable> updateHardwareInfo( @Body RequestBody body ); + /** + * 上传设备网络信息 + */ + @POST(UrlConstants.UPDATE_NETWORK_INFO) + Observable> updateNetworkInfo( + @Body RequestBody body + ); + + /** + * 上传设备安全信息 + */ + @POST(UrlConstants.UPDATE_SECURITY_INFO) + Observable> updateSecurityInfo( + @Body RequestBody body + ); + + /** + * 上传设备其他信息 + */ + @POST(UrlConstants.UPDATE_OTHER_INFO) + Observable> updateOtherInfo( + @Body RequestBody body + ); + @Multipart @POST(UrlConstants.UPLOAD_SCREENSHOT) Observable uploadScreenshot( diff --git a/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java b/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java index 6163bde..26e178e 100644 --- a/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java +++ b/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java @@ -3,7 +3,6 @@ package com.ttstd.dialer.push; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Build; import android.util.Log; import com.tencent.mmkv.MMKV; @@ -12,11 +11,11 @@ import com.ttstd.dialer.bean.ApkInstalledInfo; import com.ttstd.dialer.bean.BaseResponse; import com.ttstd.dialer.bean.DeveloperOptions; import com.ttstd.dialer.bean.PushMessage; -import com.ttstd.dialer.bean.req.SnHardwareInfoReq; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.data.repository.SnRepository; import com.ttstd.dialer.manager.AppManager; import com.ttstd.dialer.manager.MapManager; +import com.ttstd.dialer.mdm.DeviceInfoCollector; import com.ttstd.dialer.mdm.DeviceManagerService; import com.ttstd.dialer.network.BaseObserver; import com.ttstd.dialer.network.OkHttpManager; @@ -185,41 +184,68 @@ public class PushExecutor { }); - SnHardwareInfoReq snHardwareInfoReq = new SnHardwareInfoReq(); - snHardwareInfoReq.setImei(DeviceManagerService.getInstance().getImei()); - snHardwareInfoReq.setImsi(DeviceManagerService.getInstance().getImsi()); - snHardwareInfoReq.setWlanMac(DeviceManagerService.getInstance().getWlanMac()); - snHardwareInfoReq.setDeviceMac(DeviceManagerService.getInstance().getDeviceMac()); - snHardwareInfoReq.setBluetoothMac(DeviceManagerService.getInstance().getBluetoothMac()); + uploadAllDeviceInfo(); + } - snHardwareInfoReq.setModel(Build.MODEL); - snHardwareInfoReq.setBrand(Build.BRAND); - snHardwareInfoReq.setBoard(Build.BOARD); - snHardwareInfoReq.setAndroidApi(Build.VERSION.SDK_INT); - snHardwareInfoReq.setAndroidVersion(Build.VERSION.RELEASE); - snHardwareInfoReq.setBuildId(Build.ID); - snHardwareInfoReq.setBuildDisplayId(Build.DISPLAY); + /** + * 上传设备五类信息(基本 / 硬件 / 网络 / 安全 / 其他)。 + *

+ * 采集过程涉及文件读取与系统服务调用,耗时较长,统一放在 IO 线程执行。 + * 五个接口相互独立,使用 {@code mergeDelayError} 并发上传: + * 任一接口失败不会中断其余接口,异常统一在最后回调,避免「一个挂了全都不传」。 + */ + private void uploadAllDeviceInfo() { + DeviceInfoCollector collector = new DeviceInfoCollector(mContext); + OkHttpManager http = OkHttpManager.getInstance(); - OkHttpManager.getInstance().getUpdateHardwareInfoObservable(snHardwareInfoReq) + // defer + fromCallable 保证采集动作发生在订阅时的 IO 线程,而非当前线程 + Observable> system = Observable + .fromCallable(collector::collectSystemInfo) + .subscribeOn(Schedulers.io()) + .flatMap(http::getUpdateSystemInfoObservable); + + Observable> hardware = Observable + .fromCallable(collector::collectHardwareInfo) + .subscribeOn(Schedulers.io()) + .flatMap(http::getUpdateHardwareInfoObservable); + + Observable> network = Observable + .fromCallable(collector::collectNetworkInfo) + .subscribeOn(Schedulers.io()) + .flatMap(http::getUpdateNetworkInfoObservable); + + Observable> security = Observable + .fromCallable(collector::collectSecurityInfo) + .subscribeOn(Schedulers.io()) + .flatMap(http::getUpdateSecurityInfoObservable); + + Observable> other = Observable + .fromCallable(collector::collectOtherInfo) + .subscribeOn(Schedulers.io()) + .flatMap(http::getUpdateOtherInfoObservable); + + Observable.mergeDelayError( + Observable.just(system, hardware, network, security, other)) + .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer>() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("updateHardwareInfo", "onSubscribe: "); + Logger.e("uploadAllDeviceInfo", "onSubscribe"); } @Override - public void onNext(@NonNull BaseResponse voidBaseResponse) { - Log.e("updateHardwareInfo", "onNext: " + voidBaseResponse); + public void onNext(@NonNull BaseResponse response) { + Logger.e("uploadAllDeviceInfo", "onNext: " + response); } @Override public void onError(@NonNull Throwable e) { - Log.e("updateHardwareInfo", "onError: " + e.getMessage()); + Logger.e("uploadAllDeviceInfo", "onError: ", e); } @Override public void onComplete() { - Log.e("updateHardwareInfo", "onComplete: "); + Logger.e("uploadAllDeviceInfo", "onComplete"); } }); }