diff --git a/app/build.gradle b/app/build.gradle
index 7396058..2c442e7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,34 +1,27 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 29
+ namespace "com.ttstd.remoteclient"
+
+ compileSdkVersion 33
// buildToolsVersion "36.0.0"
defaultConfig {
applicationId "com.ttstd.remoteclient"
minSdkVersion 24
- targetSdkVersion 29
+ targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
ndk {
//根据需要 自行选择添加的对应cpu类型的.so库。
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
// 还可以添加 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
}
- dataBinding {
- enabled true
- }
-
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
@@ -37,6 +30,16 @@ android {
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ buildFeatures {
+ dataBinding true
+ buildConfig true
+ }
+
buildTypes {
release {
minifyEnabled false
@@ -49,6 +52,14 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
+ // 添加 Kotlin 标准库
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ // 添加这行,使用 BOM 统一 Kotlin 相关库的版本
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version"))
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.11.0'
+
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
// For control over item selection of both touch and mouse driven selection
@@ -101,7 +112,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.7.3'
implementation 'org.webrtc:google-webrtc:1.0.32006'
- implementation 'org.java-websocket:Java-WebSocket:1.5.3'
+// implementation 'org.java-websocket:Java-WebSocket:1.5.3'
//MMKV
implementation 'com.tencent:mmkv-static:1.2.14'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index af1bb7c..53a9889 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,6 +26,7 @@
android:theme="@style/AppTheme">
@@ -34,14 +35,6 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java
index 865df3d..a564554 100644
--- a/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java
+++ b/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java
@@ -1,23 +1,64 @@
package com.ttstd.remoteclient.activity.main;
-import android.content.Intent;
-import android.media.projection.MediaProjectionManager;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.os.Build;
+import android.text.Editable;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
import com.ttstd.remoteclient.R;
import com.ttstd.remoteclient.base.mvvm.BaseMvvmActivity;
import com.ttstd.remoteclient.databinding.ActivityMainBinding;
-import com.ttstd.remoteclient.service.ControlService;
-import com.ttstd.remoteclient.service.ScreenCaptureService;
+import com.ttstd.remoteclient.gson.GsonUtils;
+import com.ttstd.remoteclient.manager.SignalingWebSocketClient;
+import com.ttstd.remoteclient.utils.SystemUtils;
-public class MainActivity extends BaseMvvmActivity {
- private static final String TAG ="MainActivity";
+import org.webrtc.DataChannel;
+import org.webrtc.DefaultVideoDecoderFactory;
+import org.webrtc.DefaultVideoEncoderFactory;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RtpReceiver;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+import org.webrtc.VideoTrack;
- private static final int REQUEST_CODE_SCREEN_CAPTURE = 7897;
- private MediaProjectionManager mMediaProjectionManager;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+public class MainActivity extends BaseMvvmActivity implements SignalingWebSocketClient.SignalingListener {
+ private static final String TAG = "MainActivity";
+
+ private static final int NETWORK_REQUEST_CODE = 1001;
+
+ // ICE服务器配置(必须,用于NAT穿透,公共免费ICE服务器)
+ private static final List ICE_SERVERS = new ArrayList() {{
+ add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
+ add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
+ }};
+
+ // WebRTC 核心对象(核心引擎)
+ private PeerConnectionFactory peerConnectionFactory;
+ private PeerConnection peerConnection;
+ private EglBase rootEglBase;
+
+ private SignalingWebSocketClient signalingClient;
+ private String mRemoteUserId;
@Override
public boolean setNightMode() {
@@ -39,8 +80,8 @@ public class MainActivity extends BaseMvvmActivity= Build.VERSION_CODES.O) {
- startForegroundService(screenServiceIntent);
- }else {
- startService(screenServiceIntent);
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == NETWORK_REQUEST_CODE) {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initWebRTC();
+ } else {
+ Toast.makeText(this, "权限被拒绝,无法使用屏幕共享功能", Toast.LENGTH_SHORT).show();
}
-
- // 启动指令接收服务
- Intent controlServiceIntent = new Intent(this, ControlService.class);
- startService(controlServiceIntent);
- } else {
- Log.e(TAG, "屏幕采集权限申请失败");
}
}
+ // 权限申请逻辑
+ private void requestPermissions() {
+ String[] permissions = {Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE};
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (ContextCompat.checkSelfPermission(this, permissions[0]) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, permissions, NETWORK_REQUEST_CODE);
+ } else {
+ initWebRTC();
+ }
+ } else {
+ initWebRTC();
+ }
+ }
+
+ // ======================== 核心:初始化WebRTC引擎 ========================
+ private void initWebRTC() {
+ Log.e(TAG, "initWebRTC: ");
+ // 1. 初始化EGL环境(WebRTC渲染必须)
+ rootEglBase = EglBase.create();
+ // 2. 初始化渲染控件
+ mViewDataBinding.remoteVideoView.init(rootEglBase.getEglBaseContext(), null);
+ mViewDataBinding.remoteVideoView.setMirror(false); // 屏幕共享不需要镜像
+ mViewDataBinding.remoteVideoView.setEnableHardwareScaler(true); // 硬件缩放,提升性能
+
+ // 3. 初始化PeerConnectionFactory配置
+ PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions
+ .builder(this)
+ .setEnableInternalTracer(true)
+ .createInitializationOptions();
+ PeerConnectionFactory.initialize(initOptions);
+
+ // 4. 创建PeerConnectionFactory(WebRTC核心工厂)
+ PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
+ peerConnectionFactory = PeerConnectionFactory.builder()
+ .setOptions(factoryOptions)
+ .setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()))
+ .setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true))
+ .createPeerConnectionFactory();
+
+ // 5. 创建PeerConnection(P2P连接核心对象,信令+数据传输都靠它)
+ createPeerConnection();
+ mViewDataBinding.tvStatus.setText("WebRTC初始化完成,等待信令协商...");
+ }
+
+ // ======================== 创建P2P连接对象 ========================
+ private void createPeerConnection() {
+ Log.e(TAG, "createPeerConnection: ");
+ PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(ICE_SERVERS);
+ peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
+ // 1. ICE候选地址收集回调:收集到本地ICE地址,通过信令发送给远端
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ Log.e(TAG, "onIceCandidate: ");
+ runOnUiThread(() -> mViewDataBinding.tvStatus.setText("收集到ICE候选地址,发送给远端..."));
+ // TODO: 这里将 iceCandidate 转换成JSON,通过你的信令服务器发送给远端设备
+ sendIceCandidateToRemote(iceCandidate);
+ }
+
+ // 2. ICE连接状态变化回调
+ @Override
+ public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
+ Log.e(TAG, "onIceConnectionChange: ");
+ runOnUiThread(() -> {
+ mViewDataBinding.tvStatus.setText("连接状态:" + iceConnectionState.name());
+ if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
+ mViewDataBinding.tvStatus.setText("✅ 已成功连接,正在接收远程屏幕...");
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
+ mViewDataBinding.tvStatus.setText("❌ 连接断开");
+ }
+ });
+ }
+
+ // 3. 收到远端媒体流回调【核心】:收到远程屏幕流,渲染到控件上
+ @Override
+ public void onAddStream(MediaStream mediaStream) {
+ Log.e(TAG, "onAddStream: ");
+ runOnUiThread(() -> mViewDataBinding.tvStatus.setText("✅ 收到远程屏幕流,开始渲染..."));
+ // 获取视频轨道,添加到渲染控件
+ VideoTrack videoTrack = mediaStream.videoTracks.get(0);
+ videoTrack.addSink(mViewDataBinding.remoteVideoView);
+ }
+
+ // 其他生命周期回调,默认实现即可
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ Log.e(TAG, "onSignalingChange: ");
+ }
+
+ @Override
+ public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
+ Log.e(TAG, "onIceGatheringChange: ");
+ }
+
+ @Override
+ public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
+ Log.e(TAG, "onIceCandidatesRemoved: ");
+ }
+
+ @Override
+ public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
+ Log.e(TAG, "onConnectionChange: ");
+ }
+
+ @Override
+ public void onIceConnectionReceivingChange(boolean b) {
+ Log.e(TAG, "onIceConnectionReceivingChange: ");
+ }
+
+ @Override
+ public void onRemoveStream(MediaStream mediaStream) {
+ Log.e(TAG, "onRemoveStream: ");
+ }
+
+ @Override
+ public void onDataChannel(DataChannel dataChannel) {
+ Log.e(TAG, "onDataChannel: ");
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ Log.e(TAG, "onRenegotiationNeeded: ");
+ }
+
+ @Override
+ public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+ Log.e(TAG, "onAddTrack: ");
+ }
+ });
+ }
+
+ // ======================== 核心信令交互方法(对外提供调用) ========================
+
+ /**
+ * 1. 收到远端发送的 SDP Offer 数据(信令协商第一步)
+ *
+ * @param sdpJson 远端发送的Offer-SDP字符串(JSON格式)
+ */
+ public void onReceiveRemoteOffer(String sdpJson) {
+ SessionDescription offerSdp = new SessionDescription(SessionDescription.Type.OFFER, sdpJson);
+ peerConnection.setRemoteDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e("setRemoteDescription", "onCreateSuccess: ");
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e("setRemoteDescription", "onCreateSuccess: ");
+ // 设置远端Offer成功,生成本地Answer并发送给远端
+ peerConnection.createAnswer(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription answerSdp) {
+ peerConnection.setLocalDescription(new SdpObserver() {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e("setLocalDescription", "onCreateSuccess: ");
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e("setLocalDescription", "onSetSuccess: ");
+ // TODO: 将本地Answer-SDP发送给远端
+ sendSdpAnswerToRemote(answerSdp);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("setLocalDescription", "onCreateFailure: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e("setLocalDescription", "onSetFailure: " + s);
+ }
+ }, answerSdp);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("createAnswer", "onCreateFailure: " + s);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e("createAnswer", "onSetSuccess: ");
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e("createAnswer", "onSetFailure: " + s);
+ }
+ }, new MediaConstraints());
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("setRemoteDescription", "onCreateFailure: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e("setRemoteDescription", "onSetFailure: " + s);
+ }
+ }, offerSdp);
+ }
+
+ /**
+ * 2. 收到远端发送的 ICE候选地址
+ *
+ * @param iceCandidateJson 远端的ICE地址字符串(JSON格式)
+ * @param sdpMid ICE的mid标识
+ * @param sdpMLineIndex ICE的索引
+ */
+ public void onReceiveRemoteIceCandidate(String iceCandidateJson, String sdpMid, int sdpMLineIndex) {
+ Log.e(TAG, "onReceiveRemoteIceCandidate: ");
+
+ IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, iceCandidateJson);
+ peerConnection.addIceCandidate(iceCandidate);
+ }
+
+ // ======================== 信令发送的桥接方法(需要你对接自己的信令服务器) ========================
+
+ /**
+ * 发送本地ICE候选地址到远端
+ * TODO: 这里替换成你的信令逻辑(WebSocket/Http长连接/MQTT等)
+ */
+ private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
+ Log.e(TAG, "sendIceCandidateToRemote: ");
+
+ String mid = iceCandidate.sdpMid;
+ int index = iceCandidate.sdpMLineIndex;
+ String sdp = iceCandidate.sdp;
+ // 调用你的信令发送接口,把iceJson、mid、index发给远端
+
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("msg_type", 4);
+
+ // 发送信令消息
+ JsonObject iceCandidateInfo = new JsonObject();
+ iceCandidateInfo.addProperty("sdpMid", mid);
+ iceCandidateInfo.addProperty("sdpMLineIndex", index);
+ iceCandidateInfo.addProperty("sdp", sdp);
+
+ iceCandidateInfo.addProperty("fromUser", SystemUtils.getSerial());
+ iceCandidateInfo.addProperty("toUser", mRemoteUserId);
+ iceCandidateInfo.addProperty("timestamp", System.currentTimeMillis());
+
+ jsonObject.add("content", iceCandidateInfo);
+ }
+
+ /**
+ * 发送本地Answer-SDP到远端
+ * TODO: 这里替换成你的信令逻辑
+ */
+ private void sendSdpAnswerToRemote(SessionDescription description) {
+ Log.e(TAG, "sendIceCandidateToRemote: ");
+
+ // 调用你的信令发送接口,把sdpAnswer发给远端
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("msg_type", 3);
+
+ // 发送信令消息
+ JsonObject signal = new JsonObject();
+ signal.addProperty("type", description.type.canonicalForm()); // 示例:offer信令
+ signal.addProperty("sdp", description.description);
+
+ signal.addProperty("fromUser", SystemUtils.getSerial());
+ signal.addProperty("toUser", mRemoteUserId);
+ signal.addProperty("timestamp", System.currentTimeMillis());
+
+ jsonObject.add("content", signal);
+
+ signalingClient.sendSignal(jsonObject.toString());
+ }
+
+ // ======================== 生命周期管理 + 资源释放(必须,防止内存泄漏) ========================
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 释放渲染控件
+ if (mViewDataBinding.remoteVideoView != null) {
+ mViewDataBinding.remoteVideoView.release();
+ }
+ // 释放P2P连接
+ if (peerConnection != null) {
+ peerConnection.close();
+ peerConnection.dispose();
+ }
+ // 释放工厂
+ if (peerConnectionFactory != null) {
+ peerConnectionFactory.dispose();
+ }
+ // 释放EGL环境
+ if (rootEglBase != null) {
+ rootEglBase.release();
+ }
+
+ if (signalingClient != null) {
+ signalingClient.disconnect();
+ }
+ }
+
+
+ // 实现 SignalingListener 接口方法
+ @Override
+ public void onConnected(String sessionId) {
+ Log.e(TAG, "onConnected: " + sessionId);
+
+ // 调用你的信令发送接口,把sdpAnswer发给远端
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("msg_type", 3);
+
+ // 发送信令消息
+ JsonObject signal = new JsonObject();
+ signal.addProperty("type", "offer"); // 示例:offer信令
+ signal.addProperty("fromUser", SystemUtils.getSerial());
+ signal.addProperty("toUser", mRemoteUserId);
+ signal.addProperty("timestamp", System.currentTimeMillis());
+
+ jsonObject.add("content", signal);
+
+ signalingClient.sendSignal(jsonObject.toString());
+
+ runOnUiThread(() -> {
+ updateStatus("已连接,会话ID: " + sessionId);
+ addMessage("系统: 连接到信令服务器");
+ });
+ }
+
+ @Override
+ public void onMessageReceived(String msgType, String data) {
+ Log.e(TAG, "onMessageReceived: type = " + msgType);
+ Log.e(TAG, "onMessageReceived: data = " + data);
+ JsonObject jsonObject = GsonUtils.getJsonObject(data);
+ int msg_type = jsonObject.get("msg_type").getAsInt();
+ if (msg_type == 11) {
+ String sdpJson = jsonObject.get("sdp").getAsString();
+ onReceiveRemoteOffer(sdpJson);
+ } else if (msg_type == 22) {
+ String iceJsonString = jsonObject.get("ice").getAsString();
+ JsonObject iceJson = GsonUtils.getJsonObject(iceJsonString);
+ String sdpMid = iceJson.get("sdpMid").getAsString();
+ int sdpMLineIndex = iceJson.get("sdpMLineIndex").getAsInt();
+ String sdp = iceJson.get("sdp").getAsString();
+
+ onReceiveRemoteIceCandidate(sdp, sdpMid, sdpMLineIndex);
+ }
+ runOnUiThread(() -> {
+ addMessage("收到[" + msgType + "]: " + data);
+ });
+ }
+
+ @Override
+ public void onDisconnected(String reason) {
+ Log.e(TAG, "onDisconnected: ");
+ runOnUiThread(() -> {
+ updateStatus("已断开: " + reason);
+ addMessage("系统: 连接断开 - " + reason);
+ });
+ }
+
+ @Override
+ public void onError(String error) {
+ Log.e(TAG, "onError: " + error);
+ runOnUiThread(() -> {
+ Log.e(TAG, "WebSocket错误: " + error);
+ addMessage("错误: " + error);
+ });
+ }
+
+ private void updateStatus(String status) {
+ mViewDataBinding.tvStatus.setText("状态: " + status);
+ }
+
+ private void addMessage(String message) {
+ String current = mViewDataBinding.tvStatus.getText().toString();
+ mViewDataBinding.tvStatus.setText(current + "\n" + message);
+ }
+
+
public class BtnClick {
- public void requestPermission(View view){
- // 适配Android 13+ 通知权限
-// if (Build.VERSION.SDK_INT >= 33) {
-// if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
-// ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1002);
-// }
-// }
- // 申请屏幕采集权限
- Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();
- startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
+ public void connectWebRtc(View view) {
+ Editable editable = mViewDataBinding.etNumber.getText();
+ if (TextUtils.isEmpty(editable)) {
+ Log.e(TAG, "connectWebRtc: userId is empty");
+ mViewDataBinding.tvStatus.setText("用户id为空");
+ return;
+ }
+ mRemoteUserId = editable.toString();
+ // 创建信令客户端
+ signalingClient = new SignalingWebSocketClient(MainActivity.this);
+ signalingClient.connect();
+ signalingClient.setUserId(mRemoteUserId);
+ updateStatus("正在连接...");
}
}
diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java
new file mode 100644
index 0000000..d1a9695
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java
@@ -0,0 +1,9 @@
+package com.ttstd.remoteclient.bean;
+
+import java.io.Serializable;
+
+public class SignalMessage implements Serializable {
+
+
+
+}
diff --git a/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java
new file mode 100644
index 0000000..06d0e7d
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java
@@ -0,0 +1,18 @@
+package com.ttstd.remoteclient.config;
+
+/**
+ * WebSocket 配置类
+ * 用于配置WebSocket连接的各种参数
+ */
+public class WebSocketConfig {
+ public String wsUrl; // WebSocket服务器地址
+ public static final long heartbeatInterval = 30000; // 心跳间隔,默认10秒
+ public static final long reconnectInterval = 5000; // 重连间隔,默认5秒
+ public static final int maxReconnectAttempts = 5; // 最大重连次数
+ public static final boolean autoReconnect = true; // 是否自动重连
+ public static final boolean needHeartbeat = true; // 是否需要心跳检测
+ public static final long connectTimeout = 10; // 连接超时时间(秒)
+ public static final long readTimeout = 10; // 读取超时时间(秒)
+ public static final long writeTimeout = 10; // 写入超时时间(秒)
+
+}
diff --git a/app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java b/app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java
new file mode 100644
index 0000000..bcb2aa9
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java
@@ -0,0 +1,153 @@
+package com.ttstd.remoteclient.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.reflect.TypeToken;
+
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+
+public class GsonUtils {
+ //https://blog.csdn.net/zte1055889498/article/details/122400299
+
+ public static JsonObject getJsonObject(String jsonString) {
+ JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject();
+ return jsonObject;
+ }
+
+ private static final Gson gson;
+ private static final Gson exposeGson;
+
+ static {
+ GsonBuilder builder = new GsonBuilder();
+ builder.registerTypeAdapterFactory(new NullStringToEmptyAdapterFactory());
+ builder.registerTypeAdapter(Integer.class, new IntegerDefault0Adapter());
+ builder.registerTypeAdapter(int.class, new IntegerDefault0Adapter());
+ builder.disableHtmlEscaping();
+ builder.enableComplexMapKeySerialization();
+ // builder.excludeFieldsWithoutExposeAnnotation();
+ builder.setDateFormat("yyyy-MM-dd HH:mm:ss");
+ gson = builder.create();
+ exposeGson = new GsonBuilder()
+ .excludeFieldsWithoutExposeAnnotation()
+ .create();
+ }
+
+ public static Type makeJavaType(Type rawType, Type... typeArguments) {
+ return TypeToken.getParameterized(rawType, typeArguments).getType();
+ }
+
+ public static String toString(Object value) {
+ if (Objects.isNull(value)) {
+ return null;
+ }
+ if (value instanceof String) {
+ return (String) value;
+ }
+ return toJSONString(value);
+ }
+
+ public static String toJSONString(Object value) {
+ return gson.toJson(value);
+ }
+
+ public static String toExposeJSONString(Object value) {
+ return gson.toJson(value);
+ }
+
+
+ public static String toPrettyString(Object value) {
+ return gson.newBuilder().setPrettyPrinting().create().toJson(value);
+ }
+
+ public static JsonElement fromJavaObject(Object value) {
+ JsonElement result = null;
+ if (Objects.nonNull(value) && (value instanceof String)) {
+ result = parseObject((String) value);
+ } else {
+ result = gson.toJsonTree(value);
+ }
+ return result;
+ }
+
+ public static JsonElement parseObject(String content) {
+ return JsonParser.parseString(content);
+ }
+
+ public static JsonElement getJsonElement(JsonObject node, String name) {
+ return node.get(name);
+ }
+
+ public static JsonElement getJsonElement(JsonArray node, int index) {
+ return node.get(index);
+ }
+
+ public static T toJavaObject(JsonElement node, Class clazz) {
+ return gson.fromJson(node, clazz);
+ }
+
+ public static T toJavaObject(JsonElement node, Type type) {
+ return gson.fromJson(node, type);
+ }
+
+ public static T toJavaObject(JsonElement node, TypeToken> typeToken) {
+ return toJavaObject(node, typeToken.getType());
+ }
+
+ public static List toJavaList(JsonElement node, Class clazz) {
+ return toJavaObject(node, makeJavaType(List.class, clazz));
+ }
+
+ public static List