From 41a9f81c0e102ad173bff104b03dfdbddc7fbd1f Mon Sep 17 00:00:00 2001 From: jiangchen <1703935164@qq.com> Date: Tue, 1 Sep 2026 18:51:11 +0800 Subject: [PATCH] feat: phone ServerClient (OkHttp WebSocket) + MainActivity wiring --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 + .../java/com/dshwatch/app/MainActivity.java | 56 +++++++- .../java/com/dshwatch/app/ServerClient.java | 132 +++++++++++++++--- 4 files changed, 165 insertions(+), 26 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 313b7e4..7f92a92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,4 +26,5 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 22e969d..3aef3e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ + + status.setText(connected ? "已连接(空壳)" : "连接断开")); + } + + @Override + public void onHello(JSONObject hello) { + // 连接建立后拉一次状态 + client.sendRequest("harness.status", new JSONObject()); + } + + @Override + public void onStatus(JSONObject s) { + runOnUiThread(() -> { + if (s != null) { + status.setText("状态: " + s.optString("model") + " / " + s.optString("cwd")); + } + }); + } + + @Override + public void onEvent(JSONObject event) { + // 空壳:后续写入事件流 + } + + @Override + public void onResponse(String requestId, boolean ok, JSONObject result, JSONObject error) { + runOnUiThread(() -> status.setText(ok ? "响应 OK" : "错误: " + (error != null ? error.optString("message") : ""))); + } + + @Override + public void onError(String message) { + runOnUiThread(() -> status.setText("错误: " + message)); + } + }); Button settings = findViewById(R.id.btn_settings); settings.setOnClickListener(new View.OnClickListener() { @@ -30,9 +71,16 @@ public class MainActivity extends AppCompatActivity { refresh.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - // 空壳:后续调用 ServerClient 拉取 Harness 状态 - status.setText(R.string.status_disconnected); + status.setText("连接中..."); + // 空壳:默认连本机模拟器 server;真实地址/用户/Token 从设置读取 + client.connect("ws://10.0.2.2:8787/ws", "dshwatch", "change-me"); } }); } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (client != null) client.close(); + } } diff --git a/app/src/main/java/com/dshwatch/app/ServerClient.java b/app/src/main/java/com/dshwatch/app/ServerClient.java index f6fd63e..91102f8 100644 --- a/app/src/main/java/com/dshwatch/app/ServerClient.java +++ b/app/src/main/java/com/dshwatch/app/ServerClient.java @@ -1,31 +1,119 @@ package com.dshwatch.app; +import org.json.JSONObject; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + /** - * Harness 服务器客户端(空壳)。 - * 后续接入 WebSocket(OkHttp ws),通过 DSHWatch_Server 或直连 dsh-watch 插件, - * 实现 harness.status / session.prompt / session.list 等 RPC。 + * Harness 服务器客户端(WebSocket)。 + * 对接 DSHWatch_Server 中继服务器,或直连 dsh-watch 插件(direct 模式)。 + * 请求/响应/事件均走 JSON 帧,见 DSHWatch_Server/docs/PROTOCOL.md。 */ public class ServerClient { - private String serverUrl; - private String userId; - private String token; - public ServerClient(String serverUrl, String userId, String token) { - this.serverUrl = serverUrl; - this.userId = userId; - this.token = token; - } - - public void connect() { - // 空壳:TODO 实现 WebSocket 握手 + 请求/响应 + 事件回调 - } - - public void sendRequest(String method, String paramsJson, Callback cb) { - // 空壳:TODO - } - - public interface Callback { - void onResult(String json); + public interface Listener { + void onConnectionChanged(boolean connected); + void onHello(JSONObject hello); + void onStatus(JSONObject status); + void onEvent(JSONObject event); + void onResponse(String requestId, boolean ok, JSONObject result, JSONObject error); void onError(String message); } + + private final OkHttpClient client; + private final Listener listener; + private WebSocket webSocket; + + public ServerClient(Listener listener) { + this.listener = listener; + this.client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + } + + public void connect(String serverUrl, String userId, String token) { + Request request = new Request.Builder() + .url(serverUrl) + .build(); + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket ws, Response response) { + listener.onConnectionChanged(true); + JSONObject hello = new JSONObject(); + try { + hello.put("type", "hello"); + hello.put("role", "client"); + hello.put("userId", userId); + hello.put("token", token); + ws.send(hello.toString()); + } catch (Exception e) { + listener.onError("hello error: " + e.getMessage()); + } + } + + @Override + public void onMessage(WebSocket ws, String text) { + try { + JSONObject msg = new JSONObject(text); + String type = msg.optString("type", ""); + if ("hello".equals(type)) { + listener.onHello(msg); + } else if ("status".equals(type)) { + listener.onStatus(msg.optJSONObject("status")); + } else if ("event".equals(type)) { + listener.onEvent(msg.optJSONObject("event")); + } else if ("response".equals(type)) { + String requestId = msg.optString("requestId"); + boolean ok = msg.optBoolean("ok", false); + JSONObject result = msg.optJSONObject("result"); + JSONObject error = msg.optJSONObject("error"); + listener.onResponse(requestId, ok, result, error); + } + } catch (Exception e) { + listener.onError("bad message: " + e.getMessage()); + } + } + + @Override + public void onClosed(WebSocket ws, int code, String reason) { + listener.onConnectionChanged(false); + } + + @Override + public void onFailure(WebSocket ws, Throwable t, Response response) { + listener.onConnectionChanged(false); + listener.onError(t != null ? t.getMessage() : "websocket failure"); + } + }); + } + + public void sendRequest(String method, JSONObject params) { + if (webSocket == null) return; + JSONObject msg = new JSONObject(); + try { + msg.put("type", "request"); + msg.put("requestId", UUID.randomUUID().toString()); + msg.put("method", method); + msg.put("params", params != null ? params : new JSONObject()); + webSocket.send(msg.toString()); + } catch (Exception e) { + listener.onError("sendRequest error: " + e.getMessage()); + } + } + + public void close() { + if (webSocket != null) { + webSocket.close(1000, "bye"); + webSocket = null; + } + } }