feat: phone ServerClient (OkHttp WebSocket) + MainActivity wiring

This commit is contained in:
2026-09-01 18:51:11 +08:00
parent 0673ff77b7
commit 41a9f81c0e
4 changed files with 165 additions and 26 deletions
+1
View File
@@ -26,4 +26,5 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0' implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
} }
+2
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:label="@string/app_name" android:label="@string/app_name"
@@ -8,15 +8,56 @@ import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONObject;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private TextView status;
private ServerClient client;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
TextView status = findViewById(R.id.status_text); status = findViewById(R.id.status_text);
status.setText(getString(R.string.status_disconnected));
client = new ServerClient(new ServerClient.Listener() {
@Override
public void onConnectionChanged(boolean connected) {
runOnUiThread(() -> 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); Button settings = findViewById(R.id.btn_settings);
settings.setOnClickListener(new View.OnClickListener() { settings.setOnClickListener(new View.OnClickListener() {
@@ -30,9 +71,16 @@ public class MainActivity extends AppCompatActivity {
refresh.setOnClickListener(new View.OnClickListener() { refresh.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// 空壳:后续调用 ServerClient 拉取 Harness 状态 status.setText("连接中...");
status.setText(R.string.status_disconnected); // 空壳:默认连本机模拟器 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();
}
} }
@@ -1,31 +1,119 @@
package com.dshwatch.app; 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 服务器客户端(空壳)。 * Harness 服务器客户端(WebSocket)。
* 后续接入 WebSocket(OkHttp ws),通过 DSHWatch_Server 或直连 dsh-watch 插件, * 对接 DSHWatch_Server 中继服务器,或直连 dsh-watch 插件(direct 模式)。
* 实现 harness.status / session.prompt / session.list 等 RPC。 * 请求/响应/事件均走 JSON 帧,见 DSHWatch_Server/docs/PROTOCOL.md。
*/ */
public class ServerClient { public class ServerClient {
private String serverUrl;
private String userId;
private String token;
public ServerClient(String serverUrl, String userId, String token) { public interface Listener {
this.serverUrl = serverUrl; void onConnectionChanged(boolean connected);
this.userId = userId; void onHello(JSONObject hello);
this.token = token; void onStatus(JSONObject status);
} void onEvent(JSONObject event);
void onResponse(String requestId, boolean ok, JSONObject result, JSONObject error);
public void connect() {
// 空壳:TODO 实现 WebSocket 握手 + 请求/响应 + 事件回调
}
public void sendRequest(String method, String paramsJson, Callback cb) {
// 空壳:TODO
}
public interface Callback {
void onResult(String json);
void onError(String message); 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;
}
}
} }