使用二维码链接WiFi ADB

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编写一个命令行程序来实现这个功能:

  1. 使用 https://github.com/mdp/qrterminal 库来实现二维码的展示
  2. 考虑到某些旧版本ADB不支持mDNS(但是却支持ADB Pair),我们采用mDNS的Go语言实现:github.com/grandcat/zeroconf

    最后的成品发布在GitHub:

    https://github.com/lx200916/ADB_Pair_Go

tag(s): none
show comments · back · home
Edit with markdown