Improve Tailscale defaults and update UX
Gitea Smoke / smoke (push) Successful in 2s

This commit is contained in:
Codex
2026-07-05 05:25:54 +00:00
parent a4b8f457d2
commit 8709c7bae5
5 changed files with 162 additions and 11 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
val repoSlug = providers.gradleProperty("repoSlug")
.orElse("neatstudio/tmux-android")
val defaultServerUrl = providers.gradleProperty("defaultServerUrl")
.orElse("http://127.0.0.1:3000")
.orElse("http://100.89.0.116:3000")
val defaultUpdateUrl = providers.gradleProperty("defaultUpdateUrl")
.orElse("https://github.com/${repoSlug.get()}/releases/latest/download/latest.json")
+1
View File
@@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
@@ -1,5 +1,6 @@
package com.neatstudio.tmuxandroid;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
@@ -7,6 +8,7 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
@@ -14,6 +16,7 @@ import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.InputType;
import android.view.Gravity;
import android.view.View;
@@ -45,6 +48,15 @@ public final class MainActivity extends Activity {
private static final int TERMINAL_COLS = 96;
private static final int TERMINAL_ROWS = 32;
private static final int MAX_TERMINAL_CHARS = 120_000;
private static final String OLD_LOCAL_DEFAULT_URL = "http://127.0.0.1:3000";
private static final String DEFAULT_TAILSCALE_URL = "http://100.89.0.116:3000";
private static final String[] SERVER_PROFILES = {
"http://100.89.0.116:3000",
"http://100.89.0.2:3000",
"http://100.89.0.4:3000",
"http://100.89.0.9:3000",
"http://100.89.0.11:3000"
};
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private SharedPreferences prefs;
@@ -72,6 +84,14 @@ public final class MainActivity extends Activity {
.putString("server_url", BuildConfig.DEFAULT_SERVER_URL)
.putString("update_url", BuildConfig.DEFAULT_UPDATE_URL)
.apply();
} else if (
OLD_LOCAL_DEFAULT_URL.equals(prefs.getString("server_url", ""))
&& !prefs.getBoolean("tailscale_defaults_applied_v1", false)
) {
prefs.edit()
.putString("server_url", DEFAULT_TAILSCALE_URL)
.putBoolean("tailscale_defaults_applied_v1", true)
.apply();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(Color.rgb(17, 20, 24));
@@ -121,6 +141,10 @@ public final class MainActivity extends Activity {
activeSessionName = null;
root.removeAllViews();
root.addView(createServerBar(), matchWrap());
root.addView(createServerProfileBar(), new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(46)
));
root.addView(progressBar, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(3)
@@ -174,6 +198,47 @@ public final class MainActivity extends Activity {
return toolbar;
}
private HorizontalScrollView createServerProfileBar() {
HorizontalScrollView scroller = new HorizontalScrollView(this);
scroller.setHorizontalScrollBarEnabled(false);
scroller.setBackgroundColor(Color.rgb(22, 27, 34));
LinearLayout row = new LinearLayout(this);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL);
row.setPadding(dp(6), dp(4), dp(6), dp(4));
for (String url : SERVER_PROFILES) {
String label = url.replace("http://", "").replace(":3000", "");
Button button = toolbarButton(label, view -> selectServer(url));
button.setMinWidth(dp(92));
button.setMinimumWidth(dp(92));
row.addView(button, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
}
row.addView(toolbarButton("Custom", view -> {
urlField.requestFocus();
urlField.selectAll();
}));
scroller.addView(row, new HorizontalScrollView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
return scroller;
}
private void selectServer(String url) {
prefs.edit().putString("server_url", url).apply();
api = new SessionApiClient(url);
urlField.setText(url);
setStatus("Selected " + url);
connectAppEvents();
refreshSessions();
}
private void refreshSessions() {
setStatus("Loading sessions from " + api.getBaseUrl());
progressBar.setVisibility(View.VISIBLE);
@@ -369,7 +434,8 @@ public final class MainActivity extends Activity {
"Upload image file",
"Upload image URL",
"Image preview info",
"Open image preview"
"Open image preview",
"Permissions / update status"
};
new AlertDialog.Builder(this)
.setTitle("Native API actions")
@@ -429,6 +495,9 @@ public final class MainActivity extends Activity {
case 17:
promptOpenImagePreview();
break;
case 18:
showPermissionsAndUpdateStatus();
break;
default:
break;
}
@@ -811,6 +880,58 @@ public final class MainActivity extends Activity {
.show();
}
private void showPermissionsAndUpdateStatus() {
StringBuilder text = new StringBuilder();
text.append("Server: ").append(getServerUrl()).append('\n');
text.append("Update manifest: ")
.append(prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL))
.append('\n');
text.append("Network: manifest permission, no runtime grant required\n");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
text.append("Install unknown apps: ")
.append(getPackageManager().canRequestPackageInstalls() ? "allowed" : "not allowed")
.append('\n');
} else {
text.append("Install unknown apps: allowed by Android version\n");
}
if (Build.VERSION.SDK_INT >= 33) {
text.append("Notifications: ")
.append(checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED ? "allowed" : "not allowed")
.append('\n');
} else {
text.append("Notifications: no runtime permission required\n");
}
text.append("SMS: not requested; this tmux client does not need SMS permission.\n");
new AlertDialog.Builder(this)
.setTitle("Permissions / update")
.setMessage(text.toString())
.setNegativeButton("Close", null)
.setNeutralButton("Install permission", (dialog, which) -> openInstallPermissionSettings())
.setPositiveButton("Notify permission", (dialog, which) -> requestNotificationPermission())
.show();
}
private void openInstallPermissionSettings() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
showMessage("Install permission is allowed on this Android version");
return;
}
Intent intent = new Intent(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:" + getPackageName())
);
startActivity(intent);
}
private void requestNotificationPermission() {
if (Build.VERSION.SDK_INT < 33) {
showMessage("Notification permission is not required on this Android version");
return;
}
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 3001);
}
private void showImagePreview(String path, String basePath) {
progressBar.setVisibility(View.VISIBLE);
executor.execute(() -> {
@@ -43,13 +43,15 @@ final class UpdateManager {
void check(boolean userInitiated) {
String manifestUrl = prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL);
callback.onChecking(true);
postMessage("Checking update...");
executor.execute(() -> {
try {
ReleaseInfo info = fetchReleaseInfo(manifestUrl);
if (info.versionCode <= BuildConfig.VERSION_CODE) {
postMessage(userInitiated ? "Already up to date" : null);
postMessage("Already up to date: " + BuildConfig.VERSION_NAME);
return;
}
postMessage("Update found: " + info.versionName);
activity.runOnUiThread(() -> showUpdateDialog(info));
} catch (Exception error) {
postMessage(userInitiated ? "Update check failed: " + error.getMessage() : null);
@@ -95,10 +97,12 @@ final class UpdateManager {
private void downloadAndInstall(ReleaseInfo info) {
callback.onChecking(true);
postMessage("Downloading " + info.versionName + "...");
executor.execute(() -> {
try {
File apk = downloadApk(info);
if (!info.sha256.isEmpty()) {
postMessage("Verifying APK...");
String actual = sha256(apk);
if (!actual.equalsIgnoreCase(info.sha256)) {
throw new IllegalStateException("APK SHA-256 mismatch");
@@ -145,6 +149,7 @@ final class UpdateManager {
);
activity.startActivity(settingsIntent);
Toast.makeText(activity, "Allow installs, then run update again", Toast.LENGTH_LONG).show();
postMessage("Install permission required");
return;
}
@@ -159,6 +164,7 @@ final class UpdateManager {
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
activity.startActivity(intent);
postMessage("Opened Android package installer");
} catch (ActivityNotFoundException error) {
postMessage("No package installer found");
}