Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aba3930417 | ||
|
|
4476f41aa6 | ||
|
|
5122683761 | ||
|
|
924835dd55 |
@@ -4,6 +4,7 @@ import android.util.Base64;
|
|||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -14,16 +15,22 @@ import java.util.Arrays;
|
|||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
final class AppEventSocketClient {
|
final class AppEventSocketClient {
|
||||||
|
private static final long HEARTBEAT_INTERVAL_MS = 15000L;
|
||||||
|
private static final int SOCKET_CONNECT_TIMEOUT_MS = 10000;
|
||||||
|
private static final int SOCKET_READ_TIMEOUT_MS = 45000;
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
void onMessage(String text);
|
void onMessage(String text);
|
||||||
void onClosed();
|
void onClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
|
private final Object writeLock = new Object();
|
||||||
private Socket socket;
|
private Socket socket;
|
||||||
private BufferedInputStream input;
|
private BufferedInputStream input;
|
||||||
private BufferedOutputStream output;
|
private BufferedOutputStream output;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
|
private Thread heartbeatThread;
|
||||||
|
|
||||||
AppEventSocketClient(Listener listener) {
|
AppEventSocketClient(Listener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
@@ -40,12 +47,11 @@ final class AppEventSocketClient {
|
|||||||
sendFrame(8, new byte[0]);
|
sendFrame(8, new byte[0]);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
try {
|
closeSocketQuietly();
|
||||||
if (socket != null) {
|
}
|
||||||
socket.close();
|
|
||||||
}
|
boolean isClosed() {
|
||||||
} catch (Exception ignored) {
|
return closed;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void run(String baseUrl) {
|
private void run(String baseUrl) {
|
||||||
@@ -55,6 +61,7 @@ final class AppEventSocketClient {
|
|||||||
input = new BufferedInputStream(socket.getInputStream());
|
input = new BufferedInputStream(socket.getInputStream());
|
||||||
output = new BufferedOutputStream(socket.getOutputStream());
|
output = new BufferedOutputStream(socket.getOutputStream());
|
||||||
handshake(uri);
|
handshake(uri);
|
||||||
|
startHeartbeat();
|
||||||
while (!closed) {
|
while (!closed) {
|
||||||
Frame frame = readFrame();
|
Frame frame = readFrame();
|
||||||
if (frame.opcode == 1) {
|
if (frame.opcode == 1) {
|
||||||
@@ -69,12 +76,7 @@ final class AppEventSocketClient {
|
|||||||
} finally {
|
} finally {
|
||||||
closed = true;
|
closed = true;
|
||||||
listener.onClosed();
|
listener.onClosed();
|
||||||
try {
|
closeSocketQuietly();
|
||||||
if (socket != null) {
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +93,39 @@ final class AppEventSocketClient {
|
|||||||
if (port == -1) {
|
if (port == -1) {
|
||||||
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
||||||
}
|
}
|
||||||
|
Socket raw = new Socket();
|
||||||
|
raw.connect(new InetSocketAddress(uri.getHost(), port), SOCKET_CONNECT_TIMEOUT_MS);
|
||||||
|
Socket connected;
|
||||||
if ("wss".equalsIgnoreCase(uri.getScheme())) {
|
if ("wss".equalsIgnoreCase(uri.getScheme())) {
|
||||||
return SSLSocketFactory.getDefault().createSocket(uri.getHost(), port);
|
connected = ((SSLSocketFactory) SSLSocketFactory.getDefault())
|
||||||
|
.createSocket(raw, uri.getHost(), port, true);
|
||||||
|
} else {
|
||||||
|
connected = raw;
|
||||||
}
|
}
|
||||||
return new Socket(uri.getHost(), port);
|
connected.setKeepAlive(true);
|
||||||
|
connected.setTcpNoDelay(true);
|
||||||
|
connected.setSoTimeout(SOCKET_READ_TIMEOUT_MS);
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startHeartbeat() {
|
||||||
|
heartbeatThread = new Thread(() -> {
|
||||||
|
while (!closed) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(HEARTBEAT_INTERVAL_MS);
|
||||||
|
if (!closed) {
|
||||||
|
sendFrame(9, new byte[0]);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return;
|
||||||
|
} catch (Exception error) {
|
||||||
|
closeSocketQuietly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "app-events-ws-heartbeat");
|
||||||
|
heartbeatThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handshake(URI uri) throws Exception {
|
private void handshake(URI uri) throws Exception {
|
||||||
@@ -181,32 +212,47 @@ final class AppEventSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void sendFrame(int opcode, byte[] payload) throws Exception {
|
private void sendFrame(int opcode, byte[] payload) throws Exception {
|
||||||
if (output == null) {
|
synchronized (writeLock) {
|
||||||
return;
|
if (output == null) {
|
||||||
}
|
return;
|
||||||
output.write(0x80 | opcode);
|
|
||||||
byte[] mask = new byte[4];
|
|
||||||
new SecureRandom().nextBytes(mask);
|
|
||||||
int length = payload.length;
|
|
||||||
if (length < 126) {
|
|
||||||
output.write(0x80 | length);
|
|
||||||
} else if (length <= 0xffff) {
|
|
||||||
output.write(0x80 | 126);
|
|
||||||
output.write((length >>> 8) & 0xff);
|
|
||||||
output.write(length & 0xff);
|
|
||||||
} else {
|
|
||||||
output.write(0x80 | 127);
|
|
||||||
for (int i = 7; i >= 0; i--) {
|
|
||||||
output.write((length >>> (8 * i)) & 0xff);
|
|
||||||
}
|
}
|
||||||
|
output.write(0x80 | opcode);
|
||||||
|
byte[] mask = new byte[4];
|
||||||
|
new SecureRandom().nextBytes(mask);
|
||||||
|
int length = payload.length;
|
||||||
|
if (length < 126) {
|
||||||
|
output.write(0x80 | length);
|
||||||
|
} else if (length <= 0xffff) {
|
||||||
|
output.write(0x80 | 126);
|
||||||
|
output.write((length >>> 8) & 0xff);
|
||||||
|
output.write(length & 0xff);
|
||||||
|
} else {
|
||||||
|
output.write(0x80 | 127);
|
||||||
|
for (int i = 7; i >= 0; i--) {
|
||||||
|
output.write((length >>> (8 * i)) & 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.write(mask);
|
||||||
|
byte[] masked = Arrays.copyOf(payload, payload.length);
|
||||||
|
for (int i = 0; i < masked.length; i++) {
|
||||||
|
masked[i] = (byte) (masked[i] ^ mask[i % 4]);
|
||||||
|
}
|
||||||
|
output.write(masked);
|
||||||
|
output.flush();
|
||||||
}
|
}
|
||||||
output.write(mask);
|
}
|
||||||
byte[] masked = Arrays.copyOf(payload, payload.length);
|
|
||||||
for (int i = 0; i < masked.length; i++) {
|
private void closeSocketQuietly() {
|
||||||
masked[i] = (byte) (masked[i] ^ mask[i % 4]);
|
if (heartbeatThread != null) {
|
||||||
|
heartbeatThread.interrupt();
|
||||||
|
heartbeatThread = null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (socket != null) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
output.write(masked);
|
|
||||||
output.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Frame {
|
private static final class Frame {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import android.graphics.drawable.StateListDrawable;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
@@ -46,7 +48,9 @@ import org.json.JSONObject;
|
|||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -60,12 +64,13 @@ public final class MainActivity extends Activity {
|
|||||||
private static final int MAX_TERMINAL_COLS = 140;
|
private static final int MAX_TERMINAL_COLS = 140;
|
||||||
private static final int MIN_TERMINAL_ROWS = 8;
|
private static final int MIN_TERMINAL_ROWS = 8;
|
||||||
private static final int MAX_TERMINAL_ROWS = 80;
|
private static final int MAX_TERMINAL_ROWS = 80;
|
||||||
private static final int TERMINAL_KEYS_HEIGHT_DP = 76;
|
private static final int TERMINAL_KEYS_HEIGHT_DP = 64;
|
||||||
private static final int STATUS_NORMAL = 0;
|
private static final int STATUS_NORMAL = 0;
|
||||||
private static final int STATUS_BUSY = 1;
|
private static final int STATUS_BUSY = 1;
|
||||||
private static final int STATUS_SUCCESS = 2;
|
private static final int STATUS_SUCCESS = 2;
|
||||||
private static final int STATUS_ERROR = 3;
|
private static final int STATUS_ERROR = 3;
|
||||||
private static final long TERMINAL_RENDER_INTERVAL_MS = 80L;
|
private static final long TERMINAL_RENDER_INTERVAL_MS = 80L;
|
||||||
|
private static final long[] SOCKET_RECONNECT_DELAYS_MS = {1000L, 2000L, 4000L, 8000L, 15000L};
|
||||||
private static final int COLOR_APP_BG = Color.rgb(9, 11, 13);
|
private static final int COLOR_APP_BG = Color.rgb(9, 11, 13);
|
||||||
private static final int COLOR_BAR = Color.rgb(15, 18, 21);
|
private static final int COLOR_BAR = Color.rgb(15, 18, 21);
|
||||||
private static final int COLOR_PANEL = Color.rgb(22, 26, 30);
|
private static final int COLOR_PANEL = Color.rgb(22, 26, 30);
|
||||||
@@ -92,7 +97,7 @@ public final class MainActivity extends Activity {
|
|||||||
private static final String PAGE_TOOLS = "Tools";
|
private static final String PAGE_TOOLS = "Tools";
|
||||||
private static final String PAGE_UPDATE = "Update";
|
private static final String PAGE_UPDATE = "Update";
|
||||||
private static final String PAGE_ABOUT = "About";
|
private static final String PAGE_ABOUT = "About";
|
||||||
private static final String TERMINAL_ENTER = "\n";
|
private static final String TERMINAL_ENTER = "\r";
|
||||||
private static final String[] SERVER_PROFILES = {
|
private static final String[] SERVER_PROFILES = {
|
||||||
"http://100.89.0.116:3000",
|
"http://100.89.0.116:3000",
|
||||||
"http://100.89.0.2:3000",
|
"http://100.89.0.2:3000",
|
||||||
@@ -102,6 +107,7 @@ public final class MainActivity extends Activity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
private SharedPreferences prefs;
|
private SharedPreferences prefs;
|
||||||
private UpdateManager updateManager;
|
private UpdateManager updateManager;
|
||||||
private SessionApiClient api;
|
private SessionApiClient api;
|
||||||
@@ -129,10 +135,18 @@ public final class MainActivity extends Activity {
|
|||||||
private TerminalScreenBuffer terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS);
|
private TerminalScreenBuffer terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS);
|
||||||
private final StringBuilder queuedTerminalInput = new StringBuilder();
|
private final StringBuilder queuedTerminalInput = new StringBuilder();
|
||||||
private boolean terminalConnected;
|
private boolean terminalConnected;
|
||||||
|
private boolean terminalConnecting;
|
||||||
private boolean terminalRenderPending;
|
private boolean terminalRenderPending;
|
||||||
private boolean terminalSelectionEnabled;
|
private boolean terminalSelectionEnabled;
|
||||||
private boolean terminalFollowOutput = true;
|
private boolean terminalFollowOutput = true;
|
||||||
private int terminalKeyPage;
|
private int terminalKeyPage;
|
||||||
|
private int terminalReconnectAttempt;
|
||||||
|
private int terminalConnectionGeneration;
|
||||||
|
private int eventReconnectAttempt;
|
||||||
|
private int eventConnectionGeneration;
|
||||||
|
private boolean activityDestroyed;
|
||||||
|
private Runnable terminalReconnectTask;
|
||||||
|
private Runnable eventReconnectTask;
|
||||||
private long lastTerminalRenderMs;
|
private long lastTerminalRenderMs;
|
||||||
private int terminalCols = DEFAULT_TERMINAL_COLS;
|
private int terminalCols = DEFAULT_TERMINAL_COLS;
|
||||||
private int terminalRows = DEFAULT_TERMINAL_ROWS;
|
private int terminalRows = DEFAULT_TERMINAL_ROWS;
|
||||||
@@ -1320,9 +1334,10 @@ public final class MainActivity extends Activity {
|
|||||||
JSONObject rootObject = new JSONObject(text == null || text.isEmpty() ? "{}" : text);
|
JSONObject rootObject = new JSONObject(text == null || text.isEmpty() ? "{}" : text);
|
||||||
JSONArray projects = rootObject.optJSONArray("projects");
|
JSONArray projects = rootObject.optJSONArray("projects");
|
||||||
if (projects == null) {
|
if (projects == null) {
|
||||||
terminalGroupRow.addView(groupLabel("no group"));
|
renderUngroupedTerminalSessions(sessionName, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Set<String> groupedSessions = collectGroupedSessions(projects);
|
||||||
for (int projectIndex = 0; projectIndex < projects.length(); projectIndex++) {
|
for (int projectIndex = 0; projectIndex < projects.length(); projectIndex++) {
|
||||||
JSONObject project = projects.optJSONObject(projectIndex);
|
JSONObject project = projects.optJSONObject(projectIndex);
|
||||||
if (project == null) {
|
if (project == null) {
|
||||||
@@ -1342,12 +1357,62 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalGroupRow.addView(groupLabel("no group"));
|
renderUngroupedTerminalSessions(sessionName, groupedSessions);
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
terminalGroupRow.addView(groupLabel("group parse"));
|
terminalGroupRow.addView(groupLabel("group parse"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> collectGroupedSessions(JSONArray projects) {
|
||||||
|
Set<String> names = new HashSet<>();
|
||||||
|
for (int projectIndex = 0; projectIndex < projects.length(); projectIndex++) {
|
||||||
|
JSONObject project = projects.optJSONObject(projectIndex);
|
||||||
|
JSONArray agents = project == null ? null : project.optJSONArray("agents");
|
||||||
|
for (int agentIndex = 0; agents != null && agentIndex < agents.length(); agentIndex++) {
|
||||||
|
String sessionName = agentSessionName(agents.optJSONObject(agentIndex));
|
||||||
|
if (!sessionName.isEmpty()) {
|
||||||
|
names.add(sessionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderUngroupedTerminalSessions(String sessionName, Set<String> groupedSessions) {
|
||||||
|
terminalGroupRow.addView(groupLabel("ungrouped..."));
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
List<SessionSummary> sessions = api.getSessions();
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (!sessionName.equals(activeSessionName) || terminalGroupRow == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
terminalGroupRow.removeAllViews();
|
||||||
|
terminalGroupRow.addView(groupLabel("ungrouped"));
|
||||||
|
int count = 0;
|
||||||
|
for (SessionSummary session : sessions) {
|
||||||
|
if (groupedSessions != null && groupedSessions.contains(session.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
terminalGroupRow.addView(groupSessionButton(session.name, session.name.equals(sessionName)));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
terminalGroupRow.removeAllViews();
|
||||||
|
terminalGroupRow.addView(groupLabel("no group"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception error) {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (sessionName.equals(activeSessionName) && terminalGroupRow != null) {
|
||||||
|
terminalGroupRow.removeAllViews();
|
||||||
|
terminalGroupRow.addView(groupLabel("ungrouped failed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private boolean projectContainsSession(JSONArray agents, String sessionName) {
|
private boolean projectContainsSession(JSONArray agents, String sessionName) {
|
||||||
if (agents == null) {
|
if (agents == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1543,10 +1608,10 @@ public final class MainActivity extends Activity {
|
|||||||
|
|
||||||
terminalKeyPageButton = terminalPageButton();
|
terminalKeyPageButton = terminalPageButton();
|
||||||
LinearLayout.LayoutParams pageParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams pageParams = new LinearLayout.LayoutParams(
|
||||||
dp(70),
|
dp(62),
|
||||||
dp(48)
|
dp(42)
|
||||||
);
|
);
|
||||||
pageParams.rightMargin = dp(6);
|
pageParams.rightMargin = dp(4);
|
||||||
row.addView(terminalKeyPageButton, pageParams);
|
row.addView(terminalKeyPageButton, pageParams);
|
||||||
|
|
||||||
inputField = new EditText(this);
|
inputField = new EditText(this);
|
||||||
@@ -1555,7 +1620,7 @@ public final class MainActivity extends Activity {
|
|||||||
inputField.setHint("type, edit, paste");
|
inputField.setHint("type, edit, paste");
|
||||||
inputField.setSingleLine(false);
|
inputField.setSingleLine(false);
|
||||||
inputField.setMinLines(1);
|
inputField.setMinLines(1);
|
||||||
inputField.setMaxLines(3);
|
inputField.setMaxLines(2);
|
||||||
inputField.setCursorVisible(true);
|
inputField.setCursorVisible(true);
|
||||||
inputField.setFocusableInTouchMode(true);
|
inputField.setFocusableInTouchMode(true);
|
||||||
inputField.setGravity(Gravity.TOP | Gravity.START);
|
inputField.setGravity(Gravity.TOP | Gravity.START);
|
||||||
@@ -1581,13 +1646,9 @@ public final class MainActivity extends Activity {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
row.addView(inputField, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
row.addView(inputField, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||||
row.addView(toolbarButton("Type", view -> sendTypedText()), new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
||||||
dp(48)
|
|
||||||
));
|
|
||||||
row.addView(toolbarButton("Send", view -> sendLine()), new LinearLayout.LayoutParams(
|
row.addView(toolbarButton("Send", view -> sendLine()), new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
dp(48)
|
dp(42)
|
||||||
));
|
));
|
||||||
bar.addView(row, matchWrap());
|
bar.addView(row, matchWrap());
|
||||||
return bar;
|
return bar;
|
||||||
@@ -2258,9 +2319,22 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void connectTerminal(String sessionName) {
|
private void connectTerminal(String sessionName) {
|
||||||
closeTerminalSocket();
|
terminalReconnectAttempt = 0;
|
||||||
queuedTerminalInput.setLength(0);
|
terminalConnectionGeneration++;
|
||||||
|
cancelTerminalReconnect();
|
||||||
|
closeTerminalSocket(false);
|
||||||
|
connectTerminalSocket(sessionName, terminalConnectionGeneration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectTerminalSocket(String sessionName, int generation) {
|
||||||
|
if (activityDestroyed
|
||||||
|
|| activeSessionName == null
|
||||||
|
|| !sessionName.equals(activeSessionName)
|
||||||
|
|| generation != terminalConnectionGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
terminalConnected = false;
|
terminalConnected = false;
|
||||||
|
terminalConnecting = true;
|
||||||
terminalSocketStatus = "terminal connecting";
|
terminalSocketStatus = "terminal connecting";
|
||||||
updateTerminalMeta();
|
updateTerminalMeta();
|
||||||
setStatus("Connecting " + sessionName);
|
setStatus("Connecting " + sessionName);
|
||||||
@@ -2270,7 +2344,12 @@ public final class MainActivity extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public void onConnected() {
|
public void onConnected() {
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
|
if (!isCurrentTerminalConnection(sessionName, generation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
terminalConnected = true;
|
terminalConnected = true;
|
||||||
|
terminalConnecting = false;
|
||||||
|
terminalReconnectAttempt = 0;
|
||||||
terminalSocketStatus = "terminal connected";
|
terminalSocketStatus = "terminal connected";
|
||||||
updateTerminalMeta();
|
updateTerminalMeta();
|
||||||
setStatus("Connected " + sessionName);
|
setStatus("Connected " + sessionName);
|
||||||
@@ -2281,12 +2360,20 @@ public final class MainActivity extends Activity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOutput(String data) {
|
public void onOutput(String data) {
|
||||||
runOnUiThread(() -> appendTerminal(data));
|
runOnUiThread(() -> {
|
||||||
|
if (isCurrentTerminalConnection(sessionName, generation)) {
|
||||||
|
appendTerminal(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(String message) {
|
public void onError(String message) {
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
|
if (!isCurrentTerminalConnection(sessionName, generation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
terminalConnecting = false;
|
||||||
appendTerminal("\r\n[error] " + message + "\r\n");
|
appendTerminal("\r\n[error] " + message + "\r\n");
|
||||||
terminalSocketStatus = "terminal error";
|
terminalSocketStatus = "terminal error";
|
||||||
updateTerminalMeta();
|
updateTerminalMeta();
|
||||||
@@ -2297,10 +2384,15 @@ public final class MainActivity extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public void onClosed() {
|
public void onClosed() {
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
|
if (!isCurrentTerminalConnection(sessionName, generation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
terminalConnected = false;
|
terminalConnected = false;
|
||||||
|
terminalConnecting = false;
|
||||||
terminalSocketStatus = "terminal disconnected";
|
terminalSocketStatus = "terminal disconnected";
|
||||||
updateTerminalMeta();
|
updateTerminalMeta();
|
||||||
setStatus("Disconnected " + sessionName);
|
setStatus("Disconnected " + sessionName);
|
||||||
|
scheduleTerminalReconnect(sessionName, generation);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2360,23 +2452,9 @@ public final class MainActivity extends Activity {
|
|||||||
setStatus("Sent " + text.length() + " chars");
|
setStatus("Sent " + text.length() + " chars");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendTypedText() {
|
|
||||||
if (inputField == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String text = inputField.getText().toString();
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String normalized = text.replace("\r\n", "\n").replace('\r', '\n');
|
|
||||||
terminalFollowOutput = true;
|
|
||||||
sendTerminalInput(normalized);
|
|
||||||
inputField.setText("");
|
|
||||||
setStatus("Typed " + text.length() + " chars");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendTerminalInput(String data) {
|
private void sendTerminalInput(String data) {
|
||||||
if (activeSessionName == null) {
|
String sessionName = activeSessionName;
|
||||||
|
if (sessionName == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TerminalSocketClient socket = terminalSocket;
|
TerminalSocketClient socket = terminalSocket;
|
||||||
@@ -2385,7 +2463,7 @@ public final class MainActivity extends Activity {
|
|||||||
setStatus("Sent input");
|
setStatus("Sent input");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (socket != null && !socket.isClosed()) {
|
if ((socket != null && !socket.isClosed()) || terminalConnecting || terminalReconnectTask != null) {
|
||||||
queuedTerminalInput.append(data);
|
queuedTerminalInput.append(data);
|
||||||
setStatus("Queued input until terminal connects");
|
setStatus("Queued input until terminal connects");
|
||||||
return;
|
return;
|
||||||
@@ -2393,7 +2471,7 @@ public final class MainActivity extends Activity {
|
|||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < data.length(); i += 200) {
|
for (int i = 0; i < data.length(); i += 200) {
|
||||||
api.sendInput(activeSessionName, data.substring(i, Math.min(i + 200, data.length())));
|
api.sendInput(sessionName, data.substring(i, Math.min(i + 200, data.length())));
|
||||||
}
|
}
|
||||||
runOnUiThread(() -> setStatus("Sent input"));
|
runOnUiThread(() -> setStatus("Sent input"));
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
@@ -2673,9 +2751,9 @@ public final class MainActivity extends Activity {
|
|||||||
private Button terminalPageButton() {
|
private Button terminalPageButton() {
|
||||||
Button button = toolbarButton("", view -> setTerminalKeyPage(terminalKeyPage + 1));
|
Button button = toolbarButton("", view -> setTerminalKeyPage(terminalKeyPage + 1));
|
||||||
button.setTextSize(10);
|
button.setTextSize(10);
|
||||||
button.setPadding(dp(5), 0, dp(5), 0);
|
button.setPadding(dp(4), 0, dp(4), 0);
|
||||||
button.setMinWidth(dp(70));
|
button.setMinWidth(dp(62));
|
||||||
button.setMinimumWidth(dp(70));
|
button.setMinimumWidth(dp(62));
|
||||||
button.setOnLongClickListener(view -> {
|
button.setOnLongClickListener(view -> {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||||
setTerminalKeyPage(terminalKeyPage - 1);
|
setTerminalKeyPage(terminalKeyPage - 1);
|
||||||
@@ -2691,7 +2769,7 @@ public final class MainActivity extends Activity {
|
|||||||
|
|
||||||
private void updateTerminalPageButton(Button button) {
|
private void updateTerminalPageButton(Button button) {
|
||||||
if (button != null) {
|
if (button != null) {
|
||||||
button.setText("‹ " + accessoryPageName() + " ›");
|
button.setText(accessoryPageName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2708,7 +2786,7 @@ public final class MainActivity extends Activity {
|
|||||||
input.setTextColor(COLOR_TEXT);
|
input.setTextColor(COLOR_TEXT);
|
||||||
input.setHintTextColor(COLOR_TEXT_DIM);
|
input.setHintTextColor(COLOR_TEXT_DIM);
|
||||||
input.setTextSize(14);
|
input.setTextSize(14);
|
||||||
input.setPadding(dp(10), dp(8), dp(10), dp(8));
|
input.setPadding(dp(9), dp(6), dp(9), dp(6));
|
||||||
input.setBackground(inputBackground());
|
input.setBackground(inputBackground());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2770,18 +2848,18 @@ public final class MainActivity extends Activity {
|
|||||||
|
|
||||||
private void addSoftButton(LinearLayout row, String label, View.OnClickListener listener) {
|
private void addSoftButton(LinearLayout row, String label, View.OnClickListener listener) {
|
||||||
Button button = toolbarButton(label, listener);
|
Button button = toolbarButton(label, listener);
|
||||||
button.setTextSize(isArrowLabel(label) ? 17 : 10);
|
button.setTextSize(isArrowLabel(label) ? 15 : 9);
|
||||||
button.setPadding(dp(5), 0, dp(5), 0);
|
button.setPadding(dp(4), 0, dp(4), 0);
|
||||||
int width = "Space".equals(label) ? 64 : (isArrowLabel(label) ? 38 : 44);
|
int width = "Space".equals(label) ? 56 : (isArrowLabel(label) ? 32 : 38);
|
||||||
button.setMinWidth(dp(width));
|
button.setMinWidth(dp(width));
|
||||||
button.setMinimumWidth(dp(width));
|
button.setMinimumWidth(dp(width));
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
dp(27)
|
||||||
);
|
);
|
||||||
params.leftMargin = dp(2);
|
params.leftMargin = dp(1);
|
||||||
params.rightMargin = dp(2);
|
params.rightMargin = dp(1);
|
||||||
params.topMargin = dp(1);
|
params.topMargin = dp(2);
|
||||||
params.bottomMargin = dp(1);
|
params.bottomMargin = dp(1);
|
||||||
row.addView(button, params);
|
row.addView(button, params);
|
||||||
}
|
}
|
||||||
@@ -2914,9 +2992,53 @@ public final class MainActivity extends Activity {
|
|||||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentTerminalConnection(String sessionName, int generation) {
|
||||||
|
return !activityDestroyed
|
||||||
|
&& generation == terminalConnectionGeneration
|
||||||
|
&& sessionName.equals(activeSessionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleTerminalReconnect(String sessionName, int generation) {
|
||||||
|
if (!isCurrentTerminalConnection(sessionName, generation) || terminalReconnectTask != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long delay = reconnectDelay(terminalReconnectAttempt++);
|
||||||
|
terminalSocketStatus = "retry in " + Math.max(1L, delay / 1000L) + "s";
|
||||||
|
updateTerminalMeta();
|
||||||
|
setStatus("Terminal reconnecting in " + Math.max(1L, delay / 1000L) + "s");
|
||||||
|
terminalReconnectTask = () -> {
|
||||||
|
terminalReconnectTask = null;
|
||||||
|
if (isCurrentTerminalConnection(sessionName, generation)) {
|
||||||
|
connectTerminalSocket(sessionName, generation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mainHandler.postDelayed(terminalReconnectTask, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelTerminalReconnect() {
|
||||||
|
if (terminalReconnectTask != null) {
|
||||||
|
mainHandler.removeCallbacks(terminalReconnectTask);
|
||||||
|
terminalReconnectTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long reconnectDelay(int attempt) {
|
||||||
|
return SOCKET_RECONNECT_DELAYS_MS[Math.min(attempt, SOCKET_RECONNECT_DELAYS_MS.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
private void closeTerminalSocket() {
|
private void closeTerminalSocket() {
|
||||||
|
closeTerminalSocket(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeTerminalSocket(boolean invalidateConnection) {
|
||||||
|
if (invalidateConnection) {
|
||||||
|
terminalConnectionGeneration++;
|
||||||
|
terminalReconnectAttempt = 0;
|
||||||
|
cancelTerminalReconnect();
|
||||||
|
queuedTerminalInput.setLength(0);
|
||||||
|
}
|
||||||
terminalConnected = false;
|
terminalConnected = false;
|
||||||
queuedTerminalInput.setLength(0);
|
terminalConnecting = false;
|
||||||
terminalRenderPending = false;
|
terminalRenderPending = false;
|
||||||
if (terminalSocket != null) {
|
if (terminalSocket != null) {
|
||||||
terminalSocket.close();
|
terminalSocket.close();
|
||||||
@@ -2925,25 +3047,69 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void connectAppEvents() {
|
private void connectAppEvents() {
|
||||||
|
eventReconnectAttempt = 0;
|
||||||
|
eventConnectionGeneration++;
|
||||||
|
cancelEventReconnect();
|
||||||
if (eventSocket != null) {
|
if (eventSocket != null) {
|
||||||
eventSocket.close();
|
eventSocket.close();
|
||||||
|
eventSocket = null;
|
||||||
|
}
|
||||||
|
connectAppEventSocket(api.getBaseUrl(), eventConnectionGeneration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectAppEventSocket(String baseUrl, int generation) {
|
||||||
|
if (activityDestroyed || generation != eventConnectionGeneration) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
eventSocket = new AppEventSocketClient(new AppEventSocketClient.Listener() {
|
eventSocket = new AppEventSocketClient(new AppEventSocketClient.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(String text) {
|
public void onMessage(String text) {
|
||||||
runOnUiThread(() -> handleAppEvent(text));
|
runOnUiThread(() -> {
|
||||||
|
if (generation == eventConnectionGeneration && !activityDestroyed) {
|
||||||
|
handleAppEvent(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClosed() {
|
public void onClosed() {
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
|
if (generation != eventConnectionGeneration || activityDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
terminalEventStatus = "events disconnected";
|
terminalEventStatus = "events disconnected";
|
||||||
updateTerminalMeta();
|
updateTerminalMeta();
|
||||||
setStatus("Event stream disconnected");
|
setStatus("Event stream disconnected");
|
||||||
|
scheduleEventReconnect(baseUrl, generation);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventSocket.connect(api.getBaseUrl());
|
eventSocket.connect(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleEventReconnect(String baseUrl, int generation) {
|
||||||
|
if (activityDestroyed
|
||||||
|
|| generation != eventConnectionGeneration
|
||||||
|
|| eventReconnectTask != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long delay = reconnectDelay(eventReconnectAttempt++);
|
||||||
|
terminalEventStatus = "events retry in " + Math.max(1L, delay / 1000L) + "s";
|
||||||
|
updateTerminalMeta();
|
||||||
|
eventReconnectTask = () -> {
|
||||||
|
eventReconnectTask = null;
|
||||||
|
if (!activityDestroyed && generation == eventConnectionGeneration) {
|
||||||
|
connectAppEventSocket(baseUrl, generation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mainHandler.postDelayed(eventReconnectTask, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelEventReconnect() {
|
||||||
|
if (eventReconnectTask != null) {
|
||||||
|
mainHandler.removeCallbacks(eventReconnectTask);
|
||||||
|
eventReconnectTask = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAppEvent(String text) {
|
private void handleAppEvent(String text) {
|
||||||
@@ -2951,6 +3117,7 @@ public final class MainActivity extends Activity {
|
|||||||
JSONObject event = new JSONObject(text);
|
JSONObject event = new JSONObject(text);
|
||||||
String type = event.optString("type", "");
|
String type = event.optString("type", "");
|
||||||
if ("hello".equals(type)) {
|
if ("hello".equals(type)) {
|
||||||
|
eventReconnectAttempt = 0;
|
||||||
terminalEventStatus = "events connected";
|
terminalEventStatus = "events connected";
|
||||||
updateTerminalMeta();
|
updateTerminalMeta();
|
||||||
setStatus("Event stream connected");
|
setStatus("Event stream connected");
|
||||||
@@ -3043,11 +3210,23 @@ public final class MainActivity extends Activity {
|
|||||||
if (updateManager != null) {
|
if (updateManager != null) {
|
||||||
updateManager.resumePendingInstall();
|
updateManager.resumePendingInstall();
|
||||||
}
|
}
|
||||||
|
if (activeSessionName != null
|
||||||
|
&& !terminalConnected
|
||||||
|
&& !terminalConnecting
|
||||||
|
&& terminalReconnectTask == null) {
|
||||||
|
scheduleTerminalReconnect(activeSessionName, terminalConnectionGeneration);
|
||||||
|
}
|
||||||
|
if ((eventSocket == null || eventSocket.isClosed()) && eventReconnectTask == null) {
|
||||||
|
connectAppEvents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
activityDestroyed = true;
|
||||||
closeTerminalSocket();
|
closeTerminalSocket();
|
||||||
|
eventConnectionGeneration++;
|
||||||
|
cancelEventReconnect();
|
||||||
if (eventSocket != null) {
|
if (eventSocket != null) {
|
||||||
eventSocket.close();
|
eventSocket.close();
|
||||||
eventSocket = null;
|
eventSocket = null;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.json.JSONObject;
|
|||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -19,6 +20,10 @@ import java.util.concurrent.RejectedExecutionException;
|
|||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
final class TerminalSocketClient {
|
final class TerminalSocketClient {
|
||||||
|
private static final long HEARTBEAT_INTERVAL_MS = 15000L;
|
||||||
|
private static final int SOCKET_CONNECT_TIMEOUT_MS = 10000;
|
||||||
|
private static final int SOCKET_READ_TIMEOUT_MS = 45000;
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
void onConnected();
|
void onConnected();
|
||||||
void onOutput(String data);
|
void onOutput(String data);
|
||||||
@@ -35,6 +40,7 @@ final class TerminalSocketClient {
|
|||||||
private BufferedOutputStream output;
|
private BufferedOutputStream output;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
|
private Thread heartbeatThread;
|
||||||
|
|
||||||
TerminalSocketClient(Listener listener) {
|
TerminalSocketClient(Listener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
@@ -97,6 +103,7 @@ final class TerminalSocketClient {
|
|||||||
"rows", rows
|
"rows", rows
|
||||||
);
|
);
|
||||||
listener.onConnected();
|
listener.onConnected();
|
||||||
|
startHeartbeat();
|
||||||
readLoop();
|
readLoop();
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
@@ -123,10 +130,39 @@ final class TerminalSocketClient {
|
|||||||
if (port == -1) {
|
if (port == -1) {
|
||||||
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
||||||
}
|
}
|
||||||
|
Socket raw = new Socket();
|
||||||
|
raw.connect(new InetSocketAddress(uri.getHost(), port), SOCKET_CONNECT_TIMEOUT_MS);
|
||||||
|
Socket connected;
|
||||||
if ("wss".equalsIgnoreCase(uri.getScheme())) {
|
if ("wss".equalsIgnoreCase(uri.getScheme())) {
|
||||||
return SSLSocketFactory.getDefault().createSocket(uri.getHost(), port);
|
connected = ((SSLSocketFactory) SSLSocketFactory.getDefault())
|
||||||
|
.createSocket(raw, uri.getHost(), port, true);
|
||||||
|
} else {
|
||||||
|
connected = raw;
|
||||||
}
|
}
|
||||||
return new Socket(uri.getHost(), port);
|
connected.setKeepAlive(true);
|
||||||
|
connected.setTcpNoDelay(true);
|
||||||
|
connected.setSoTimeout(SOCKET_READ_TIMEOUT_MS);
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startHeartbeat() {
|
||||||
|
heartbeatThread = new Thread(() -> {
|
||||||
|
while (!closed) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(HEARTBEAT_INTERVAL_MS);
|
||||||
|
if (!closed) {
|
||||||
|
sendFrame(9, new byte[0]);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return;
|
||||||
|
} catch (Exception error) {
|
||||||
|
closeSocketQuietly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "terminal-ws-heartbeat");
|
||||||
|
heartbeatThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handshake(URI uri) throws Exception {
|
private void handshake(URI uri) throws Exception {
|
||||||
@@ -279,6 +315,10 @@ final class TerminalSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void closeSocketQuietly() {
|
private void closeSocketQuietly() {
|
||||||
|
if (heartbeatThread != null) {
|
||||||
|
heartbeatThread.interrupt();
|
||||||
|
heartbeatThread = null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (socket != null) {
|
if (socket != null) {
|
||||||
socket.close();
|
socket.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user