简介:本资源是一套完整的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.2+(API 30+)环境下直接编译验证,无需任何第三方SDK或闭源库。
2. 用 Android SDK 原生 API 实现设备状态采集,避开高危反射调用
2.1 为什么不用TelephonyManager.getDeviceId()?——Android 10+ 的兼容性断层
在 Android 10(API 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.0(API 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 网络状态与信号强度:ConnectivityManager+TelephonyManager组合查询
单靠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-4,4为最强 } 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.0(API 23)引入 Doze 模式,Android 9(API 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; charset=utf-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 = PeriodicWorkRequestBuilder<MonitoringWorker>(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的必要性
即使使用WorkManager,Android 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:name="android.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_STATS onRequestPermissionsResult()中校验结果,失败则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(); List<UsageStats> 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(): List<ReportEntity> @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 指数退避参数表(单位:秒)
| 重试次数 | 退避时间 | 说明 |
|---|---|---|
| 1 | 10 | 首次失败后等待10秒 |
| 2 | 30 | 第二次失败后等待30秒 |
| 3 | 90 | 第三次失败后等待90秒 |
| 4 | 270 | 第四次失败后等待4.5分钟 |
| 5 | 810 | 第五次失败后等待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/.MonitoringWorker"6.3 检查权限授予状态
# 查看所有危险权限状态 adb shell dumpsys package com.example.monitoring | grep -A 20 "requested permissions" # 单独检查 PACKAGE_USAGE_STATS adb shell dumpsys usagestate | grep "com.example.monitoring" # 若输出包含 "granted=true",则已授权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_percent(0.0~100.0) # - network_type(WIFI/MOBILE) # - is_connected(true/false)提示:若抓包中无请求,优先检查
WorkManager是否注册成功(见6.1)、AndroidManifest.xml中android:exported="true"是否漏设(Android 12+ 强制要求)、以及minSdkVersion是否低于WorkManager支持的最低版本(21)。
最后一步,打开Logcat过滤Monitoring标签,观察日志流是否稳定输出Report sent successfully—— 当你看到连续10条以上无中断的日志,且adb shell cmd jobscheduler list显示Next run时间严格按15分钟递增,说明整套远程监控采集链路已在你的设备上闭环跑通。
本文还有配套的精品资源,点击获取