feat: 设备端 Agent 首个版本——剪贴板注入 / 应用商店 / 身份显示 / 配置下发

配合 auto_control 平台的设备端补位 App(接口契约见 INTERFACE.md,与平台仓库
doc/DEVICE_AGENT.md 是同一份)。

四个 adb 可拉起的 Activity + 一个商店界面:
- ClipActivity    剪贴板注入(透明 Activity,**必须等窗口获得焦点再写** ——
                  Android 10+ 只有前台应用能写剪贴板,这正是 u2 set_clipboard
                  "成功但内容被丢弃"的原因);写完立即退出,不驻留
- InfoActivity    全屏大字显示设备身份(名称/IP/序列号/平台地址),
                  替代原来"推 HTML 让设备浏览器打开"那套
- ConfigActivity  一次性配置下发(平台用 adb 把 server/token/指纹推进来),
                  一批设备不用一台台手输
- PingActivity    自检(版本/指纹/配置状态),结果写 logcat + 外部私有目录的
                  status.json,平台据此判断"装没装、哪一版"
- MainActivity    应用商店:拉平台清单 → 选应用 → 安装;已装的显示「已安装」,
                  有新版显示「更新到 vX」

实机踩出来的几个坑(都已处理):
- **应用身份读不到 ro.serialno**(shell 能读、应用被 SELinux 挡)→ 指纹改由平台
  通过 ConfigActivity 下发(平台本来就知道),本机 getprop 只作兜底
- **Android 9+ 默认拦明文 HTTP** → 平台是局域网 http,需 usesCleartextTraffic
- **Android 11+ 软件包可见性** → 不加 QUERY_ALL_PACKAGES 会"明明装了却显示未安装"
- **MIUI 安装确认不止一步** → 装完检测窗口给到 3 分钟(人会依次点继续→勾选→继续更新)
- 上报带重试(实测遇到过一次网络抖动导致 timeout)

构建:JDK 17 + Android SDK(minSdk 29 / targetSdk 34),只依赖 androidx.core,
HTTP 用 HttpURLConnection、JSON 用系统内置 org.json。
This commit is contained in:
2026-09-13 16:30:16 +08:00
commit 2580cfaead
20 changed files with 1455 additions and 0 deletions
@@ -0,0 +1,47 @@
package com.example.deviceagent
import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
/**
* 一次性配置下发(透明 Activity,由平台用 adb 拉起):
*
* adb shell am start -n com.example.deviceagent/.ConfigActivity \
* --es server http://192.168.20.220:18050 --es token <设备令牌> \
* --es fingerprint <ro.serialno>
*
* 有了它,一批设备不用一台台手输地址和令牌。
* 只写传进来的字段;三个都能单独更新。
*
* ⚠️ fingerprint 必须由平台推下来:实测应用身份读不到 ro.serialno
*(shell 能读、应用被 SELinux 挡住),而平台通过 adb 本来就知道这个值。
*/
class ConfigActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val server = intent?.getStringExtra("server")
val token = intent?.getStringExtra("token")
val fp = intent?.getStringExtra("fingerprint")
if (server.isNullOrEmpty() && token.isNullOrEmpty() && fp.isNullOrEmpty()) {
Log.w(TAG, "ConfigActivity 没收到 server/token/fingerprint")
finish()
return
}
try {
Prefs.save(this, server, token, fp)
val msg = "配置已更新:${Prefs.server(this)} / 令牌${if (Prefs.token(this).isEmpty()) "空" else "已设置"} / 指纹${if (Prefs.fingerprint(this).isEmpty()) "空" else "已设置"}"
Log.i(TAG, msg)
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e(TAG, "配置写入失败: ${e.message}")
}
finish()
}
companion object {
const val TAG = "DeviceAgent"
}
}