Fix mobile insets and update mirrors
All checks were successful
Gitea Smoke / smoke (push) Successful in 1s

This commit is contained in:
Codex 2026-07-05 15:24:21 +00:00
parent 8709c7bae5
commit 08637eafa6
4 changed files with 123 additions and 10 deletions

View File

@ -14,6 +14,7 @@ APIs directly:
and kill session through HTTP API and kill session through HTTP API
- open one live terminal viewer through `/ws/terminal` - open one live terminal viewer through `/ws/terminal`
- native `/ws/events` listener for session invalidation and hook notifications - 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, - native API action center for health, server status, timeline, preferences,
kanban projects, group messages, hook events, image file/URL upload, image kanban projects, group messages, hook events, image file/URL upload, image
preview info, and native image preview display 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 ```text
https://github.com/neatstudio/tmux-browser-android/releases/latest/download/tmux-android.apk 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://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 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 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. The workflow uploads both the APK and `latest.json` to each release.

View File

@ -10,6 +10,8 @@ val defaultServerUrl = providers.gradleProperty("defaultServerUrl")
.orElse("http://100.89.0.116:3000") .orElse("http://100.89.0.116:3000")
val defaultUpdateUrl = providers.gradleProperty("defaultUpdateUrl") val defaultUpdateUrl = providers.gradleProperty("defaultUpdateUrl")
.orElse("https://github.com/${repoSlug.get()}/releases/latest/download/latest.json") .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 signingProps = Properties()
val signingFile = rootProject.file("signing.properties") val signingFile = rootProject.file("signing.properties")
@ -30,6 +32,7 @@ android {
buildConfigField("String", "DEFAULT_SERVER_URL", "\"${defaultServerUrl.get()}\"") buildConfigField("String", "DEFAULT_SERVER_URL", "\"${defaultServerUrl.get()}\"")
buildConfigField("String", "DEFAULT_UPDATE_URL", "\"${defaultUpdateUrl.get()}\"") buildConfigField("String", "DEFAULT_UPDATE_URL", "\"${defaultUpdateUrl.get()}\"")
buildConfigField("String", "DEFAULT_GITEA_UPDATE_URL", "\"${defaultGiteaUpdateUrl.get()}\"")
} }
buildFeatures { buildFeatures {

View File

@ -21,6 +21,7 @@ import android.text.InputType;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
@ -133,9 +134,24 @@ public final class MainActivity extends Activity {
statusText.setBackgroundColor(Color.rgb(29, 34, 41)); statusText.setBackgroundColor(Color.rgb(29, 34, 41));
statusText.setSingleLine(true); statusText.setSingleLine(true);
setStatus("Ready"); setStatus("Ready");
applySystemBarInsets(root);
return 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() { private void renderSessionScreen() {
closeTerminalSocket(); closeTerminalSocket();
activeSessionName = null; activeSessionName = null;
@ -169,11 +185,14 @@ public final class MainActivity extends Activity {
private LinearLayout createServerBar() { private LinearLayout createServerBar() {
LinearLayout toolbar = new LinearLayout(this); LinearLayout toolbar = new LinearLayout(this);
toolbar.setOrientation(LinearLayout.HORIZONTAL); toolbar.setOrientation(LinearLayout.VERTICAL);
toolbar.setGravity(Gravity.CENTER_VERTICAL);
toolbar.setPadding(dp(8), dp(6), dp(8), dp(6)); toolbar.setPadding(dp(8), dp(6), dp(8), dp(6));
toolbar.setBackgroundColor(Color.rgb(17, 20, 24)); 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 = new EditText(this);
urlField.setSingleLine(true); urlField.setSingleLine(true);
urlField.setTextColor(Color.WHITE); urlField.setTextColor(Color.WHITE);
@ -190,11 +209,20 @@ public final class MainActivity extends Activity {
} }
return false; return false;
}); });
toolbar.addView(urlField, new LinearLayout.LayoutParams(0, dp(42), 1)); urlRow.addView(urlField, new LinearLayout.LayoutParams(0, dp(44), 1));
toolbar.addView(toolbarButton("Go", view -> saveServerAndRefresh())); urlRow.addView(toolbarButton("Go", view -> saveServerAndRefresh()));
toolbar.addView(toolbarButton("New", view -> promptCreateSession()));
toolbar.addView(toolbarButton("Update", view -> updateManager.check(true))); LinearLayout actionRow = new LinearLayout(this);
toolbar.addView(toolbarButton("More", view -> showMainActions())); 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; return toolbar;
} }
@ -435,6 +463,7 @@ public final class MainActivity extends Activity {
"Upload image URL", "Upload image URL",
"Image preview info", "Image preview info",
"Open image preview", "Open image preview",
"Update source",
"Permissions / update status" "Permissions / update status"
}; };
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
@ -496,6 +525,9 @@ public final class MainActivity extends Activity {
promptOpenImagePreview(); promptOpenImagePreview();
break; break;
case 18: case 18:
showUpdateSourcePicker();
break;
case 19:
showPermissionsAndUpdateStatus(); showPermissionsAndUpdateStatus();
break; break;
default: default:
@ -883,9 +915,11 @@ public final class MainActivity extends Activity {
private void showPermissionsAndUpdateStatus() { private void showPermissionsAndUpdateStatus() {
StringBuilder text = new StringBuilder(); StringBuilder text = new StringBuilder();
text.append("Server: ").append(getServerUrl()).append('\n'); 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(prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL))
.append('\n'); .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"); text.append("Network: manifest permission, no runtime grant required\n");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
text.append("Install unknown apps: ") text.append("Install unknown apps: ")
@ -912,6 +946,31 @@ public final class MainActivity extends Activity {
.show(); .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() { private void openInstallPermissionSettings() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
showMessage("Install permission is allowed on this Android version"); showMessage("Install permission is allowed on this Android version");

View File

@ -19,6 +19,9 @@ import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -41,12 +44,12 @@ final class UpdateManager {
} }
void check(boolean userInitiated) { void check(boolean userInitiated) {
String manifestUrl = prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL); List<String> manifestUrls = getUpdateManifestUrls();
callback.onChecking(true); callback.onChecking(true);
postMessage("Checking update..."); postMessage("Checking update...");
executor.execute(() -> { executor.execute(() -> {
try { try {
ReleaseInfo info = fetchReleaseInfo(manifestUrl); ReleaseInfo info = fetchFirstReleaseInfo(manifestUrls);
if (info.versionCode <= BuildConfig.VERSION_CODE) { if (info.versionCode <= BuildConfig.VERSION_CODE) {
postMessage("Already up to date: " + BuildConfig.VERSION_NAME); postMessage("Already up to date: " + BuildConfig.VERSION_NAME);
return; return;
@ -61,6 +64,42 @@ final class UpdateManager {
}); });
} }
private List<String> getUpdateManifestUrls() {
LinkedHashSet<String> 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<String> urls, String url) {
if (url != null && !url.trim().isEmpty()) {
urls.add(url.trim());
}
}
private ReleaseInfo fetchFirstReleaseInfo(List<String> 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 { private ReleaseInfo fetchReleaseInfo(String manifestUrl) throws Exception {
String json = readText(manifestUrl); String json = readText(manifestUrl);
JSONObject root = new JSONObject(json); JSONObject root = new JSONObject(json);