mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
530 lines
25 KiB
Java
530 lines
25 KiB
Java
package top.mobilegl.plugin;
|
|
|
|
import android.app.Activity;
|
|
import android.app.ActivityManager;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.graphics.Typeface;
|
|
import android.net.LocalSocket;
|
|
import android.net.LocalSocketAddress;
|
|
import android.os.Bundle;
|
|
import android.text.InputType;
|
|
import android.util.Log;
|
|
import android.util.TypedValue;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.Button;
|
|
import android.widget.CheckBox;
|
|
import android.widget.EditText;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.ScrollView;
|
|
import android.widget.TextView;
|
|
|
|
import java.io.File;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
|
|
/**
|
|
* Control screen for the render server, in either shape: the offscreen supervisor
|
|
* ({@link MobileGLServerService}) or the on-screen display server
|
|
* ({@link MobileGLDisplayActivity}), which renders a client's window surface onto this
|
|
* screen. Collects the shared {@code listen}/{@code token}/{@code env} extras and starts
|
|
* or stops the selected one; no native code of its own. The endpoint is a tcp://host:port
|
|
* for remote clients, or an @abstract name / filesystem path for on-device ones. One
|
|
* server per device (P12 D8): the two shapes replace each other.
|
|
*/
|
|
public final class ServerControlActivity extends Activity {
|
|
private static final String TAG = "MobileGLServerCtl";
|
|
|
|
private static final int COLOR_BACKGROUND = 0xFF121212;
|
|
private static final int COLOR_TEXT = 0xFFEEEEEE;
|
|
private static final int COLOR_PASS = 0xFF4CAF50;
|
|
private static final int COLOR_WARN = 0xFFFF9800;
|
|
private static final int COLOR_FAIL = 0xFFF44336;
|
|
private static final int COLOR_INFO = 0xFF9E9E9E;
|
|
|
|
private static final String DEFAULT_ENDPOINT = "tcp://0.0.0.0:40613";
|
|
private static final int MIN_TOKEN_BYTES = 16;
|
|
private static final int PROBE_ATTEMPTS = 10;
|
|
private static final long PROBE_INTERVAL_MS = 500;
|
|
private static final int PROBE_TIMEOUT_MS = 500;
|
|
|
|
private EditText listenField;
|
|
private EditText tokenField;
|
|
private EditText envField;
|
|
private CheckBox onScreenBox;
|
|
private Button startButton;
|
|
private Button stopButton;
|
|
private TextView statusView;
|
|
private volatile Thread probeThread;
|
|
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
|
|
LinearLayout contentLayout = new LinearLayout(this);
|
|
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
|
int padding = dp(16);
|
|
contentLayout.setPadding(padding, padding, padding, padding);
|
|
|
|
ScrollView scrollView = new ScrollView(this);
|
|
scrollView.setBackgroundColor(COLOR_BACKGROUND);
|
|
scrollView.setFillViewport(true);
|
|
scrollView.addView(contentLayout);
|
|
setContentView(NavBar.wrap(this, scrollView, NavBar.TAB_SERVER));
|
|
|
|
contentLayout.addView(makeText("MobileGL Render Server", 20, COLOR_TEXT, true));
|
|
|
|
TextView hint = makeText(
|
|
"Starts the render server. Offscreen (default): the supervisor "
|
|
+ "(libMobileGLServer.so --serve) runs as a foreground service. On-screen "
|
|
+ "window: the server runs in a display Activity that renders the client's "
|
|
+ "frames onto this screen (clients opt in with MOBILEGL_IPC_SURFACE=server). "
|
|
+ "Endpoint forms: tcp://host:port for remote clients, @name for an "
|
|
+ "on-device abstract socket, or a filesystem path (relative to the app's "
|
|
+ "files dir) for on-device clients. Non-loopback TCP requires a token of at "
|
|
+ "least " + MIN_TOKEN_BYTES + " bytes; unix endpoints need none, but GL programs "
|
|
+ "from other apps (Termux, adb shell) reach the server through its broker, which "
|
|
+ "needs one (see the launch command below). Extra env "
|
|
+ "uses the KEY=VALUE;KEY grammar (e.g. MOBILEGL_BACKEND_TYPE=DirectVulkan). "
|
|
+ "One server per device: the two modes replace each other.",
|
|
12, COLOR_INFO, false);
|
|
LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
hintParams.topMargin = dp(8);
|
|
contentLayout.addView(hint, hintParams);
|
|
|
|
listenField = addField(contentLayout, "Listen endpoint", DEFAULT_ENDPOINT, dp(16));
|
|
tokenField = addField(contentLayout, "Auth token (empty = loopback only)", "", dp(8));
|
|
envField = addField(contentLayout, "Extra env (optional)", "", dp(8));
|
|
|
|
onScreenBox = new CheckBox(this);
|
|
onScreenBox.setText("On-screen window (render client frames onto this screen)");
|
|
onScreenBox.setTextColor(COLOR_TEXT);
|
|
onScreenBox.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
|
LinearLayout.LayoutParams boxParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
boxParams.topMargin = dp(8);
|
|
contentLayout.addView(onScreenBox, boxParams);
|
|
|
|
LinearLayout buttons = new LinearLayout(this);
|
|
buttons.setOrientation(LinearLayout.HORIZONTAL);
|
|
LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
buttonsParams.topMargin = dp(16);
|
|
contentLayout.addView(buttons, buttonsParams);
|
|
|
|
startButton = new Button(this);
|
|
startButton.setText("Start server");
|
|
startButton.setAllCaps(false);
|
|
startButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
|
startButton.setOnClickListener(v -> startServer());
|
|
buttons.addView(startButton);
|
|
|
|
stopButton = new Button(this);
|
|
stopButton.setText("Stop server");
|
|
stopButton.setAllCaps(false);
|
|
stopButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
|
LinearLayout.LayoutParams stopParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
stopParams.leftMargin = dp(8);
|
|
buttons.addView(stopButton, stopParams);
|
|
stopButton.setOnClickListener(v -> stopServer());
|
|
|
|
statusView = makeText("", 13, COLOR_INFO, false);
|
|
LinearLayout.LayoutParams statusParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
statusParams.topMargin = dp(12);
|
|
contentLayout.addView(statusView, statusParams);
|
|
|
|
addExternalClientSection(contentLayout);
|
|
|
|
if (isServerRunning()) {
|
|
setStatus("A server is running (started outside this screen).", COLOR_WARN);
|
|
} else {
|
|
setStatus("Stopped.", COLOR_INFO);
|
|
}
|
|
|
|
handleIntent(getIntent());
|
|
}
|
|
|
|
@Override
|
|
protected void onNewIntent(Intent intent) {
|
|
super.onNewIntent(intent);
|
|
setIntent(intent);
|
|
handleIntent(intent);
|
|
}
|
|
|
|
/**
|
|
* Intent extras prefill the form, so adb can drive the screen without the IME:
|
|
* --es listen @name --es token T --es env "K=V;K" --ez onscreen true --ez start true
|
|
* (--ez stop true instead stops). This mirrors how every other entry point in this
|
|
* APK is already driven by extras.
|
|
*/
|
|
private void handleIntent(Intent intent) {
|
|
if (intent != null) {
|
|
String prefillListen = intent.getStringExtra("listen");
|
|
if (prefillListen != null) listenField.setText(prefillListen);
|
|
String prefillToken = intent.getStringExtra("token");
|
|
if (prefillToken != null) tokenField.setText(prefillToken);
|
|
String prefillEnv = intent.getStringExtra("env");
|
|
if (prefillEnv != null) envField.setText(prefillEnv);
|
|
if (intent.getBooleanExtra("onscreen", false)) onScreenBox.setChecked(true);
|
|
if (intent.getBooleanExtra("stop", false)) stopServer();
|
|
else if (intent.getBooleanExtra("start", false)) startServer();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
Thread probe = probeThread;
|
|
if (probe != null) probe.interrupt();
|
|
super.onDestroy();
|
|
}
|
|
|
|
private void startServer() {
|
|
String endpoint = listenField.getText().toString().trim();
|
|
String token = tokenField.getText().toString();
|
|
String env = envField.getText().toString().trim();
|
|
|
|
String probeDisplay;
|
|
Probe probe;
|
|
if (endpoint.startsWith("tcp://")) {
|
|
String[] hostPort = parseTcpEndpoint(endpoint);
|
|
if (hostPort == null) {
|
|
setStatus("Invalid TCP endpoint: expected tcp://host:port", COLOR_FAIL);
|
|
return;
|
|
}
|
|
if (!isLoopback(hostPort[0]) && token.getBytes().length < MIN_TOKEN_BYTES) {
|
|
setStatus("Non-loopback listen requires a token of at least "
|
|
+ MIN_TOKEN_BYTES + " bytes.", COLOR_FAIL);
|
|
return;
|
|
}
|
|
final String probeHost = isWildcard(hostPort[0]) ? "127.0.0.1" : hostPort[0];
|
|
final int port;
|
|
try {
|
|
port = Integer.parseInt(hostPort[1]);
|
|
} catch (NumberFormatException error) {
|
|
setStatus("Invalid port in endpoint.", COLOR_FAIL);
|
|
return;
|
|
}
|
|
probeDisplay = probeHost + ":" + port;
|
|
probe = () -> {
|
|
try (Socket socket = new Socket()) {
|
|
socket.connect(new InetSocketAddress(probeHost, port), PROBE_TIMEOUT_MS);
|
|
}
|
|
};
|
|
} else {
|
|
String invalid = validateUnixEndpoint(endpoint);
|
|
if (invalid != null) {
|
|
setStatus(invalid, COLOR_FAIL);
|
|
return;
|
|
}
|
|
final boolean abstractName = endpoint.startsWith("@");
|
|
final String address = abstractName ? endpoint.substring(1)
|
|
: endpoint.startsWith("/") ? endpoint
|
|
: new File(getFilesDir(), endpoint).getAbsolutePath();
|
|
final LocalSocketAddress.Namespace namespace = abstractName
|
|
? LocalSocketAddress.Namespace.ABSTRACT : LocalSocketAddress.Namespace.FILESYSTEM;
|
|
probeDisplay = endpoint;
|
|
probe = () -> {
|
|
LocalSocket socket = new LocalSocket();
|
|
try {
|
|
socket.connect(new LocalSocketAddress(address, namespace));
|
|
} finally {
|
|
socket.close();
|
|
}
|
|
};
|
|
}
|
|
if (isServerRunning()) {
|
|
setStatus("A server is already running; stop it before reconfiguring.", COLOR_WARN);
|
|
return;
|
|
}
|
|
|
|
final boolean onScreen = onScreenBox.isChecked();
|
|
Intent intent = new Intent(this,
|
|
onScreen ? MobileGLDisplayActivity.class : MobileGLServerService.class);
|
|
intent.putExtra("listen", endpoint);
|
|
intent.putExtra("token", token);
|
|
if (!env.isEmpty()) intent.putExtra("env", env);
|
|
try {
|
|
// The display Activity runs the server in its own process (:mglwin) and shows the
|
|
// client's window surface; launching it brings that window to the front.
|
|
if (onScreen) startActivity(intent);
|
|
else startForegroundService(intent);
|
|
} catch (RuntimeException error) {
|
|
Log.e(TAG, "cannot start the render server", error);
|
|
setStatus("start failed: " + error, COLOR_FAIL);
|
|
return;
|
|
}
|
|
setStatus("Starting " + endpoint + (onScreen ? " (on-screen)" : "") + " ...", COLOR_INFO);
|
|
probeUntilListening(probeDisplay, probe);
|
|
}
|
|
|
|
/**
|
|
* unix endpoint check: an @abstract name or a filesystem path (relative paths resolve
|
|
* against the app's files dir, which is the service's working directory). Returns the
|
|
* problem, or null when the endpoint is usable. No token: an AF_UNIX address is reachable
|
|
* only from this device, and an abstract name only from this app.
|
|
*/
|
|
private String validateUnixEndpoint(String endpoint) {
|
|
if (endpoint.isEmpty()) {
|
|
return "Endpoint is empty.";
|
|
}
|
|
if (endpoint.contains("://")) {
|
|
return "Unknown endpoint scheme; use tcp://host:port, @name or a filesystem path.";
|
|
}
|
|
String resolved = endpoint.startsWith("@") || endpoint.startsWith("/") ? endpoint
|
|
: new File(getFilesDir(), endpoint).getAbsolutePath();
|
|
// sun_path is 108 bytes on Linux/Android, and an abstract name drops the '@' into it.
|
|
if (resolved.getBytes().length >= 108) {
|
|
return "Endpoint is too long for an AF_UNIX address.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void stopServer() {
|
|
Thread probe = probeThread;
|
|
if (probe != null) probe.interrupt();
|
|
boolean stopped = stopService(new Intent(this, MobileGLServerService.class));
|
|
boolean displayKilled = killDisplayServer();
|
|
setStatus(stopped || displayKilled ? "Stopped." : "Stop requested, but no server was running.",
|
|
stopped || displayKilled ? COLOR_INFO : COLOR_WARN);
|
|
}
|
|
|
|
/**
|
|
* Kills the on-screen server's process (:mglwin). Same uid, so killProcess is allowed;
|
|
* this mirrors MobileGLServerService.stopDisplayServer for the reverse direction.
|
|
*/
|
|
private boolean killDisplayServer() {
|
|
ActivityManager activities = getSystemService(ActivityManager.class);
|
|
String displayProcess = getPackageName() + ":mglwin";
|
|
List<ActivityManager.RunningAppProcessInfo> processes = activities.getRunningAppProcesses();
|
|
if (processes == null) return false;
|
|
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
|
if (displayProcess.equals(process.processName)) {
|
|
android.os.Process.killProcess(process.pid);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Confirms the supervisor bound its socket: connect to the endpoint until one attempt
|
|
* succeeds. TCP probes go to loopback when the server listens on a wildcard address;
|
|
* unix probes use LocalSocket. The server closes each unauthenticated probe after its
|
|
* pre-auth timeout; at most {@link #PROBE_ATTEMPTS} probes are made, then the screen
|
|
* reports whatever the service state is.
|
|
*/
|
|
private interface Probe {
|
|
void connect() throws Exception;
|
|
}
|
|
|
|
private void probeUntilListening(String display, Probe probe) {
|
|
Thread thread = new Thread(() -> {
|
|
for (int attempt = 1; attempt <= PROBE_ATTEMPTS; ++attempt) {
|
|
if (Thread.currentThread().isInterrupted()) return;
|
|
try {
|
|
probe.connect();
|
|
runOnUiThread(() -> setStatus("Listening on " + display, COLOR_PASS));
|
|
return;
|
|
} catch (Exception refused) {
|
|
try {
|
|
Thread.sleep(PROBE_INTERVAL_MS);
|
|
} catch (InterruptedException stopped) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
runOnUiThread(() -> {
|
|
if (isServerRunning()) {
|
|
setStatus("Server running but " + display + " did not accept a probe; "
|
|
+ "see logcat tags MobileGLServer/MobileGLDisplay.", COLOR_WARN);
|
|
} else {
|
|
setStatus("Server exited; see logcat tags MobileGLServer/MobileGLDisplay.",
|
|
COLOR_FAIL);
|
|
}
|
|
});
|
|
}, "mgl-server-probe");
|
|
probeThread = thread;
|
|
thread.start();
|
|
}
|
|
|
|
/** True when either server shape is up: the supervisor service or the :mglwin process. */
|
|
private boolean isServerRunning() {
|
|
ActivityManager activities = getSystemService(ActivityManager.class);
|
|
List<ActivityManager.RunningServiceInfo> services =
|
|
activities.getRunningServices(Integer.MAX_VALUE);
|
|
if (services != null) {
|
|
for (ActivityManager.RunningServiceInfo service : services) {
|
|
if (MobileGLServerService.class.getName().equals(service.service.getClassName())) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
String displayProcess = getPackageName() + ":mglwin";
|
|
List<ActivityManager.RunningAppProcessInfo> processes = activities.getRunningAppProcesses();
|
|
if (processes != null) {
|
|
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
|
if (displayProcess.equals(process.processName)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---- P11 B1 (CONTRACT-P11 B1): the launch command for same-device external clients ----------
|
|
//
|
|
// A GL program started from another app (Termux, adb shell) cannot connect to this app's unix
|
|
// endpoint (SELinux), so it runs ExternalClientHelper under app_process with THIS APK as its class
|
|
// path; the helper asks ExternalClientBroker for a connected pair and execs the program on it. The
|
|
// program's side needs three facts it cannot look up itself - package visibility filters `pm path`
|
|
// for an app, and a native program has no Context - so this screen prints them: the APK path, the
|
|
// helper class, the broker component, and how the token travels (the environment, never argv).
|
|
|
|
private TextView launchCommandView;
|
|
|
|
private void addExternalClientSection(LinearLayout parent) {
|
|
TextView title = makeText("Same-device GL programs (Termux, adb shell, other apps)", 14, COLOR_TEXT, true);
|
|
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
titleParams.topMargin = dp(20);
|
|
parent.addView(title, titleParams);
|
|
|
|
launchCommandView = makeText("", 12, COLOR_INFO, false);
|
|
launchCommandView.setTextIsSelectable(true);
|
|
LinearLayout.LayoutParams commandParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
commandParams.topMargin = dp(8);
|
|
parent.addView(launchCommandView, commandParams);
|
|
|
|
LinearLayout row = new LinearLayout(this);
|
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
rowParams.topMargin = dp(8);
|
|
parent.addView(row, rowParams);
|
|
|
|
Button generate = new Button(this);
|
|
generate.setText("Generate token");
|
|
generate.setAllCaps(false);
|
|
generate.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
|
generate.setOnClickListener(v -> {
|
|
byte[] bytes = new byte[16];
|
|
new java.security.SecureRandom().nextBytes(bytes);
|
|
StringBuilder hex = new StringBuilder();
|
|
for (byte b : bytes) hex.append(String.format(Locale.ROOT, "%02x", b & 0xff));
|
|
tokenField.setText(hex.toString());
|
|
});
|
|
row.addView(generate);
|
|
|
|
Button copy = new Button(this);
|
|
copy.setText("Copy launch command");
|
|
copy.setAllCaps(false);
|
|
copy.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
|
LinearLayout.LayoutParams copyParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
copyParams.leftMargin = dp(8);
|
|
row.addView(copy, copyParams);
|
|
copy.setOnClickListener(v -> {
|
|
android.content.ClipboardManager clipboard = getSystemService(android.content.ClipboardManager.class);
|
|
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("MobileGL launch command",
|
|
launchCommand(tokenField.getText().toString())));
|
|
setStatus("Launch command copied (it carries the token: paste it only where you run the program).",
|
|
COLOR_INFO);
|
|
});
|
|
|
|
tokenField.addTextChangedListener(new android.text.TextWatcher() {
|
|
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
|
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
|
@Override public void afterTextChanged(android.text.Editable s) { refreshLaunchCommand(); }
|
|
});
|
|
refreshLaunchCommand();
|
|
}
|
|
|
|
/** The one line a user pastes before their program: token in the environment, the APK as class path. */
|
|
private String launchCommand(String token) {
|
|
return "MOBILEGL_IPC_TOKEN='" + token + "' CLASSPATH=" + getApplicationInfo().sourceDir
|
|
+ " app_process / " + ExternalClientHelper.class.getName() + " -- <program> [args...]";
|
|
}
|
|
|
|
private void refreshLaunchCommand() {
|
|
if (launchCommandView == null) return;
|
|
String token = tokenField.getText().toString();
|
|
boolean usable = token.getBytes().length >= ExternalClientBroker.MIN_TOKEN_BYTES;
|
|
StringBuilder text = new StringBuilder();
|
|
if (!usable) {
|
|
text.append("Needs an Auth token of at least ").append(ExternalClientBroker.MIN_TOKEN_BYTES)
|
|
.append(" bytes (Generate token), then start the server offscreen.\n\n");
|
|
}
|
|
text.append(launchCommand(usable ? token : "<token>")).append("\n\n")
|
|
.append("Broker: ").append(getPackageName()).append('/').append(ExternalClientBroker.class.getName())
|
|
.append("\nThe token travels in the environment (MOBILEGL_IPC_TOKEN, or MOBILEGL_IPC_TOKEN_FILE=<file>)")
|
|
.append(", never on the command line. The program runs over shared memory, off the one cpu core")
|
|
.append(" the server's render thread takes (--keep-affinity to opt out); the server keeps")
|
|
.append(" running as a foreground service. Battery: set both this app and the client's app to")
|
|
.append(" \"No restrictions\".");
|
|
launchCommandView.setText(text.toString());
|
|
}
|
|
|
|
/** {host, port} for tcp://host:port, or null. */
|
|
private static String[] parseTcpEndpoint(String endpoint) {
|
|
if (endpoint == null || !endpoint.startsWith("tcp://")) return null;
|
|
String hostPort = endpoint.substring("tcp://".length());
|
|
int separator = hostPort.lastIndexOf(':');
|
|
if (separator <= 0 || separator == hostPort.length() - 1) return null;
|
|
return new String[]{hostPort.substring(0, separator), hostPort.substring(separator + 1)};
|
|
}
|
|
|
|
private static boolean isLoopback(String host) {
|
|
// A wildcard bind is reachable from the network, so the server holds it to the
|
|
// non-loopback token requirement (AuthToken.h) just like a real interface address.
|
|
return host.equals("127.0.0.1") || host.equals("localhost") || host.equals("::1");
|
|
}
|
|
|
|
private static boolean isWildcard(String host) {
|
|
return host.isEmpty() || host.equals("0.0.0.0") || host.equals("::") || host.equals("*");
|
|
}
|
|
|
|
private EditText addField(LinearLayout parent, String label, String initial, int topMarginPx) {
|
|
TextView labelView = makeText(label, 12, COLOR_INFO, false);
|
|
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
labelParams.topMargin = topMarginPx;
|
|
parent.addView(labelView, labelParams);
|
|
|
|
EditText field = new EditText(this);
|
|
field.setText(initial);
|
|
field.setTextColor(COLOR_TEXT);
|
|
field.setHintTextColor(COLOR_INFO);
|
|
field.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
|
field.setTypeface(Typeface.MONOSPACE);
|
|
field.setSingleLine(true);
|
|
field.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
|
field.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
|
parent.addView(field, new LinearLayout.LayoutParams(
|
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
return field;
|
|
}
|
|
|
|
private void setStatus(String message, int color) {
|
|
Log.i(TAG, String.format(Locale.ROOT, "status: %s", message));
|
|
statusView.setText(message);
|
|
statusView.setTextColor(color);
|
|
}
|
|
|
|
private TextView makeText(String text, int sizeSp, int color, boolean bold) {
|
|
TextView view = new TextView(this);
|
|
view.setText(text);
|
|
view.setTextColor(color);
|
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sizeSp);
|
|
view.setTypeface(Typeface.MONOSPACE, bold ? Typeface.BOLD : Typeface.NORMAL);
|
|
return view;
|
|
}
|
|
|
|
private int dp(int value) {
|
|
return Math.round(value * getResources().getDisplayMetrics().density);
|
|
}
|
|
}
|