其他语言

通过 HTTP 或原始套接字(每条命令一行 JSON),在 Go、Java、C#、PHP 或任何其他语言中使用 Droidline,附带经过测试的示例。

本页为译文。如与英文版不一致,以英文版为准。 English

并不是每种语言都有 SDK,你也不需要 SDK。服务器接受两种发送命令的方式,任何语言都能使用:

  • HTTP:每条命令一个请求。这是最简单的选择;所有端点见 HTTP 指南。
  • 原始套接字:打开一个到 127.0.0.1:8780 的 TCP 连接,每行写入一个 JSON 对象,再逐行读回 JSON 对象。它更适合长时间运行的程序,也是接收通知等事件的唯一方式。

本页的每个示例都在 droidline-fakephone 上运行过。每个示例都会打开演示应用,尝试点击一个不存在的元素来演示错误处理,并读取一个值。

通过 HTTP

// Java 11 或更高版本。运行方式:java Droid.java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Droid {
    static final HttpClient http = HttpClient.newHttpClient();

    // 在唯一在线的手机上运行一条命令,并以文本形式返回 JSON 回复。
    static String run(String command, String jsonBody) throws Exception {
        HttpRequest req = HttpRequest.newBuilder(URI.create("http://localhost:8780/devices/_/" + command))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();
        HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
        return res.statusCode() + " " + res.body();
    }

    public static void main(String[] args) throws Exception {
        System.out.println(run("launch", "{\"package\":\"dev.droidline.demo\"}"));
        System.out.println(run("input", "{\"by\":\"id\",\"value\":\"email\",\"text\":\"knife\"}"));
        System.out.println(run("touch", "{\"by\":\"text\",\"value\":\"Nope\",\"timeout\":1}"));
    }
}
// .NET 6 或更高版本。运行方式:dotnet run droid.cs(.NET 10),或粘贴到 Program.cs 中
using System.Text;
using System.Text.Json.Nodes;

var http = new HttpClient { BaseAddress = new Uri("http://localhost:8780/") };

// 在唯一在线的手机上运行一条命令,并返回解析后的回复。
async Task<JsonNode> Run(string command, JsonObject body)
{
    var content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
    var res = await http.PostAsync($"devices/_/{command}", content);
    return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
}

Console.WriteLine(await Run("launch", new() { ["package"] = "dev.droidline.demo" }));

var r = await Run("touch", new() { ["by"] = "text", ["value"] = "Nope", ["timeout"] = 1 });
if (!(bool)r["ok"]!)
    Console.WriteLine($"{r["error"]}: {r["msg"]}");

var title = await Run("get_text", new() { ["by"] = "text", ["value"] = "Sign in" });
Console.WriteLine(title["value"]);
<?php
// 在唯一在线的手机上运行一条命令,并返回解码后的回复。
function droid(string $command, array $params = []): array {
    $context = stream_context_create(["http" => [
        "method" => "POST",
        "header" => "Content-Type: application/json",
        "content" => json_encode((object) $params),
        "ignore_errors" => true,   // 保留 4xx 和 5xx 回复的 JSON 正文
    ]]);
    $body = file_get_contents("http://localhost:8780/devices/_/$command", false, $context);
    return json_decode($body, true);
}

print_r(droid("launch", ["package" => "dev.droidline.demo"]));
$r = droid("touch", ["by" => "text", "value" => "Nope", "timeout" => 1]);
if (!$r["ok"]) {
    echo $r["error"], ": ", $r["msg"], "\n";
}
echo droid("current")["package"], "\n";

这些示例的共同点,也就是在其他任何语言中都要照搬的部分:

  1. 向 http://localhost:8780/devices/_/<command> 发送 POST 请求,或者把 _ 换成手机的名称。
  2. 发送请求头 Content-Type: application/json,以及一个包含命令参数的 JSON 对象。
  3. 读取回复中的 ok。如果它为 false,error 中是错误码,msg 中是一条可读的消息;此时 HTTP 状态码是 4xx 或 5xx,所以要确保你的 HTTP 库仍然把响应体交给你。

通过原始套接字

套接字协议是纯文本:每个请求和每个回复都是单独一行的 JSON 对象,以换行符结尾。你为每个请求选定一个 id,回复会把它带回来,因此你可以连续发送多个请求,不必等待:

→ {"id":1,"cmd":"launch","package":"dev.droidline.demo"}
← {"id":1,"ok":true,"ms":612}
→ {"id":2,"cmd":"touch","by":"text","value":"Nope","timeout":1}
← {"id":2,"ok":false,"error":"NOT_FOUND","msg":"Could not find text 'Nope' within 1s. ...","retryable":true}
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:8780")
	if err != nil {
		panic(err) // droidline serve 在运行吗?
	}
	defer conn.Close()
	in := bufio.NewReader(conn)

	// send 写入一行请求,再读取一行回复。
	send := func(req map[string]any) map[string]any {
		line, _ := json.Marshal(req)
		conn.Write(append(line, '\n'))
		reply, err := in.ReadBytes('\n')
		if err != nil {
			panic(err)
		}
		var out map[string]any
		json.Unmarshal(reply, &out)
		return out
	}

	fmt.Println(send(map[string]any{"id": 1, "cmd": "launch", "package": "dev.droidline.demo"}))
	r := send(map[string]any{"id": 2, "cmd": "touch", "by": "text", "value": "Nope", "timeout": 1})
	if r["ok"] != true {
		fmt.Println("failed:", r["error"], r["msg"])
	}
	fmt.Println(send(map[string]any{"id": 3, "cmd": "current"})["package"])
}
# SDK 会替你完成这些;下面是底层实际发生的事。
import json, socket

s = socket.create_connection(("127.0.0.1", 8780))
f = s.makefile("rw", encoding="utf-8")

def send(req):
    f.write(json.dumps(req) + "\n")
    f.flush()
    return json.loads(f.readline())

print(send({"id": 1, "cmd": "launch", "package": "dev.droidline.demo"}))
print(send({"id": 2, "cmd": "touch", "by": "text", "value": "Nope", "timeout": 1}))

上面的示例在每个请求之后读取一个回复,这是最简单的模式。完整规则如下:

  • 请求字段是命令的参数,加上 id、cmd,以及可选的 device(手机的名称或 ID)。
  • 发给同一部手机的命令,回复按顺序返回。不同手机的回复可能以任意顺序到达,所以要按 id 匹配。
  • 发送 {"id":9,"cmd":"subscribe","events":["notification"]} 之后,以 {"event":...} 开头的行会夹在回复之间到达。读取代码应该根据 event 字段区分这两种行。
  • 同一端口上的 GET /ws 通过 WebSocket 传输同样的行,适合那些用 WebSocket 比用 TCP 更方便的语言。

协议说明了每个字段、各种事件,以及会切断网络的命令如何报告最终结果。

如何选择

HTTP原始套接字
工作量最少:任何 HTTP 库都行稍多:要保持连接并逐行读取
速度每条命令一个连接所有命令共用一个连接
同时执行多条命令并行发出多个请求在一个连接上流水线发送
事件(通知、设备离线)不支持支持,需先 subscribe

先从 HTTP 开始。需要接收事件,或者每秒要发送很多命令时,再改用套接字。