diff --git a/app/build.gradle b/app/build.gradle index 2c442e7..d426419 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,13 +3,13 @@ apply plugin: 'com.android.application' android { namespace "com.ttstd.remoteclient" - compileSdkVersion 33 + compileSdkVersion 36 // buildToolsVersion "36.0.0" defaultConfig { applicationId "com.ttstd.remoteclient" minSdkVersion 24 - targetSdkVersion 33 + targetSdkVersion 36 versionCode 1 versionName "1.0" @@ -50,6 +50,7 @@ android { } dependencies { + implementation 'androidx.activity:activity-ktx:1.13.0' implementation fileTree(dir: 'libs', include: ['*.jar']) // 添加 Kotlin 标准库 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53a9889..6079280 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,16 +1,14 @@ - - - - - + + - - - + + + + + + android:exported="true" + android:theme="@style/AppThemeFitsSystem"> - \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/remoteclient/activity/display/DisplayActivity.java b/app/src/main/java/com/ttstd/remoteclient/activity/display/DisplayActivity.java new file mode 100644 index 0000000..8d928d4 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/activity/display/DisplayActivity.java @@ -0,0 +1,26 @@ +package com.ttstd.remoteclient.activity.display; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.ttstd.remoteclient.R; + +public class DisplayActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_display); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } +} \ 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 a564554..9e969e8 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 @@ -15,13 +15,13 @@ 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.bean.SignalingMessage; +import com.ttstd.remoteclient.bean.WebRTCMessage; import com.ttstd.remoteclient.databinding.ActivityMainBinding; -import com.ttstd.remoteclient.gson.GsonUtils; -import com.ttstd.remoteclient.manager.SignalingWebSocketClient; -import com.ttstd.remoteclient.utils.SystemUtils; +import com.ttstd.remoteclient.manager.WebSocketCallback; +import com.ttstd.remoteclient.manager.WebSocketManager; import org.webrtc.DataChannel; import org.webrtc.DefaultVideoDecoderFactory; @@ -37,28 +37,36 @@ import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; import org.webrtc.VideoTrack; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -public class MainActivity extends BaseMvvmActivity implements SignalingWebSocketClient.SignalingListener { +import okio.ByteString; + +public class MainActivity extends BaseMvvmActivity implements WebSocketCallback { private static final String TAG = "MainActivity"; + private WebSocketManager mWebSocketManager; + private static final int NETWORK_REQUEST_CODE = 1001; - // ICE服务器配置(必须,用于NAT穿透,公共免费ICE服务器) + // ICE服务器配置(用于NAT穿透) private static final List ICE_SERVERS = new ArrayList() {{ add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()); + add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()); + add(PeerConnection.IceServer.builder("stun:stun2.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; + private boolean mIsWebRTCReady = false; + + private final List mPendingIceCandidates = new ArrayList<>(); + + private final Gson mGson = new Gson(); @Override public boolean setNightMode() { @@ -80,32 +88,44 @@ public class MainActivity extends BaseMvvmActivity 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initWebRTC(); } else { - Toast.makeText(this, "权限被拒绝,无法使用屏幕共享功能", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, "权限被拒绝,无法使用相关功能", Toast.LENGTH_SHORT).show(); } } } - // 权限申请逻辑 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) { + boolean allGranted = true; + for (String p : permissions) { + if (ContextCompat.checkSelfPermission(this, p) != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + if (!allGranted) { ActivityCompat.requestPermissions(this, permissions, NETWORK_REQUEST_CODE); } else { initWebRTC(); @@ -115,388 +135,466 @@ public class MainActivity extends BaseMvvmActivity 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(); + Log.e(TAG, "initWebRTC"); + if (rootEglBase == null) { + rootEglBase = EglBase.create(); } - // 释放P2P连接 + + mViewDataBinding.remoteVideoView.init(rootEglBase.getEglBaseContext(), null); + mViewDataBinding.remoteVideoView.setMirror(false); + mViewDataBinding.remoteVideoView.setEnableHardwareScaler(true); + + if (peerConnectionFactory == null) { + PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions + .builder(this) + .setEnableInternalTracer(true) + .createInitializationOptions(); + PeerConnectionFactory.initialize(initOptions); + + PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options(); + peerConnectionFactory = PeerConnectionFactory.builder() + .setOptions(factoryOptions) + .setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext())) + .setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true)) + .createPeerConnectionFactory(); + } + + mIsWebRTCReady = true; + mViewDataBinding.tvStatus.setText("WebRTC引擎就绪"); + checkAndPerformConnect(); + } + + private void createPeerConnection() { + Log.e(TAG, "createPeerConnection"); if (peerConnection != null) { peerConnection.close(); peerConnection.dispose(); } - // 释放工厂 - if (peerConnectionFactory != null) { - peerConnectionFactory.dispose(); - } - // 释放EGL环境 - if (rootEglBase != null) { - rootEglBase.release(); - } - if (signalingClient != null) { - signalingClient.disconnect(); - } + PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(ICE_SERVERS); + peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() { + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + Log.e(TAG, "onIceCandidate"); + sendIceCandidateToRemote(iceCandidate); + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + Log.e(TAG, "onIceConnectionChange: " + iceConnectionState); + runOnUiThread(() -> { + updateStatus("连接状态: " + iceConnectionState.name()); + if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) { + Log.e(TAG, "onIceConnectionChange: ✅ 已连接,正在接收远程屏幕" ); + updateStatus("✅ 已连接,正在接收远程屏幕..."); + } else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { + updateStatus("❌ 连接已断开"); + } else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) { + updateStatus("❌ 连接失败"); + } + }); + } + + @Override + public void onAddStream(MediaStream mediaStream) { + Log.e(TAG, "onAddStream"); + runOnUiThread(() -> { + Log.e(TAG, "onAddStream: ✅ 收到远程媒体流" ); + updateStatus("✅ 收到远程媒体流"); + if (!mediaStream.videoTracks.isEmpty()) { + VideoTrack videoTrack = mediaStream.videoTracks.get(0); + videoTrack.addSink(mViewDataBinding.remoteVideoView); + } + }); + } + + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.e(TAG, "onSignalingChange: " + signalingState); + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + Log.e(TAG, "onIceGatheringChange: " + iceGatheringState); + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + } + + @Override + public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + Log.e(TAG, "onConnectionChange: " + newState); + } + + @Override + public void onIceConnectionReceivingChange(boolean b) { + } + + @Override + public void onRemoveStream(MediaStream mediaStream) { + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + } + + @Override + public void onRenegotiationNeeded() { + Log.e(TAG, "onRenegotiationNeeded"); + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + } + }); + } + + // ======================== 信令交互 ======================== + + private void sendSignalingMessage(String type, SignalingMessage message) { + if (mWebSocketManager == null || !mWebSocketManager.isConnected()) return; + + // 按照服务器要求的包装格式发送 + JsonObject wrapper = new JsonObject(); + wrapper.addProperty("type", type); + wrapper.addProperty("target", mRemoteUserId); + wrapper.add("data", mGson.toJsonTree(message)); + + mWebSocketManager.sendSignal(wrapper.toString()); + } + + private void sendInitToRemote() { + SignalingMessage message = new SignalingMessage("init", mWebSocketManager.getLocalUserId(), mRemoteUserId, "Ready to connect"); + sendSignalingMessage("init", message); + } + + private void sendAskToRemote() { + SignalingMessage message = new SignalingMessage("ask", mWebSocketManager.getLocalUserId(), mRemoteUserId, "ask to " + mRemoteUserId); + sendSignalingMessage("ask", message); + } + + private void sendOfferToRemote() { + SignalingMessage message = new SignalingMessage("offer", mWebSocketManager.getLocalUserId(), mRemoteUserId, "offer to " + mRemoteUserId); + sendSignalingMessage("offer", message); } - // 实现 SignalingListener 接口方法 + private void sendIceCandidateToRemote(IceCandidate iceCandidate) { + if (mWebSocketManager != null && mWebSocketManager.isConnected()) { + SignalingMessage message = new SignalingMessage("iceCandidate", mWebSocketManager.getLocalUserId(), mRemoteUserId, iceCandidate); + sendSignalingMessage("iceCandidate", message); + } else { + Log.e(TAG, "Signaling not connected, queueing ice candidate"); + mPendingIceCandidates.add(iceCandidate); + } + } + + private void sendIceCandidateToRemote() { + if (peerConnection == null) { + createPeerConnection(); + } + SignalingMessage message = new SignalingMessage("iceCandidate", mWebSocketManager.getLocalUserId(), mRemoteUserId, "offer to remote"); + sendSignalingMessage("iceCandidate", message); + } + + private void sendSdpAnswerToRemote(SessionDescription description) { + SignalingMessage message = new SignalingMessage("answer", mWebSocketManager.getLocalUserId(), mRemoteUserId, description); + sendSignalingMessage("answer", message); + } + + public void onReceiveRemoteOffer(String jsonString) { + + Log.e(TAG, "onReceiveRemoteOffer"); + if (peerConnection == null) { + createPeerConnection(); + } + + SignalingMessage message = mGson.fromJson(jsonString, SignalingMessage.class); + SessionDescription offerSdp = mGson.fromJson(mGson.toJson(message.getPayload()), SessionDescription.class); + + peerConnection.setRemoteDescription(new SimpleSdpObserver("setRemoteDescription") { + @Override + public void onSetSuccess() { + super.onSetSuccess(); + peerConnection.createAnswer(new SimpleSdpObserver("createAnswer") { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.e(TAG, "onReceiveRemoteOffer onCreateSuccess: " ); + super.onCreateSuccess(sessionDescription); + peerConnection.setLocalDescription(new SimpleSdpObserver("setLocalDescription") { + @Override + public void onSetSuccess() { + super.onSetSuccess(); + sendSdpAnswerToRemote(sessionDescription); + } + }, sessionDescription); + } + }, new MediaConstraints()); + } + }, offerSdp); + } + + public void onReceiveRemoteAnswer(String sdp) { + Log.e(TAG, "onReceiveRemoteAnswer"); + if (peerConnection != null) { + SessionDescription answerSdp = new SessionDescription(SessionDescription.Type.ANSWER, sdp); + peerConnection.setRemoteDescription(new SimpleSdpObserver("setRemoteAnswer"), answerSdp); + } + } + + public void onReceiveRemoteIceCandidate(String sdp, String sdpMid, int sdpMLineIndex) { + Log.e(TAG, "onReceiveRemoteIceCandidate"); + if (peerConnection != null) { + IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp); + peerConnection.addIceCandidate(iceCandidate); + } + } + + // ======================== 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()); - + Log.e(TAG, "Signaling connected: " + sessionId); runOnUiThread(() -> { - updateStatus("已连接,会话ID: " + sessionId); - addMessage("系统: 连接到信令服务器"); + updateWsStatus("已连接到信令服务器"); + mViewDataBinding.btConnect.setEnabled(false); + + // 发送暂存的 ICE Candidate + if (!mPendingIceCandidates.isEmpty()) { + Log.e(TAG, "Sending " + mPendingIceCandidates.size() + " pending ice candidates"); + for (IceCandidate candidate : mPendingIceCandidates) { + sendIceCandidateToRemote(candidate); + } + mPendingIceCandidates.clear(); + } + + checkAndPerformConnect(); }); } - @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); + private synchronized void checkAndPerformConnect() { + if (mIsWebRTCReady && mWebSocketManager != null && mWebSocketManager.isConnected() && !TextUtils.isEmpty(mRemoteUserId)) { + Log.e(TAG, "Both WebRTC and Signaling are ready, sending init signal..."); + // 发送初始化消息告知对方我已上线并想连接 + sendInitToRemote(); + // sendIceCandidateToRemote(); // 现在改为手动点击 bt_stream 发送 + runOnUiThread(() -> updateStatus("已向设备 " + mRemoteUserId + " 发送初始化消息...")); } - runOnUiThread(() -> { - addMessage("收到[" + msgType + "]: " + data); - }); } @Override public void onDisconnected(String reason) { - Log.e(TAG, "onDisconnected: "); runOnUiThread(() -> { - updateStatus("已断开: " + reason); - addMessage("系统: 连接断开 - " + reason); + updateWsStatus("信令已断开: " + reason); + mViewDataBinding.btConnect.setEnabled(true); }); } @Override - public void onError(String error) { - Log.e(TAG, "onError: " + error); + public void onMessage(String message) { + Log.e(TAG, "onMessageReceived: " + message); + try { + WebRTCMessage webRTCMessage = mGson.fromJson(message, WebRTCMessage.class); + String type = webRTCMessage.getType(); + Object data = webRTCMessage.getData(); + + // 尝试解析为 SignalingMessage + SignalingMessage sm = null; + if (data != null) { + if (data instanceof String) { + try { + sm = mGson.fromJson((String) data, SignalingMessage.class); + } catch (Exception ignored) {} + } else { + try { + sm = mGson.fromJson(mGson.toJson(data), SignalingMessage.class); + } catch (Exception ignored) {} + } + } + + switch (type) { + case "offer": + onReceiveRemoteOffer(sm != null ? mGson.toJson(sm) : (data instanceof String ? (String) data : mGson.toJson(data))); + break; + case "answer": + if (sm != null && sm.getPayload() != null) { + if (sm.getPayload() instanceof String) { + onReceiveRemoteAnswer((String) sm.getPayload()); + } else { + JsonObject payloadJson = mGson.toJsonTree(sm.getPayload()).getAsJsonObject(); + if (payloadJson.has("description")) { + onReceiveRemoteAnswer(payloadJson.get("description").getAsString()); + } else { + onReceiveRemoteAnswer(mGson.toJson(sm.getPayload())); + } + } + } else if (data instanceof String) { + onReceiveRemoteAnswer((String) data); + } + break; + case "reject": + Object rejectReason = (sm != null && sm.getPayload() != null) ? sm.getPayload() : data; + handleRejectMessage(rejectReason instanceof String ? (String) rejectReason : mGson.toJson(rejectReason)); + break; + case "accept": + Object acceptReason = (sm != null && sm.getPayload() != null) ? sm.getPayload() : data; + handleAcceptMessage(acceptReason instanceof String ? (String) acceptReason : mGson.toJson(acceptReason)); + break; + case "sdp": + onReceiveRemoteAnswer((String) data); + break; + case "iceCandidate": + Object iceData = (sm != null && sm.getPayload() != null) ? sm.getPayload() : data; + try { + JsonObject iceJson = mGson.toJsonTree(iceData).getAsJsonObject(); + String sdp = iceJson.get("sdp").getAsString(); + String sdpMid = iceJson.get("sdpMid").getAsString(); + int sdpMLineIndex = iceJson.get("sdpMLineIndex").getAsInt(); + onReceiveRemoteIceCandidate(sdp, sdpMid, sdpMLineIndex); + } catch (Exception e) { + Log.e(TAG, "Error parsing iceCandidate: " + e.getMessage()); + } + break; + case "init": + // 收到初始化请求,主动发起Offer (如果是这种逻辑) + // 在本客户端通常是被控端发Offer,如果本端想看远程,可能需要等对方发Offer + break; + } + } catch (Exception e) { + Log.e(TAG, "Error parsing signaling message", e); + } + } + + private void handleAcceptMessage(String reason) { + Log.e(TAG, "handleAcceptMessage: reason = " + reason); runOnUiThread(() -> { - Log.e(TAG, "WebSocket错误: " + error); - addMessage("错误: " + error); + updateWsStatus("对方拒绝了连接: " + reason); + Toast.makeText(this, "正在连接", Toast.LENGTH_SHORT).show(); + }); + + + } + + private void handleRejectMessage(String reason) { + Log.e(TAG, "handleRejectMessage: reason = " + reason); + runOnUiThread(() -> { + updateWsStatus("对方拒绝了连接: " + reason); + mViewDataBinding.btConnect.setEnabled(true); + Toast.makeText(this, "对方拒绝了连接," + reason, Toast.LENGTH_SHORT).show(); + }); + } + + @Override + public void onMessage(ByteString bytes) { + + } + + @Override + public void onError(String error) { + runOnUiThread(() -> { + updateWsStatus("信令错误: " + error); + mViewDataBinding.btConnect.setEnabled(true); + Toast.makeText(this, "信令错误: " + error, Toast.LENGTH_SHORT).show(); }); } private void updateStatus(String status) { - mViewDataBinding.tvStatus.setText("状态: " + status); + mViewDataBinding.tvStatus.setText(status); } - private void addMessage(String message) { - String current = mViewDataBinding.tvStatus.getText().toString(); - mViewDataBinding.tvStatus.setText(current + "\n" + message); + private void updateWsStatus(String status) { + mViewDataBinding.tvWsStatus.setText(status); } + // ======================== 生命周期 ======================== - public class BtnClick { - 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("正在连接..."); + @Override + protected void onDestroy() { + super.onDestroy(); + if (mViewDataBinding.remoteVideoView != null) { + mViewDataBinding.remoteVideoView.release(); + } + if (peerConnection != null) { + peerConnection.close(); + peerConnection.dispose(); + } + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + } + if (rootEglBase != null) { + rootEglBase.release(); + } + if (mWebSocketManager != null) { + mWebSocketManager.removeCallback(this); } } + public class BtnClick { + public void connectWebSocket(View view) { + if (mWebSocketManager != null) { + if (mWebSocketManager.isConnected()) { + updateWsStatus("已连接到信令服务器"); + } else { + updateWsStatus("正在连接信令服务器..."); + mWebSocketManager.connect(); + } + } else { + mWebSocketManager = WebSocketManager.getInstance(); + updateWsStatus("正在连接信令服务器..."); + mWebSocketManager.connect(); + } + } + public void startStream(View view) { + Editable editable = mViewDataBinding.etNumber.getText(); + if (TextUtils.isEmpty(editable)) { + Toast.makeText(MainActivity.this, "请输入设备ID", Toast.LENGTH_SHORT).show(); + return; + } + mRemoteUserId = editable.toString(); + if (mWebSocketManager == null || !mWebSocketManager.isConnected()) { + Toast.makeText(MainActivity.this, "请先连接WebSocket", Toast.LENGTH_SHORT).show(); + return; + } + if (TextUtils.isEmpty(mRemoteUserId)) { + Toast.makeText(MainActivity.this, "未指定远程设备ID", Toast.LENGTH_SHORT).show(); + return; + } + Log.e(TAG, "Sending connection request to " + mRemoteUserId); + sendAskToRemote(); + updateStatus("已发送连接请求给 " + mRemoteUserId); + } + } + + // SdpObserver 简易适配器 + private static class SimpleSdpObserver implements SdpObserver { + private final String tag; + + SimpleSdpObserver(String tag) { + this.tag = tag; + } + + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.e(TAG, tag + " onCreateSuccess"); + } + + @Override + public void onSetSuccess() { + Log.e(TAG, tag + " onSetSuccess"); + } + + @Override + public void onCreateFailure(String s) { + Log.e(TAG, tag + " onCreateFailure: " + s); + } + + @Override + public void onSetFailure(String s) { + Log.e(TAG, tag + " onSetFailure: " + s); + } + } } diff --git a/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java b/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java index cadbf7d..2e02bd5 100644 --- a/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java +++ b/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java @@ -12,6 +12,7 @@ import com.alibaba.android.arouter.launcher.ARouter; import com.tencent.bugly.crashreport.CrashReport; import com.tencent.mmkv.MMKV; import com.ttstd.remoteclient.BuildConfig; +import com.ttstd.remoteclient.manager.WebSocketManager; import com.ttstd.remoteclient.utils.SystemUtils; @@ -55,6 +56,8 @@ public class BaseApplication extends Application { } ARouter.init(this); // 尽可能早,推荐在Application中初始化 + WebSocketManager.init(this); + CrashReport.initCrashReport(getApplicationContext(), "845e3ed68c", false); CrashReport.setDeviceId(this, Build.MODEL); xcrash.XCrash.init(this); diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java deleted file mode 100644 index d1a9695..0000000 --- a/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ttstd.remoteclient.bean; - -import java.io.Serializable; - -public class SignalMessage implements Serializable { - - - -} diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/SignalingMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/SignalingMessage.java new file mode 100644 index 0000000..d0e916d --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/bean/SignalingMessage.java @@ -0,0 +1,49 @@ +package com.ttstd.remoteclient.bean; + +import java.io.Serializable; + +public class SignalingMessage implements Serializable { + private String type; // "call", "offer", "answer", "iceCandidate", etc. + private String senderId; // 发送者ID + private String targetId; // 接收者ID + private Object payload; // 具体携带的数据(可以是String, SessionDescription, IceCandidate等) + + public SignalingMessage(String type, String senderId, String targetId, Object payload) { + this.type = type; + this.senderId = senderId; + this.targetId = targetId; + this.payload = payload; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getSenderId() { + return senderId; + } + + public void setSenderId(String senderId) { + this.senderId = senderId; + } + + public String getTargetId() { + return targetId; + } + + public void setTargetId(String targetId) { + this.targetId = targetId; + } + + public Object getPayload() { + return payload; + } + + public void setPayload(Object payload) { + this.payload = payload; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/WebRTCMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/WebRTCMessage.java new file mode 100644 index 0000000..c3fbc46 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/bean/WebRTCMessage.java @@ -0,0 +1,33 @@ +package com.ttstd.remoteclient.bean; + +import java.io.Serializable; + +public class WebRTCMessage implements Serializable { + private String type; // "offer", "answer", "iceCandidate", "call", "hangup", "reject", "accept" + private String target; + private Object data; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java index 06d0e7d..1070943 100644 --- a/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java +++ b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java @@ -15,4 +15,5 @@ public class WebSocketConfig { public static final long readTimeout = 10; // 读取超时时间(秒) public static final long writeTimeout = 10; // 写入超时时间(秒) + public static final long pingInterval = 10; } diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java b/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java deleted file mode 100644 index 41c3612..0000000 --- a/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java +++ /dev/null @@ -1,237 +0,0 @@ -package com.ttstd.remoteclient.manager; - -import android.os.Handler; -import android.os.Looper; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.ttstd.remoteclient.utils.SystemUtils; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.TimeUnit; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okio.ByteString; - -public class SignalingWebSocketClient { - private static final String TAG = "SignalingWebSocket"; - - public interface SignalingListener { - void onConnected(String sessionId); - - void onMessageReceived(String type, String data); - - void onDisconnected(String reason); - - void onError(String error); - } - -// private static final String SERVER_URL = "ws://175.178.213.60:2310/signaling/"; - private static final String SERVER_URL = "ws://192.168.100.111:2310/signaling/"; - - private static final int RECONNECT_DELAY_MS = 3000; - - private Gson gson = new Gson(); - private OkHttpClient client; - private WebSocket webSocket; - private Request request; - - private SignalingListener listener; - private Handler mainHandler; - private boolean mConnected = false; - private boolean shouldReconnect = true; - - private String mUserId; - - public SignalingWebSocketClient(SignalingListener listener) { - this.listener = listener; - this.mainHandler = new Handler(Looper.getMainLooper()); - - // 创建OkHttpClient,支持WebSocket - client = new OkHttpClient.Builder() - .pingInterval(20, TimeUnit.SECONDS) // 保持连接活跃 - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .build(); - } - - public void setUserId(String userId) { - mUserId = userId; - } - - public void connect() { - if (mConnected) { - return; - } - if (webSocket != null) { - return; - } - - request = new Request.Builder() - .url(SERVER_URL + SystemUtils.getSerial()) - .build(); - - mConnected = false; - shouldReconnect = true; - - webSocket = client.newWebSocket(request, new WebSocketListener() { - @Override - public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { - mConnected = true; - runOnUiThread(() -> { - if (listener != null) { - listener.onConnected("连接成功"); - } - }); - } - - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { - runOnUiThread(() -> { - try { - JsonObject json = gson.fromJson(text, JsonObject.class); - String type = json.get("type").getAsString(); - - switch (type) { - case "connected": - String sessionId = json.get("sessionId").getAsString(); - if (listener != null) { - listener.onConnected(sessionId); - } - break; - case "signal": - String from = json.get("from").getAsString(); - String data = json.get("data").toString(); - if (listener != null) { - listener.onMessageReceived("signal", "来自" + from + ": " + data); - } - break; - case "ack": - if (listener != null) { - listener.onMessageReceived("ack", "消息发送成功"); - } - break; - case "peer_left": - String leftSessionId = json.get("sessionId").getAsString(); - if (listener != null) { - listener.onMessageReceived("peer_left", "对端离开: " + leftSessionId); - } - break; - case "error": - String errorMsg = json.get("message").getAsString(); - if (listener != null) { - listener.onError(errorMsg); - } - break; - } - } catch (Exception e) { - if (listener != null) { - listener.onMessageReceived("raw", text); - } - } - }); - } - - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { - // 处理二进制消息 - runOnUiThread(() -> { - if (listener != null) { - listener.onMessageReceived("binary", bytes.hex()); - } - }); - } - - @Override - public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { - mConnected = false; - runOnUiThread(() -> { - if (listener != null) { - listener.onDisconnected("连接关闭中: " + reason); - } - }); - } - - @Override - public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { - mConnected = false; - webSocket = null; - - runOnUiThread(() -> { - if (listener != null) { - listener.onDisconnected("连接已关闭: " + reason); - } - }); - - // 自动重连 - if (shouldReconnect) { - mainHandler.postDelayed(() -> connect(), RECONNECT_DELAY_MS); - } - } - - @Override - public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, - @Nullable Response response) { - mConnected = false; - webSocket = null; - - runOnUiThread(() -> { - if (listener != null) { - listener.onError("连接失败: " + t.getMessage()); - listener.onDisconnected(t.getMessage()); - } - }); - - // 自动重连 - if (shouldReconnect) { - mainHandler.postDelayed(() -> connect(), RECONNECT_DELAY_MS); - } - } - }); - } - - public void sendSignal(String signalData) { - if (webSocket != null) { - webSocket.send(signalData); - } - } - - public void sendJsonSignal(JsonObject signalJson) { - if (webSocket != null) { - webSocket.send(gson.toJson(signalJson)); - } - } - - public void disconnect() { - mConnected = false; - shouldReconnect = false; - - if (webSocket != null) { - webSocket.close(1000, "正常断开"); - webSocket = null; - } - - if (client != null) { - client.dispatcher().executorService().shutdown(); - } - } - - public boolean isConnected() { - return webSocket != null; - } - - private void runOnUiThread(Runnable runnable) { - if (Looper.myLooper() == Looper.getMainLooper()) { - runnable.run(); - } else { - mainHandler.post(runnable); - } - } -} diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketCallback.java b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketCallback.java new file mode 100644 index 0000000..524665c --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketCallback.java @@ -0,0 +1,42 @@ +package com.ttstd.remoteclient.manager; + +import okio.ByteString; + +/** + * WebSocket 回调接口 + * 所有回调都在主线程执行,方便更新 UI + */ +public interface WebSocketCallback { + /** + * 连接成功 + */ + void onConnected(String sessionId); + + /** + * 连接断开 + * + * @param reason 断开原因 + */ + void onDisconnected(String reason); + + /** + * 收到文本消息 + * + * @param message 文本内容 + */ + void onMessage(String message); + + /** + * 收到二进制消息 + * + * @param bytes 二进制数据 + */ + void onMessage(ByteString bytes); + + /** + * 连接发生错误 + * + * @param error 错误信息 + */ + void onError(String error); +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketManager.java b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketManager.java new file mode 100644 index 0000000..4a96ca6 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketManager.java @@ -0,0 +1,213 @@ +package com.ttstd.remoteclient.manager; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.ttstd.remoteclient.config.WebSocketConfig; +import com.ttstd.remoteclient.utils.SystemUtils; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class WebSocketManager { + private static final String TAG = "WebSocketManager"; + + @SuppressLint("StaticFieldLeak") + private static volatile WebSocketManager instance; + + private final Context mContext; + + //private static final String SERVER_URL = "ws://175.178.213.60:2310/signaling/"; + private static final String SERVER_URL = "ws://192.168.100.224:2310/signaling/"; + + private static final int RECONNECT_DELAY_MS = 3000; + + private final Gson gson = new Gson(); + private final OkHttpClient client; + private final Request request; + private WebSocket webSocket; + + private final Set mCallbackSet = new CopyOnWriteArraySet<>(); + private boolean mConnected = false; + private boolean shouldReconnect = true; + + private final String mLocalUserId; + + private WebSocketManager(Context context) { + this.mContext = context.getApplicationContext(); + this.mLocalUserId = SystemUtils.getUniqueID(mContext); + + // 创建OkHttpClient,支持WebSocket + client = new OkHttpClient.Builder() + .pingInterval(WebSocketConfig.pingInterval, TimeUnit.SECONDS) // 保持连接活跃 + .connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS) + .readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS) + .writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS) + .build(); + request = new Request.Builder() + .url(SERVER_URL + mLocalUserId) + .build(); + + webSocket = client.newWebSocket(request, listener); + } + + public static void init(Context context) { + if (instance == null) { + synchronized (WebSocketManager.class) { + if (instance == null) { + instance = new WebSocketManager(context); + } + } + } + } + + public static WebSocketManager getInstance() { + if (instance == null) { + throw new IllegalStateException("WebSocketManager must be initialized first with init(Context)"); + } + return instance; + } + + public void addCallback(WebSocketCallback listener) { + this.mCallbackSet.add(listener); + if (isConnected()) { + listener.onConnected("连接成功"); + } + } + + public void removeCallback(WebSocketCallback listener) { + this.mCallbackSet.remove(listener); + } + + public void connect() { + if (mConnected && webSocket != null) return; + webSocket = client.newWebSocket(request, listener); + } + + private final WebSocketListener listener = new WebSocketListener() { + @Override + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + Log.e(TAG, "onOpen: " + response.message()); + mConnected = true; + for (WebSocketCallback callback : mCallbackSet) { + callback.onConnected("连接成功"); + } + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + Log.e(TAG, "onMessage: " + text); + for (WebSocketCallback callback : mCallbackSet) { + callback.onMessage(text); + } + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { + Log.d(TAG, "onMessage: Received binary message, size = " + bytes.size()); + // 处理二进制消息 + for (WebSocketCallback callback : mCallbackSet) { + callback.onMessage(bytes); + } + } + + @Override + public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + Log.e(TAG, "onClosing: code = " + code + " reason = " + reason); + mConnected = false; + for (WebSocketCallback callback : mCallbackSet) { + callback.onDisconnected("连接关闭中: " + reason); + } + } + + @Override + public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + Log.e(TAG, "onClosed: code = " + code + " reason = " + reason); + mConnected = false; + WebSocketManager.this.webSocket = null; + + for (WebSocketCallback callback : mCallbackSet) { + callback.onDisconnected("连接已关闭: " + reason); + } + + // 自动重连 + if (shouldReconnect) { + // 由于删除了 Handler,且用户要求不考虑线程,这里直接重连或在子线程重连 + // OkHttp 的回调已经在子线程,这里直接调用 connect 会立即尝试。 + // 加上延时通常更好,但用户说不考虑线程问题,这里可以用简单的方式。 + try { + Thread.sleep(RECONNECT_DELAY_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + connect(); + } + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { + Log.e(TAG, "onFailure: Throwable = " + t.getMessage()); + mConnected = false; + WebSocketManager.this.webSocket = null; + + for (WebSocketCallback callback : mCallbackSet) { + callback.onError("检查网络"); + callback.onDisconnected("连接失败"); + } + + // 自动重连 + if (shouldReconnect) { + try { + Thread.sleep(RECONNECT_DELAY_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + connect(); + } + } + }; + + public void sendSignal(String signalData) { + if (webSocket != null && mConnected) { + webSocket.send(signalData); + } + } + + public void sendJsonSignal(JsonObject signalJson) { + if (webSocket != null && mConnected) { + webSocket.send(gson.toJson(signalJson)); + } + } + + public void disconnect() { + mConnected = false; + shouldReconnect = false; + + if (webSocket != null) { + webSocket.close(1000, "正常断开"); + webSocket = null; + } + } + + public boolean isConnected() { + return mConnected && webSocket != null; + } + + public String getLocalUserId() { + return mLocalUserId; + } +} diff --git a/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java b/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java index feca9e4..9f59b90 100644 --- a/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java +++ b/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java @@ -4,38 +4,22 @@ import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; import android.os.Build; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; import java.lang.reflect.Method; import java.util.List; +import java.util.UUID; public class SystemUtils { - /** - * 获取设备序列号 - * - * @return - */ - @SuppressLint("MissingPermission") - public static String getSerial() { - String serial = "unknow"; - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+ - serial = Build.getSerial(); - } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+ - serial = Build.SERIAL; - } else {//8.0- - Class c = Class.forName("android.os.SystemProperties"); - Method get = c.getMethod("get", String.class); - serial = (String) get.invoke(c, "ro.serialno"); - } - } catch (Exception e) { - e.printStackTrace(); - Log.e("e", "读取设备序列号异常:" + e.toString()); - } - return serial; - } - public static boolean isMainProcessName(Context cxt, int pid) { String packageName = cxt.getPackageName(); ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE); @@ -50,4 +34,112 @@ public class SystemUtils { } return false; } + + private static final String CACHE_FILE_NAME = ".device_id_cache"; + private static String sUniqueId = null; + + /** + * 获取设备唯一标识(核心兼容方法) + */ + public static String getUniqueID(Context context) { + if (!TextUtils.isEmpty(sUniqueId)) { + return sUniqueId; + } + + Context appContext = context.getApplicationContext(); + + // 1. 尝试从本地缓存(内存/文件)读取,保证一致性 + sUniqueId = readIdFromFile(appContext); + if (!TextUtils.isEmpty(sUniqueId)) { + return sUniqueId; + } + + // 2. 针对 Android 10 (API 29) 及以上版本的处理 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // 高版本直接使用 ANDROID_ID + sUniqueId = getAndroidId(appContext); + } else { + // 3. 针对 Android 10 以下版本的处理:尝试获取 IMEI + sUniqueId = getIMEI(appContext); + if (TextUtils.isEmpty(sUniqueId)) { + sUniqueId = getAndroidId(appContext); + } + } + + // 4. 极端兜底:如果 ANDROID_ID 也为空(极少见)或格式错误,生成一个随机 UUID + if (TextUtils.isEmpty(sUniqueId) || "9774d56d6824155a".equals(sUniqueId)) { + sUniqueId = UUID.randomUUID().toString().replace("-", ""); + } + + // 5. 将生成的唯一标识持久化到本地,防止应用卸载重装或系统升级后发生改变 + saveIdToFile(appContext, sUniqueId); + + return sUniqueId; + } + + /** + * 获取 ANDROID_ID + * 优点:Android 10+ 不需要任何权限 + * 缺点:设备恢复出厂设置会改变;不同签名的 App 获取到的值可能不同 + */ + @SuppressLint("HardwareIds") + private static String getAndroidId(Context context) { + try { + String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + if (!TextUtils.isEmpty(androidId)) { + return androidId; + } + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + /** + * 获取 IMEI (仅在 Android 10 以下有效,且需要 READ_PHONE_STATE 权限) + */ + @SuppressLint({"HardwareIds", "MissingPermission"}) + private static String getIMEI(Context context) { + try { + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (tm != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return tm.getImei(); + } else { + return tm.getDeviceId(); + } + } + } catch (Exception e) { + // 权限未授予或 Android 10+ 抛出异常,直接捕获 + } + return ""; + } + + /** + * 从内部存储读取缓存的 ID + */ + private static String readIdFromFile(Context context) { + File file = new File(context.getFilesDir(), CACHE_FILE_NAME); + if (!file.exists()) return null; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return reader.readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 将 ID 保存到内部存储 + */ + private static void saveIdToFile(Context context, String id) { + File file = new File(context.getFilesDir(), CACHE_FILE_NAME); + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(id.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } diff --git a/app/src/main/res/layout/activity_display.xml b/app/src/main/res/layout/activity_display.xml new file mode 100644 index 0000000..c95b89b --- /dev/null +++ b/app/src/main/res/layout/activity_display.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 72fd377..b9e144a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -15,13 +15,36 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + +