Connect to a device over Wi-Fi (Android 11+)
在Android 11以上的版本中,Google(重新)提供了通过WiFi建立ADB连接的途径:通过配对码完成配对并连接.但是不便之处在于:配对码包含随机数字,而且配对接口随机变化,属实难顶,但是在使用Android Studio的时候,却发现其提供了使用二维码配对的方式.这种方式明显更为简便,但是Google却没有发布相关的文档.而且这个特性目前还只在不稳定的Canary版本提供,所以把它单独提取出来很有必要 XD.
解析二维码
那么AS提供的二维码中包含了什么呢?我们找了一个扫码工具进行解析:
WIFI:T:ADB;S:studio-yN<^C$&N!x;P:Ca>Uig0o!xNd;;
从格式上来看,就是典型的WiFi分享二维码,不过加密方式写为ADB,SSID与密码好像都是随机字符串.
查阅AS源码
好在Android Studio开放了几乎全部的源码,Google还提供了强悍的代码在线检索查看系统.检索Adb Pair
相关代码,很容易找到了实现这个功能的具体插件目录.即 https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-master-dev:android-adb/src/com/android/tools/idea/adb/wireless/ .抛去一些UI类与接口类,我们直接分析实现(即Impl类).最引人注意的便是WiFiPairingServiceImpl.kt
这个Kotlin文件.文件不到200行,但是却实现了WiFi ADB Pair的功能.原因正是在于这个功能其实是由Android系统和ADB工具提供的,这个插件不过是对命令行的封装.下面列出几个关键函数:
override fun checkMdnsSupport(): ListenableFuture<MdnsSupportState> {
// TODO: Investigate updating (then using) ddmlib instead of spawning an adb client command, so that
// we don't have to rely on parsing command line output
LOG.info("Checking if mDNS is supported (`adb mdns check` command)")
val futureResult = adbService.executeCommand(listOf("mdns", "check"))
return futureResult.transform(taskExecutor) { result ->
when {
result.errorCode != 0 -> {
LOG.warn("`adb mdns check` returned a non-zero error code (${result.errorCode})")
val isUnknownCommand = result.stderr.any { line -> line.contains(Regex("unknown.*command")) }
if (isUnknownCommand)
MdnsSupportState.AdbVersionTooLow
else
MdnsSupportState.AdbInvocationError
}
result.stdout.isEmpty() -> {
LOG.warn("`adb mdns check` returned an empty output (why?)")
MdnsSupportState.AdbInvocationError
}
// See https://android-review.googlesource.com/c/platform/system/core/+/1274009/5/adb/client/transport_mdns.cpp#553
result.stdout.any { it.contains("mdns daemon version") } -> {
MdnsSupportState.Supported
}
else -> {
MdnsSupportState.NotSupported
}
}
}.catching(taskExecutor, Throwable::class.java) { t ->
.....
checkMdnsSupport()
见名知义,是通过adb mdns check
这个命令来检测此ADB版本是否支持mdns
不支持就直接报错退出.(话说Google自己也感觉通过解析获取Cli的返回值有点low,想采用ddmlib来替代)
override fun scanMdnsServices(): ListenableFuture<List<MdnsService>> {
val futureResult = adbService.executeCommand(listOf("mdns", "services"))
return futureResult.transform(taskExecutor) { result ->
// Output example:
// List of discovered mdns services
// adb-939AX05XBZ-vWgJpq _adb-tls-connect._tcp. 192.168.1.86:39149
// adb-939AX05XBZ-vWgJpq _adb-tls-pairing._tcp. 192.168.1.86:37313
// Regular expression
// adb-<everything-until-space><spaces>__adb-tls-pairing._tcp.<spaces><everything-until-colon>:<port>
val lineRegex = Regex("([^\\t]+)\\t*_adb-tls-pairing._tcp.\\t*([^:]+):([0-9]+)")
if (result.errorCode != 0) {
throw AdbCommandException("Error discovering services", result.errorCode, result.stderr)
}
if (result.stdout.isEmpty()) {
throw AdbCommandException("Empty output from \"adb mdns services\" command", -1, result.stderr)
}
return@transform result.stdout
.drop(1)
.mapNotNull { line ->
val matchResult = lineRegex.find(line)
matchResult?.let {
try {
val serviceName = it.groupValues[1]
val ipAddress = InetAddress.getByName(it.groupValues[2])
val port = it.groupValues[3].toInt()
val serviceType = if (serviceName.startsWith(studioServiceNamePrefix)) ServiceType.QrCode else ServiceType.PairingCode
MdnsService(serviceName, serviceType, ipAddress, port)
}
catch (ignored: Exception) {
LOG.warn("mDNS service entry ignored due do invalid characters: ${line}")
null
}
}
}
}
}
这个函数是通过adb mdns service
来扫描网络中的adb service.经过研读相关代码和实践,可以得出:
处于配对码模式的ADB服务会发起一个_adb-tls-connect
为服务名称的mDNS 服务
,从而成功广播自己的IP与端口.而扫描二维码模式的ADB服务,会发起一个_adb-tls-pairing
为服务名称,服务节点名称为二维码中SSID的值,配对码则为二维码中密码的值的mDNS服务
.
override fun pairMdnsService(mdnsService: MdnsService, password: String): ListenableFuture<PairingResult> {
LOG.info("Start mDNS pairing: ${mdnsService}")
val deviceAddress = "${mdnsService.ipAddress.hostAddress}:${mdnsService.port}"
// TODO: Update this when password can be passed as an argument
val passwordInput = password + LineSeparator.getSystemLineSeparator().separatorString
val futureResult = adbService.executeCommand(listOf("pair", deviceAddress), passwordInput)
return futureResult.transform(taskExecutor) { result ->
LOG.info("mDNS pairing exited with code ${result.errorCode}")
result.stdout.take(5).forEachIndexed { index, line ->
LOG.info(" stdout line #$index: $line") }
if (result.errorCode != 0) {
throw AdbCommandException("Error pairing device", result.errorCode, result.stderr)
}
if (result.stdout.isEmpty()) {
throw AdbCommandException("Empty output from \"adb pair\" command", -1, result.stderr)
}
// Output example:
// Enter pairing code: Successfully paired to 192.168.1.86:41915 [guid=adb-939AX05XBZ-vWgJpq]
// Regular expression
// <Prefix><everything-until-colon>:<port>[guid=<everything-until-close-bracket>]
val lineRegex = Regex("Successfully paired to ([^:]*):([0-9]*) \\[guid=([^\\]]*)\\]")
val matchResult = lineRegex.find(result.stdout[0])
matchResult?.let {
try {
val ipAddress = InetAddress.getByName(it.groupValues[1])
val port = it.groupValues[2].toInt()
val serviceGuid = it.groupValues[3]
PairingResult(ipAddress, port, serviceGuid)
}
catch (e: Exception) {
throw InvalidDataException("Pairing result is invalid", e)
}
} ?: throw InvalidDataException("Pairing result is invalid")
}
}
pairMdnsService()
调用了adb pair IP:Port Password
这种格式的命令来完成配对(因此二维码模式其实仍然需要配对码,只是配对码在二维码中预先指定了而已
在配对成功后,macOS系统会自动连接,不知道Windows和Linux的情况如何,还未测试.
原理
由上述分析可得,流程为
生成加密方式为ADB的WIFI连接二维码=>Android扫码,发布_adb-tls-pairing
为服务名称,服务节点名称为二维码中SSID的值,配对码则为二维码中密码的值的mDNS服务
=>扫描网络中的mDNS服务
,获取相应服务的IP与Port=>利用IP-Port-Password 来执行adb Pair配对
使用Go编写命令行程序
我们采用Go编写一个命令行程序来实现这个功能:
- 使用 https://github.com/mdp/qrterminal 库来实现二维码的展示
考虑到某些旧版本ADB不支持mDNS(但是却支持ADB Pair),我们采用mDNS的Go语言实现:
github.com/grandcat/zeroconf
最后的成品发布在GitHub: