Android设备状态采集与上报工程实践(API 30+)

📅 发布时间:2026/9/16 13:06:29
Android设备状态采集与上报工程实践(API 30+)
简介本资源是一套完整的Android手机远程监控系统源码面向具备Java/Kotlin基础的Android开发者聚焦物联网与智能终端监控场景解决设备端实时视频采集、网络传输、权限管理及服务后台交互等核心问题。压缩包共66个文件含40个class字节码与14个java源文件构成主逻辑5个xml定义界面与权限配置另有apk安装包、dex可执行文件及project工程配置整体仅133KB轻量易导入调试。已有3442人学习下载适合中高级开发者通过该项目深入理解Camera2 API调用、WebSocket/HTTP流式传输、多线程图像编码如H.264封装、运行时权限申请及基础安全加固实践。代码结构清晰包含CamMonitor主监控模块与服务启动入口便于快速复现远程画面推流与本地预览功能。1. 这不是“远程控制手机”的玩具而是 Android 设备状态采集与上报的工程实践很多人看到“Android手机远程监控源码”第一反应是“能偷偷看别人屏幕”——这既误解了技术边界也混淆了合法场景。真实工业级、企业级或IoT运维场景中的“远程监控”核心是非侵入式、低开销、可审计的状态感知与指标上报比如产线测试机的电量/温度/网络连通性持续回传、教育平板的开机率与应用使用时长统计、车载终端的GPS定位与传感器异常告警。这类需求不依赖Root不劫持UI不绕过Android权限模型而是基于android.permission.READ_PHONE_STATE、android.permission.ACCESS_FINE_LOCATION、android.permission.PACKAGE_USAGE_STATS等明确声明的危险权限配合JobIntentService或WorkManager实现后台稳定采集并通过HTTPS接口将结构化数据JSON推送到自有服务器。本篇聚焦于该类源码包中最常复用的四个模块设备基础信息采集逻辑、后台任务调度机制、敏感权限动态申请流程、以及上报数据的序列化与重试策略——所有代码均可在 Android Studio 2023.2API 30环境下直接编译验证无需任何第三方SDK或闭源库。2. 用 Android SDK 原生 API 实现设备状态采集避开高危反射调用2.1 为什么不用TelephonyManager.getDeviceId()——Android 10 的兼容性断层在 Android 10API 29之后getDeviceId()被标记为Deprecated且对非系统应用返回空字符串或抛出SecurityException。强行调用不仅导致崩溃更违反 Google Play 政策。正确做法是分层降级首选Settings.Secure.ANDROID_ID设备首次启动生成跨App共享不可重置除非恢复出厂次选Build.SERIAL需READ_PHONE_STATE仅限 Android 8.0 以下Android 9 需QUERY_ALL_PACKAGES权限极难获批兜底UUID.randomUUID().toString()仅限单App生命周期内唯一需本地持久化存储// DeviceInfoHelper.java public static String getUniqueDeviceId(Context context) { // 1. 尝试 ANDROID_ID无权限要求最安全 String androidId Settings.Secure.getString( context.getContentResolver(), Settings.Secure.ANDROID_ID ); if (!TextUtils.isEmpty(androidId) !9774d56d682e549c.equals(androidId)) { return androidId; } // 2. Android 8.0 以下尝试 SERIAL需 READ_PHONE_STATE if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) PackageManager.PERMISSION_GRANTED) { return Build.SERIAL; } } // 3. 兜底生成并存入 SharedPreferences SharedPreferences sp context.getSharedPreferences(device_id, Context.MODE_PRIVATE); String fallbackId sp.getString(fallback_id, null); if (fallbackId null) { fallbackId UUID.randomUUID().toString(); sp.edit().putString(fallback_id, fallbackId).apply(); } return fallbackId; }提示9774d56d682e549c是模拟器的固定ANDROID_ID必须过滤否则所有模拟器上报同一ID导致数据污染。2.2 电池与温度采集避开已废弃的BatteryManager直接读取Android 7.0API 24起BatteryManager的getIntProperty()方法被移除旧代码会直接NoSuchMethodError。新方案必须监听ACTION_BATTERY_CHANGED广播隐式无需注册再从Intent中提取字段// BatteryMonitor.java public static BatteryStatus getBatteryStatus(Context context) { Intent batteryIntent context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); int level batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); int temperature batteryIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1); // 单位0.1℃ int status batteryIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); float batteryPercent (level * 100f) / scale; boolean isCharging status BatteryManager.BATTERY_STATUS_CHARGING || status BatteryManager.BATTERY_STATUS_FULL; return new BatteryStatus(batteryPercent, temperature / 10f, isCharging); }2.2.1 关键参数说明字段含义注意事项EXTRA_LEVEL/EXTRA_SCALE当前电量值与满电值scale通常为100但部分厂商可能不同必须用除法计算百分比EXTRA_TEMPERATURE温度原始值返回单位是0.1℃需除以10转为摄氏度若为-1表示传感器不可用EXTRA_STATUS充电状态BATTERY_STATUS_CHARGING表示正在充电BATTERY_STATUS_FULL表示已充满2.3 网络状态与信号强度ConnectivityManagerTelephonyManager组合查询单靠ConnectivityManager.getActiveNetworkInfo()已在 Android 10 被弃用。必须拆解为两步网络连通性用ConnectivityManager.getNetworkCapabilities()判断是否具备NET_CAPABILITY_INTERNET信号强度对蜂窝网络用TelephonyManager.getSignalStrength()需ACCESS_COARSE_LOCATION// NetworkMonitor.java public static NetworkStatus getNetworkStatus(Context context) { ConnectivityManager cm (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); Network network cm.getActiveNetwork(); NetworkStatus status new NetworkStatus(); if (network ! null) { NetworkCapabilities caps cm.getNetworkCapabilities(network); status.isConnected caps ! null caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); // 获取网络类型名称如 WIFI / MOBILE if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { status.type WIFI; } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { status.type MOBILE; // 获取信号强度仅限蜂窝 TelephonyManager tm (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) PackageManager.PERMISSION_GRANTED) { try { SignalStrength ss tm.getSignalStrength(); status.signalLevel ss.getLevel(); // 0-44为最强 } catch (Exception e) { status.signalLevel -1; } } } } return status; }注意getSignalStrength()在 Android 10 需要ACCESS_COARSE_LOCATION且仅对前台App有效后台服务中获取到的值可能为0或-1这是系统限制非代码缺陷。3. 后台任务调度WorkManager 替代 AlarmManager 的强制迁移路径3.1 为什么 AlarmManager 在 Android 9 失效Doze 模式与待机白名单机制Android 6.0API 23引入 Doze 模式Android 9API 28进一步收紧后台执行限制AlarmManager.setExactAndAllowWhileIdle()仅允许每9分钟触发一次setRepeating()完全失效。硬编码AlarmManager的源码在真机上必然失准。WorkManager是官方唯一推荐的、兼容 API 21 的后台任务解决方案它自动适配 Doze、App Standby 和电池优化策略。3.2 构建一个每15分钟上报一次的周期性任务// MonitoringWorker.kt class MonitoringWorker( private val context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { try { // 1. 采集设备状态 val deviceInfo DeviceInfoHelper.getDeviceInfo(context) val battery BatteryMonitor.getBatteryStatus(context) val network NetworkMonitor.getNetworkStatus(context) // 2. 构建上报JSON val report JSONObject().apply { put(device_id, deviceInfo.id) put(timestamp, System.currentTimeMillis()) put(battery_percent, battery.percent) put(temperature_c, battery.temperature) put(is_charging, battery.isCharging) put(network_type, network.type) put(signal_level, network.signalLevel) put(is_connected, network.isConnected) } // 3. HTTPS上报使用OkHttp val client OkHttpClient() val request Request.Builder() .url(https://your-api.com/v1/monitoring) .post(RequestBody.create( MediaType.parse(application/json; charsetutf-8), report.toString() )) .build() val response client.newCall(request).execute() if (response.isSuccessful) { Log.d(Monitoring, Report sent successfully) return Result.success() } else { Log.e(Monitoring, Report failed: ${response.code()}) return Result.retry() // 触发重试 } } catch (e: Exception) { Log.e(Monitoring, Work failed, e) return Result.retry() } } }3.2.1 WorkManager 初始化与约束配置// 在 Application.onCreate() 中初始化 class MyApplication : Application() { override fun onCreate() { super.onCreate() val constraints Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) // 必须联网 .setRequiresBatteryNotLow(true) // 电池未低电量 .setRequiresCharging(false) // 不强制要求充电按需设置 .build() val workRequest PeriodicWorkRequestBuilderMonitoringWorker(15, TimeUnit.MINUTES) .setConstraints(constraints) .setInitialDelay(1, TimeUnit.MINUTES) // 首次延迟1分钟避免冷启动拥堵 .build() WorkManager.getInstance(this) .enqueueUniquePeriodicWork( monitoring_work, ExistingPeriodicWorkPolicy.KEEP, // 避免重复注册 workRequest ) } }提示PeriodicWorkRequestBuilder的最小间隔为15分钟这是 Android 系统硬性限制试图设为更短将被静默提升至15分钟。3.3 权限申请与后台执行豁免REQUEST_IGNORE_BATTERY_OPTIMIZATIONS的必要性即使使用WorkManagerAndroid 6.0 仍可能因电池优化导致任务被系统终止。必须引导用户手动关闭优化// 在首次启动时检查并请求 private void requestBatteryOptimization() { PowerManager pm (PowerManager) getSystemService(POWER_SERVICE); if (Build.VERSION.SDK_INT Build.VERSION_CODES.M !pm.isIgnoringBatteryOptimizations(getPackageName())) { Intent intent new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse(package: getPackageName())); startActivity(intent); } }注意此权限需在AndroidManifest.xml中声明uses-permission android:nameandroid.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS /4. 敏感权限动态申请ActivityCompat.requestPermissions()的精准时机控制4.1 权限组与运行时申请的不可跳过性Android 6.0 将权限分为普通权限安装即授和危险权限需运行时申请。READ_PHONE_STATE、ACCESS_FINE_LOCATION、PACKAGE_USAGE_STATS均属危险权限且不能一次性全部申请——PACKAGE_USAGE_STATS需跳转系统设置页其他权限需requestPermissions()。错误地在onCreate()中批量申请会导致用户拒绝后无法再次触发造成功能残缺。4.2 分阶段、按需申请的三步法首次启动只申请基础权限READ_PHONE_STATE,ACCESS_COARSE_LOCATION用户点击“开始监控”按钮时再申请PACKAGE_USAGE_STATSonRequestPermissionsResult()中校验结果失败则Toast提示并禁用对应功能// MainActivity.java private void requestBasicPermissions() { String[] permissions { Manifest.permission.READ_PHONE_STATE, Manifest.permission.ACCESS_COARSE_LOCATION }; ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_BASIC); } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQUEST_CODE_BASIC) { boolean allGranted true; for (int result : grantResults) { if (result ! PackageManager.PERMISSION_GRANTED) { allGranted false; break; } } if (allGranted) { // 启动监控服务 startMonitoringService(); } else { Toast.makeText(this, 缺少必要权限监控功能受限, Toast.LENGTH_LONG).show(); } } } // 点击按钮时申请 UsageStats public void onEnableUsageStatsClick(View view) { Intent intent new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS); startActivity(intent); }4.2.1PACKAGE_USAGE_STATS权限校验代码// 检查是否已授权 private boolean hasUsageStatsPermission() { UsageStatsManager usm (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); long time System.currentTimeMillis(); ListUsageStats stats usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 600, time); return stats ! null !stats.isEmpty(); }提示queryUsageStats()返回非空列表即代表已授权空列表或SecurityException表示未授权。该方法本身不触发权限弹窗仅作状态判断。5. 数据上报可靠性带指数退避的 OkHttp 重试 本地 SQLite 缓存队列5.1 网络不稳定场景下的数据丢失风险为什么不能只靠Result.retry()WorkManager的Result.retry()仅保证任务重试但若连续多次失败系统可能将任务放入“永久失败”状态并停止调度。真实生产环境必须实现本地持久化缓存 指数退避重试确保网络恢复后补传。5.2 使用 Room 创建轻量级上报队列表// ReportEntity.kt Entity(tableName report_queue) data class ReportEntity( PrimaryKey(autoGenerate true) val id: Long 0, val jsonPayload: String, val createdAt: Long System.currentTimeMillis(), val retryCount: Int 0, val maxRetries: Int 5 ) // ReportDao.kt Dao interface ReportDao { Insert(onConflict OnConflictStrategy.IGNORE) suspend fun insert(report: ReportEntity): Long Query(SELECT * FROM report_queue ORDER BY createdAt ASC LIMIT 10) suspend fun getPendingReports(): ListReportEntity Query(DELETE FROM report_queue WHERE id :id) suspend fun deleteById(id: Long) Query(UPDATE report_queue SET retryCount retryCount 1 WHERE id :id) suspend fun incrementRetry(id: Long) }5.3 在 Worker 中集成缓存与重试逻辑override suspend fun doWork(): Result { val db (applicationContext as MyApplication).database val dao db.reportDao() // 1. 优先处理缓存队列 val pending dao.getPendingReports() if (pending.isNotEmpty()) { val first pending[0] try { val client OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build() val request Request.Builder() .url(https://your-api.com/v1/monitoring) .post(RequestBody.create( MediaType.parse(application/json), first.jsonPayload )) .build() val response client.newCall(request).execute() if (response.isSuccessful) { dao.deleteById(first.id) return Result.success() } else if (first.retryCount first.maxRetries) { // 达到最大重试次数丢弃 dao.deleteById(first.id) return Result.success() // 不重试避免死循环 } else { dao.incrementRetry(first.id) return Result.retry() // 触发WorkManager重试 } } catch (e: Exception) { if (first.retryCount first.maxRetries) { dao.incrementRetry(first.id) return Result.retry() } else { dao.deleteById(first.id) return Result.success() } } } // 2. 若无缓存采集新数据并插入队列 val reportJson buildReportJson() // 同前文逻辑 dao.insert(ReportEntity(jsonPayload reportJson)) return Result.success() }5.3.1 指数退避参数表单位秒重试次数退避时间说明110首次失败后等待10秒230第二次失败后等待30秒390第三次失败后等待90秒4270第四次失败后等待4.5分钟5810第五次失败后等待13.5分钟注意WorkManager本身不支持动态退避时间因此需在doWork()内部用delay()控制但会阻塞Worker线程。更优解是将退避逻辑写入数据库字段在下次调度时读取next_retry_at时间戳判断是否执行。6. 验证与调试用 ADB 命令快速确认监控逻辑是否生效6.1 查看 WorkManager 当前调度状态adb shell cmd jobscheduler list # 输出示例 # Job #0: com.example.monitoring/.MonitoringWorker # UID: 10123 # Status: RUNNING (since 123456 ms ago) # Last run: 2024-06-15 14:22:30 # Next run: 2024-06-15 14:37:306.2 手动触发一次监控任务跳过15分钟等待adb shell am broadcast -a androidx.work.action.TRIGGER_WORK \ --es androidx.work.impl.WorkManagerImpl.SERVICE_ACTION com.example.monitoring/.MonitoringWorker6.3 检查权限授予状态# 查看所有危险权限状态 adb shell dumpsys package com.example.monitoring | grep -A 20 requested permissions # 单独检查 PACKAGE_USAGE_STATS adb shell dumpsys usagestate | grep com.example.monitoring # 若输出包含 grantedtrue则已授权6.4 抓包验证上报数据格式与频率# 启动 mitmproxy 或 Charles设置代理 adb shell settings put global http_proxy 192.168.1.100:8888 adb shell setprop net.dns1 192.168.1.100 # 观察抓包中 POST /v1/monitoring 请求体确认 JSON 结构含 # - device_id非空且唯一 # - timestamp毫秒级时间戳 # - battery_percent0.0~100.0 # - network_typeWIFI/MOBILE # - is_connectedtrue/false提示若抓包中无请求优先检查WorkManager是否注册成功见6.1、AndroidManifest.xml中android:exportedtrue是否漏设Android 12 强制要求、以及minSdkVersion是否低于WorkManager支持的最低版本21。最后一步打开Logcat过滤Monitoring标签观察日志流是否稳定输出Report sent successfully—— 当你看到连续10条以上无中断的日志且adb shell cmd jobscheduler list显示Next run时间严格按15分钟递增说明整套远程监控采集链路已在你的设备上闭环跑通。本文还有配套的精品资源点击获取