feat: phone client as WebView shell of Harness web app + settings (webUrl / watch link / Hermes); menu

This commit is contained in:
2026-09-02 01:13:51 +08:00
parent d1d1ebf18d
commit 8e660162a6
6 changed files with 265 additions and 115 deletions
@@ -1,12 +1,19 @@
package com.dshwatch.app; package com.dshwatch.app;
import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -14,24 +21,45 @@ import org.json.JSONObject;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private TextView status; private TextView status;
private TextView balanceView; private WebView webView;
private View emptyHint;
private ServerClient serverClient; private ServerClient serverClient;
private WatchBridge watchBridge; private WatchBridge watchBridge;
private boolean balancePending = false; private boolean balancePending = false;
@SuppressLint("SetJavaScriptEnabled")
@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);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
status = findViewById(R.id.status_text); status = findViewById(R.id.status_text);
balanceView = findViewById(R.id.balance_text); webView = findViewById(R.id.webview);
emptyHint = findViewById(R.id.empty_hint);
// 服务器客户端 // WebView:加载 Harness 网页壳(框子常驻,数据由插件/网关填入)
WebSettings ws = webView.getSettings();
ws.setJavaScriptEnabled(true);
ws.setDomStorageEnabled(true);
ws.setCacheMode(WebSettings.LOAD_DEFAULT);
ws.setAllowFileAccess(true);
ws.setMediaPlaybackRequiresUserGesture(false);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
emptyHint.setVisibility(View.GONE);
}
});
loadWeb();
// 服务器客户端:连 Harness 插件(把 Harness 数据串过来的通道)
serverClient = new ServerClient(new ServerClient.Listener() { serverClient = new ServerClient(new ServerClient.Listener() {
@Override @Override
public void onConnectionChanged(boolean connected) { public void onConnectionChanged(boolean connected) {
runOnUiThread(() -> status.setText(connected ? "服务器已连接" : "服务器断开")); runOnUiThread(() -> status.setText(connected ? "已连接 Harness" : "未连接 Harness"));
} }
@Override @Override
@@ -73,10 +101,7 @@ public class MainActivity extends AppCompatActivity {
double total = (infos != null && infos.length() > 0 && infos.optJSONObject(0) != null) double total = (infos != null && infos.length() > 0 && infos.optJSONObject(0) != null)
? infos.optJSONObject(0).optDouble("total_balance", 0.0) : 0.0; ? infos.optJSONObject(0).optDouble("total_balance", 0.0) : 0.0;
String balance = String.format("%.2f", total); String balance = String.format("%.2f", total);
runOnUiThread(() -> { runOnUiThread(() -> status.setText("DeepSeek 余额 ¥" + balance));
status.setText("余额 ¥ " + balance);
balanceView.setText("DeepSeek 余额 ¥" + balance);
});
JSONObject payload = new JSONObject(); JSONObject payload = new JSONObject();
try { payload.put("balance", balance); } catch (Exception e) {} try { payload.put("balance", balance); } catch (Exception e) {}
JSONObject msg = new JSONObject(); JSONObject msg = new JSONObject();
@@ -93,7 +118,7 @@ public class MainActivity extends AppCompatActivity {
} }
}); });
// 手表互联桥 // 手表互联桥(BLE 网关,后续完善“手机连接手表”)
watchBridge = new WatchBridge(this, new WatchBridge.Listener() { watchBridge = new WatchBridge(this, new WatchBridge.Listener() {
@Override @Override
public void onWatchConnected(boolean connected, String nodeId) { public void onWatchConnected(boolean connected, String nodeId) {
@@ -134,51 +159,66 @@ public class MainActivity extends AppCompatActivity {
runOnUiThread(() -> status.setText("WatchBridge: " + message)); runOnUiThread(() -> status.setText("WatchBridge: " + message));
} }
}); });
Button settings = findViewById(R.id.btn_settings);
settings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
} }
});
Button sessions = findViewById(R.id.btn_sessions); private void loadWeb() {
sessions.setOnClickListener(new View.OnClickListener() { String url = SettingsStore.webUrl(this);
@Override if (url == null || url.trim().isEmpty()) {
public void onClick(View v) { emptyHint.setVisibility(View.VISIBLE);
startActivity(new Intent(MainActivity.this, SessionListActivity.class)); return;
} }
}); emptyHint.setVisibility(View.GONE);
webView.loadUrl(url.trim());
Button events = findViewById(R.id.btn_events);
events.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, EventLogActivity.class));
} }
});
Button command = findViewById(R.id.btn_command);
command.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public boolean onCreateOptionsMenu(Menu menu) {
startActivity(new Intent(MainActivity.this, CommandActivity.class)); getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
} }
});
Button refresh = findViewById(R.id.btn_refresh);
refresh.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_refresh) {
status.setText("连接中..."); status.setText("连接中...");
String serverUrl = SettingsStore.serverUrl(MainActivity.this); String serverUrl = SettingsStore.serverUrl(this);
String userId = SettingsStore.userId(MainActivity.this); String userId = SettingsStore.userId(this);
String token = SettingsStore.token(MainActivity.this); String token = SettingsStore.token(this);
serverClient.connect(serverUrl, userId, token); serverClient.connect(serverUrl, userId, token);
watchBridge.connect(); watchBridge.connect();
loadWeb();
return true;
} else if (id == R.id.action_watch) {
watchBridge.connect();
Toast.makeText(this, "正在连接手表…", Toast.LENGTH_SHORT).show();
return true;
} else if (id == R.id.action_hermes) {
Toast.makeText(this, "Hermès 链接(待接入)", Toast.LENGTH_SHORT).show();
return true;
} else if (id == R.id.action_sessions) {
startActivity(new Intent(this, SessionListActivity.class));
return true;
} else if (id == R.id.action_events) {
startActivity(new Intent(this, EventLogActivity.class));
return true;
} else if (id == R.id.action_command) {
startActivity(new Intent(this, CommandActivity.class));
return true;
} else if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
} }
});
} }
@Override @Override
@@ -4,6 +4,7 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@@ -13,6 +14,10 @@ public class SettingsActivity extends AppCompatActivity {
private EditText serverUrl; private EditText serverUrl;
private EditText userId; private EditText userId;
private EditText token; private EditText token;
private EditText web;
private TextView watchStatus;
private TextView hermesStatus;
private WatchBridge watchBridge;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -22,6 +27,14 @@ public class SettingsActivity extends AppCompatActivity {
serverUrl = findViewById(R.id.input_server); serverUrl = findViewById(R.id.input_server);
userId = findViewById(R.id.input_user); userId = findViewById(R.id.input_user);
token = findViewById(R.id.input_token); token = findViewById(R.id.input_token);
web = findViewById(R.id.input_web);
watchStatus = findViewById(R.id.watch_status);
hermesStatus = findViewById(R.id.hermes_status);
serverUrl.setText(SettingsStore.serverUrl(this));
userId.setText(SettingsStore.userId(this));
token.setText(SettingsStore.token(this));
web.setText(SettingsStore.webUrl(this));
Button save = findViewById(R.id.btn_save); Button save = findViewById(R.id.btn_save);
save.setOnClickListener(new View.OnClickListener() { save.setOnClickListener(new View.OnClickListener() {
@@ -32,8 +45,44 @@ public class SettingsActivity extends AppCompatActivity {
serverUrl.getText().toString().trim(), serverUrl.getText().toString().trim(),
userId.getText().toString().trim(), userId.getText().toString().trim(),
token.getText().toString().trim()); token.getText().toString().trim());
SettingsStore.saveWebUrl(SettingsActivity.this, web.getText().toString().trim());
Toast.makeText(SettingsActivity.this, "已保存", Toast.LENGTH_SHORT).show(); Toast.makeText(SettingsActivity.this, "已保存", Toast.LENGTH_SHORT).show();
} }
}); });
Button watchConnect = findViewById(R.id.btn_watch_connect);
watchConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
watchStatus.setText("正在连接手表…");
watchBridge = new WatchBridge(SettingsActivity.this, new WatchBridge.Listener() {
@Override
public void onWatchConnected(boolean connected, String nodeId) {
runOnUiThread(() -> watchStatus.setText(connected ? "已连接: " + nodeId : "未找到手表"));
}
@Override
public void onWatchMessage(String json) { }
@Override
public void onError(String message) {
runOnUiThread(() -> watchStatus.setText("错误: " + message));
}
});
watchBridge.connect();
}
});
Button hermes = findViewById(R.id.btn_hermes);
hermes.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
hermesStatus.setText("Hermès 链接(待接入)");
Toast.makeText(SettingsActivity.this, "Hermès 链接待接入", Toast.LENGTH_SHORT).show();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
} }
} }
@@ -8,6 +8,7 @@ public class SettingsStore {
private static final String KEY_SERVER = "serverUrl"; private static final String KEY_SERVER = "serverUrl";
private static final String KEY_USER = "userId"; private static final String KEY_USER = "userId";
private static final String KEY_TOKEN = "token"; private static final String KEY_TOKEN = "token";
private static final String KEY_WEB = "webUrl";
public static void save(Context c, String serverUrl, String userId, String token) { public static void save(Context c, String serverUrl, String userId, String token) {
SharedPreferences p = c.getSharedPreferences(PREFS, Context.MODE_PRIVATE); SharedPreferences p = c.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
@@ -29,4 +30,12 @@ public class SettingsStore {
public static String token(Context c) { public static String token(Context c) {
return c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_TOKEN, "change-me"); return c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_TOKEN, "change-me");
} }
public static String webUrl(Context c) {
return c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_WEB, "http://10.0.2.2:43120");
}
public static void saveWebUrl(Context c, String url) {
c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY_WEB, url == null ? "" : url).apply();
}
} }
+37 -65
View File
@@ -1,79 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:padding="24dp"
android:background="@color/deep_blue"> android:background="@color/deep_blue">
<TextView <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/title" android:id="@+id/toolbar"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?attr/actionBarSize"
android:text="@string/app_name" android:background="@android:color/transparent"
android:textColor="@color/white" app:title="DeepSeek Harness"
android:textSize="24sp" app:titleTextColor="@color/white"
android:textStyle="bold" /> app:titleTextAppearance="@style/TextAppearance.MaterialComponents.Headline6">
<TextView <TextView
android:id="@+id/status_text" android:id="@+id/status_text"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_gravity="end"
android:text="@string/status_disconnected" android:paddingEnd="16dp"
android:textColor="@color/white" android:text="未连接"
android:textSize="16sp" />
<TextView
android:id="@+id/balance_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="DeepSeek 余额 ¥--"
android:textColor="#33B4FF"
android:textSize="28sp"
android:textStyle="bold" />
<Button
android:id="@+id/btn_refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/btn_refresh" />
<Button
android:id="@+id/btn_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/btn_settings" />
<Button
android:id="@+id/btn_sessions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/btn_sessions" />
<Button
android:id="@+id/btn_events"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/btn_events" />
<Button
android:id="@+id/btn_command"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/btn_command" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/shell_hint"
android:textColor="@color/light_blue" android:textColor="@color/light_blue"
android:textSize="13sp" /> android:textSize="13sp" />
</com.google.android.material.appbar.MaterialToolbar>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/empty_hint"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="24dp"
android:text="请在设置中填写 Harness 网页地址\n(例如 http://10.0.2.2:43120)\n\n点右上角设置可配置"
android:textColor="@color/light_blue"
android:textSize="15sp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout> </LinearLayout>
@@ -60,6 +60,75 @@
android:textColor="@color/white" android:textColor="@color/white"
android:textColorHint="@color/light_blue" /> android:textColorHint="@color/light_blue" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Harness 网页地址(串流壳,如 http://10.0.2.2:43120)"
android:textColor="@color/light_blue" />
<EditText
android:id="@+id/input_web"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="http://10.0.2.2:43120"
android:textColor="@color/white"
android:textColorHint="@color/light_blue" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="链接"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="⌚ 手表链接"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/watch_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未连接"
android:textColor="@color/light_blue"
android:textSize="13sp" />
<Button
android:id="@+id/btn_watch_connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="连接手表" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="🔗 Hermès 链接"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/hermes_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未配置"
android:textColor="@color/light_blue"
android:textSize="13sp" />
<Button
android:id="@+id/btn_hermes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="连接 Hermès" />
<Button <Button
android:id="@+id/btn_save" android:id="@+id/btn_save"
android:layout_width="match_parent" android:layout_width="match_parent"
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_refresh" android:title="刷新" app:showAsAction="never" />
<item android:id="@+id/action_watch" android:title="手表链接" app:showAsAction="never" />
<item android:id="@+id/action_hermes" android:title="Hermès 链接" app:showAsAction="never" />
<item android:id="@+id/action_sessions" android:title="会话列表" app:showAsAction="never" />
<item android:id="@+id/action_events" android:title="事件流" app:showAsAction="never" />
<item android:id="@+id/action_command" android:title="命令" app:showAsAction="never" />
<item android:id="@+id/action_settings" android:title="设置" app:showAsAction="always" />
</menu>