前言

上一篇中我们讲了,在 Android Studio 中通过二维码使用WIFI连接手机的原理,今天就来使用 Rust 来实现类似的功能。

没有看过的建议看一下,不然这篇文档可能会看不懂。

为什么要用 Rust 呢?

一方面是 Go 已经有了,而刚好我想用 Rust 来练练手,所以就是它了。

回顾使用二维码连接的流程

在开发之前我们先来回顾一下二维码的连接过程,主要有以下这几个步骤

  1. 创建两个随机字符串,一个用于名字,一个用于密码
  2. 根据规则组装二维码的内容并生成二维码,等待用户扫码
  3. 通过 adb 检测 mdns 服务,看看是否有我们需要的
  4. 使用 adb pair 命令进行配对
  5. 使用 adb 查看连接列表是否已经连接成功

准备

在开发之前我们来分析一下需要使用那些第三方库

开发

添加依赖

根据上面的分析,我们把依赖加到 Cargo.toml

1
2
3
4
5
6
7
8
[dependencies]
qrcode = "0.12"
mdns = "3.0.0"
rand = "0.8.5"
async-stream = "0.2.0"
async-std = { version = "1.6.2", features = ["unstable", "attributes"] }
futures-core = "0.3.1"
futures-util = "0.3.1"

由于 mdns 使用到了协程,所以我们还需把 async-stdasnyc-streamfeatures-corefeatures-util 加上

生成指定个数的字符串

使用 rand 生成随机字符串,在这里我们要指定生成字符串的个数,然后从我们给定的字符集中随机取固定的个数,组成随机字符串。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const CHAR_SET: &str =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-+*/<>{}";
fn create_random_string(len: u32) -> String {
    let mut result = String::new();
    let mut rng = thread_rng();
    (0..=len).for_each(|_i| {
        result.push(CHAR_SET.chars().choose(&mut rng).unwrap());
    });
    result
}

组装二维码内容

有了生成随机的字符串后,我们就需要生成一个10个字符的名字和12个字符串的密码,然后按照 WIFI:T:ADB;S:${name};P:${pwd};; 的格式来生成二维码的内容。

这里要注意名字还有一个 studio 的前缀,其实没有也是可以的。

1
2
3
let server_id = format!("studio-{}", create_random_string(10));
let pwd = create_random_string(12);
let pair_string = format!("WIFI:T:ADB;S:{};P:{};;", server_id, pwd);

生成二维码

生成二维码也挺简单的,由于我们是在终端中使用,所以只需要使用 Unicode 字符就行了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn generate_qr_code(content: &str) {
    let code = QrCode::new(content).unwrap();
    let image = code
        .render::<unicode::Dense1x2>()
        .dark_color(unicode::Dense1x2::Light)
        .light_color(unicode::Dense1x2::Dark)
        .build();
    println!("{}", image);
}

generate_qr_code(&pair_string);

把上一步组装好的内容当做参数传入,生成好之后,打印到控制台中,就能够看到二维码了。

检测配对服务

生成好二维码之后,等待用户扫码,然后我们这里需要检测看看有没有配对的服务。有才能进行配对。

这里有一个需要注意的地方,我们通过 adb mdns services 得到的结果是不带 .local 后缀的,但是我们使用 mdns 去检测的时候要带上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async fn discovery_pair_services(service: &str) -> Result<Option<SocketAddr>, Error> {
    let stream = mdns::discover::all(service, Duration::from_secs(15))?.listen();
    pin_mut!(stream);
    while let Some(Ok(response)) = stream.next().await {
        let host = response.hostname();
        let socket_addr = response.socket_address();
        if let (Some(host), Some(socket_addr)) = (host, socket_addr) {
            println!("host: {}, socket_addr: {}", host, socket_addr);
            if host == "_adb-tls-pairing._tcp.local" {
                return Ok(Some(socket_addr));
            }
        }
    }
    Ok(None)
}

let res = discovery_pair_services("_adb-tls-pairing._tcp.local").await;

进行配对

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if let Ok(res) = res {
    if let Some(res) = res {
        let pair_result = Command::new("adb")
            .arg("pair")
            .arg(res.to_string())
            .arg(pwd)
            .output()
            .expect("failed to execute process");
        println!("{}", String::from_utf8(pair_result.stdout).unwrap());
    }
}

检测到配对服务后,我们就可以拿到它的 IP 和端口,由于返回的是个 SocketAddr ,所以调用它的 to_string 就能够得到 ip:port 的形式。

在进行配对环节时,我们要调用 adb 这个外部程序,所以需要使用 Commond 来执行。

Command::new 中传入外部程序的名字,然后通过 arg 来添加一个参数,通过 output 来以子进程的方式执行,并获取输出的结果。

Command 使用起来还是非常方便的,就像平时执行指令一样,只需要对指令和参数进行组装就行了。

查看是否连接成功

配对完成之后,还需要查看是否连接成功,这时候就需要通过把已经连接上的设备打印出来,看看我们的设备是否在其中。执行这条指令要稍微延迟一下,连接有时会慢一点。

1
2
3
4
5
6
let devices = Command::new("adb")
    .arg("devices")
    .arg("-l")
    .output()
    .expect("failed to execute process");
println!("{}", String::from_utf8(devices.stdout).unwrap());

总结

根据之前的分析,我们知道了大致的思路,所以写起来还是比较快的。

100行的代码不到,就实现了一个二维码连接手机的功能。

完整的代码在 devbins/qrcode_pair_rs

参考