From 08637eafa62e76cb70993ef0177db27b50d980d4 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 15:24:21 +0000 Subject: [PATCH] Fix mobile insets and update mirrors --- README.md | 12 +++ app/build.gradle.kts | 3 + .../neatstudio/tmuxandroid/MainActivity.java | 75 +++++++++++++++++-- .../neatstudio/tmuxandroid/UpdateManager.java | 43 ++++++++++- 4 files changed, 123 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5e55294..3f1fc49 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ APIs directly: and kill session through HTTP API - open one live terminal viewer through `/ws/terminal` - native `/ws/events` listener for session invalidation and hook notifications +- GitHub and Gitea update manifest mirrors with fallback - native API action center for health, server status, timeline, preferences, kanban projects, group messages, hook events, image file/URL upload, image preview info, and native image preview display @@ -76,6 +77,8 @@ Publish a test build by pushing a `v*` tag. That creates a GitHub Release with: ```text https://github.com/neatstudio/tmux-browser-android/releases/latest/download/tmux-android.apk https://github.com/neatstudio/tmux-browser-android/releases/latest/download/latest.json +https://gitea.neatcn.com/tmux/tmux-browser-android/releases/latest/download/tmux-android.apk +https://gitea.neatcn.com/tmux/tmux-browser-android/releases/latest/download/latest.json ``` Plain branch builds only create Actions artifacts; they are useful for CI @@ -123,4 +126,13 @@ The default update manifest is: https://github.com/neatstudio/tmux-browser-android/releases/latest/download/latest.json ``` +The Gitea mirror is: + +```text +https://gitea.neatcn.com/tmux/tmux-browser-android/releases/latest/download/latest.json +``` + +The app tries the selected update source first, then falls back to the GitHub +and Gitea mirrors. + The workflow uploads both the APK and `latest.json` to each release. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 646021e..0870e47 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,8 @@ val defaultServerUrl = providers.gradleProperty("defaultServerUrl") .orElse("http://100.89.0.116:3000") val defaultUpdateUrl = providers.gradleProperty("defaultUpdateUrl") .orElse("https://github.com/${repoSlug.get()}/releases/latest/download/latest.json") +val defaultGiteaUpdateUrl = providers.gradleProperty("defaultGiteaUpdateUrl") + .orElse("https://gitea.neatcn.com/tmux/tmux-browser-android/releases/latest/download/latest.json") val signingProps = Properties() val signingFile = rootProject.file("signing.properties") @@ -30,6 +32,7 @@ android { buildConfigField("String", "DEFAULT_SERVER_URL", "\"${defaultServerUrl.get()}\"") buildConfigField("String", "DEFAULT_UPDATE_URL", "\"${defaultUpdateUrl.get()}\"") + buildConfigField("String", "DEFAULT_GITEA_UPDATE_URL", "\"${defaultGiteaUpdateUrl.get()}\"") } buildFeatures { diff --git a/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java b/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java index 20d56c0..7d69fe7 100644 --- a/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java +++ b/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java @@ -21,6 +21,7 @@ import android.text.InputType; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; @@ -133,9 +134,24 @@ public final class MainActivity extends Activity { statusText.setBackgroundColor(Color.rgb(29, 34, 41)); statusText.setSingleLine(true); setStatus("Ready"); + applySystemBarInsets(root); return root; } + private void applySystemBarInsets(View view) { + view.setOnApplyWindowInsetsListener((target, insets) -> { + target.setPadding( + 0, + insets.getSystemWindowInsetTop(), + 0, + insets.getSystemWindowInsetBottom() + ); + return insets; + }); + view.requestApplyInsets(); + view.post(view::requestApplyInsets); + } + private void renderSessionScreen() { closeTerminalSocket(); activeSessionName = null; @@ -169,11 +185,14 @@ public final class MainActivity extends Activity { private LinearLayout createServerBar() { LinearLayout toolbar = new LinearLayout(this); - toolbar.setOrientation(LinearLayout.HORIZONTAL); - toolbar.setGravity(Gravity.CENTER_VERTICAL); + toolbar.setOrientation(LinearLayout.VERTICAL); toolbar.setPadding(dp(8), dp(6), dp(8), dp(6)); toolbar.setBackgroundColor(Color.rgb(17, 20, 24)); + LinearLayout urlRow = new LinearLayout(this); + urlRow.setOrientation(LinearLayout.HORIZONTAL); + urlRow.setGravity(Gravity.CENTER_VERTICAL); + urlField = new EditText(this); urlField.setSingleLine(true); urlField.setTextColor(Color.WHITE); @@ -190,11 +209,20 @@ public final class MainActivity extends Activity { } return false; }); - toolbar.addView(urlField, new LinearLayout.LayoutParams(0, dp(42), 1)); - toolbar.addView(toolbarButton("Go", view -> saveServerAndRefresh())); - toolbar.addView(toolbarButton("New", view -> promptCreateSession())); - toolbar.addView(toolbarButton("Update", view -> updateManager.check(true))); - toolbar.addView(toolbarButton("More", view -> showMainActions())); + urlRow.addView(urlField, new LinearLayout.LayoutParams(0, dp(44), 1)); + urlRow.addView(toolbarButton("Go", view -> saveServerAndRefresh())); + + LinearLayout actionRow = new LinearLayout(this); + actionRow.setOrientation(LinearLayout.HORIZONTAL); + actionRow.setGravity(Gravity.CENTER_VERTICAL); + actionRow.setPadding(0, dp(6), 0, 0); + actionRow.addView(toolbarButton("New", view -> promptCreateSession())); + actionRow.addView(toolbarButton("Refresh", view -> refreshSessions())); + actionRow.addView(toolbarButton("Update", view -> updateManager.check(true))); + actionRow.addView(toolbarButton("More", view -> showMainActions())); + + toolbar.addView(urlRow, matchWrap()); + toolbar.addView(actionRow, matchWrap()); return toolbar; } @@ -435,6 +463,7 @@ public final class MainActivity extends Activity { "Upload image URL", "Image preview info", "Open image preview", + "Update source", "Permissions / update status" }; new AlertDialog.Builder(this) @@ -496,6 +525,9 @@ public final class MainActivity extends Activity { promptOpenImagePreview(); break; case 18: + showUpdateSourcePicker(); + break; + case 19: showPermissionsAndUpdateStatus(); break; default: @@ -883,9 +915,11 @@ public final class MainActivity extends Activity { private void showPermissionsAndUpdateStatus() { StringBuilder text = new StringBuilder(); text.append("Server: ").append(getServerUrl()).append('\n'); - text.append("Update manifest: ") + text.append("Selected update manifest: ") .append(prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL)) .append('\n'); + text.append("GitHub mirror: ").append(BuildConfig.DEFAULT_UPDATE_URL).append('\n'); + text.append("Gitea mirror: ").append(BuildConfig.DEFAULT_GITEA_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: ") @@ -912,6 +946,31 @@ public final class MainActivity extends Activity { .show(); } + private void showUpdateSourcePicker() { + String[] items = { + "GitHub: " + BuildConfig.DEFAULT_UPDATE_URL, + "Gitea: " + BuildConfig.DEFAULT_GITEA_UPDATE_URL, + "Custom URL" + }; + new AlertDialog.Builder(this) + .setTitle("Update source") + .setItems(items, (dialog, which) -> { + if (which == 0) { + setUpdateUrl(BuildConfig.DEFAULT_UPDATE_URL); + } else if (which == 1) { + setUpdateUrl(BuildConfig.DEFAULT_GITEA_UPDATE_URL); + } else { + promptText("Custom update manifest", "https://.../latest.json", prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL), this::setUpdateUrl); + } + }) + .show(); + } + + private void setUpdateUrl(String url) { + prefs.edit().putString("update_url", url).apply(); + showMessage("Update source: " + url); + } + private void openInstallPermissionSettings() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { showMessage("Install permission is allowed on this Android version"); diff --git a/app/src/main/java/com/neatstudio/tmuxandroid/UpdateManager.java b/app/src/main/java/com/neatstudio/tmuxandroid/UpdateManager.java index c6e0dba..f128ecd 100644 --- a/app/src/main/java/com/neatstudio/tmuxandroid/UpdateManager.java +++ b/app/src/main/java/com/neatstudio/tmuxandroid/UpdateManager.java @@ -19,6 +19,9 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -41,12 +44,12 @@ final class UpdateManager { } void check(boolean userInitiated) { - String manifestUrl = prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL); + List manifestUrls = getUpdateManifestUrls(); callback.onChecking(true); postMessage("Checking update..."); executor.execute(() -> { try { - ReleaseInfo info = fetchReleaseInfo(manifestUrl); + ReleaseInfo info = fetchFirstReleaseInfo(manifestUrls); if (info.versionCode <= BuildConfig.VERSION_CODE) { postMessage("Already up to date: " + BuildConfig.VERSION_NAME); return; @@ -61,6 +64,42 @@ final class UpdateManager { }); } + private List getUpdateManifestUrls() { + LinkedHashSet urls = new LinkedHashSet<>(); + addUrl(urls, prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL)); + addUrl(urls, BuildConfig.DEFAULT_UPDATE_URL); + addUrl(urls, BuildConfig.DEFAULT_GITEA_UPDATE_URL); + return new ArrayList<>(urls); + } + + private void addUrl(LinkedHashSet urls, String url) { + if (url != null && !url.trim().isEmpty()) { + urls.add(url.trim()); + } + } + + private ReleaseInfo fetchFirstReleaseInfo(List manifestUrls) throws Exception { + Exception lastError = null; + for (String manifestUrl : manifestUrls) { + try { + postMessage("Checking " + hostLabel(manifestUrl) + "..."); + return fetchReleaseInfo(manifestUrl); + } catch (Exception error) { + lastError = error; + postMessage(hostLabel(manifestUrl) + " failed"); + } + } + throw lastError == null ? new IllegalStateException("No update source configured") : lastError; + } + + private String hostLabel(String url) { + try { + return new URL(url).getHost(); + } catch (Exception ignored) { + return url; + } + } + private ReleaseInfo fetchReleaseInfo(String manifestUrl) throws Exception { String json = readText(manifestUrl); JSONObject root = new JSONObject(json);