Fix mobile insets and update mirrors
All checks were successful
Gitea Smoke / smoke (push) Successful in 1s
All checks were successful
Gitea Smoke / smoke (push) Successful in 1s
This commit is contained in:
parent
8709c7bae5
commit
08637eafa6
12
README.md
12
README.md
@ -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.
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user