release: 1.4.0 — one version for stable/beta/dev
Light/dark theme and compact mobile layout, per-client speed limit, real speed tests, panel domain (Let's Encrypt), client address + cascade relay, RKN scanner guard, WARP, backup/restore, router web terminal, Android VPN client with split tunnelling and Android admin app. Updating from 1.2.x keeps keys, server.env and clients; tested 1.2.8 -> 1.4.0 on live VMs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1
android/.gitignore
vendored
Normal file
1
android/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build/
|
||||
27
android/AndroidManifest.xml
Normal file
27
android/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.pca.phobos"
|
||||
android:versionCode="140"
|
||||
android:versionName="1.4.0">
|
||||
|
||||
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="35" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="PCA Phobos"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
4
android/README.md
Normal file
4
android/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# PCA Phobos — админ-приложение (Android)
|
||||
|
||||
Список ваших панелей Phobos, каждая открывается в приложении (WebView), тема по системе,
|
||||
ссылки `phobos://` передаются в VPN-клиент. Сборка: `KS_PASS=… ./build.sh` → `build/PCA-Phobos.apk`.
|
||||
26
android/build.sh
Executable file
26
android/build.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build PCA Phobos Android app without Gradle: aapt2 -> javac -> d8 -> zipalign -> apksigner.
|
||||
# ANDROID_HOME=~/Library/Android/sdk KS=~/.android/pca-phobos.jks KS_PASS=… ./build.sh
|
||||
# Output: build/PCA-Phobos.apk. The keystore is NOT in git (keep the same one, or updates won't install).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
|
||||
BT="$SDK/build-tools/$(ls "$SDK/build-tools" | sort -V | tail -1)"
|
||||
JAR="$SDK/platforms/$(ls "$SDK/platforms" | sort -V | tail -1)/android.jar"
|
||||
KS="${KS:-$HOME/.android/pca-phobos.jks}"
|
||||
KS_PASS="${KS_PASS:?set KS_PASS (keystore password)}"
|
||||
rm -rf build && mkdir -p build/res build/classes build/dex
|
||||
|
||||
"$BT/aapt2" compile --dir res -o build/res/
|
||||
"$BT/aapt2" link -o build/base.apk -I "$JAR" --manifest AndroidManifest.xml \
|
||||
--java build/gen build/res/*.flat --min-sdk-version 23 --target-sdk-version 35
|
||||
javac -source 11 -target 11 -nowarn -classpath "$JAR" -d build/classes \
|
||||
$(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath" || true
|
||||
"$BT/d8" --release --lib "$JAR" --min-api 23 --output build/dex $(find build/classes -name '*.class')
|
||||
cp build/base.apk build/unsigned.apk
|
||||
(cd build/dex && zip -q ../unsigned.apk classes.dex)
|
||||
"$BT/zipalign" -f 4 build/unsigned.apk build/aligned.apk
|
||||
[ -f "$KS" ] || keytool -genkeypair -keystore "$KS" -storepass "$KS_PASS" -keypass "$KS_PASS" \
|
||||
-alias pca -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=PCA Phobos, O=PCAdministration" >/dev/null
|
||||
"$BT/apksigner" sign --ks "$KS" --ks-pass "pass:$KS_PASS" --out build/PCA-Phobos.apk build/aligned.apk
|
||||
"$BT/apksigner" verify build/PCA-Phobos.apk && ls -la build/PCA-Phobos.apk
|
||||
12
android/res/drawable/ic_launcher.xml
Normal file
12
android/res/drawable/ic_launcher.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp" android:height="108dp"
|
||||
android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:fillColor="#1e1b4b" android:pathData="M0,0h108v108h-108z" />
|
||||
<path android:fillColor="#4f46e5"
|
||||
android:pathData="M54,18 L82,28 L82,52 C82,70 70,84 54,90 C38,84 26,70 26,52 L26,28 Z" />
|
||||
<path android:fillColor="#c7d2fe"
|
||||
android:pathData="M54,30 L72,36.5 L72,52 C72,64 64,73.5 54,78 C44,73.5 36,64 36,52 L36,36.5 Z" />
|
||||
<path android:fillColor="#1e1b4b"
|
||||
android:pathData="M47,46 h14 v5 h-4.5 v16 h-5 v-16 h-4.5 z" />
|
||||
</vector>
|
||||
10
android/res/values-night/styles.xml
Normal file
10
android/res/values-night/styles.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:colorPrimary">#a5b4fc</item>
|
||||
<item name="android:colorPrimaryDark">#1e1b4b</item>
|
||||
<item name="android:colorAccent">#818cf8</item>
|
||||
<item name="android:windowBackground">#0f172a</item>
|
||||
<item name="android:statusBarColor">#1e1b4b</item>
|
||||
</style>
|
||||
</resources>
|
||||
10
android/res/values/styles.xml
Normal file
10
android/res/values/styles.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:colorPrimary">#4338ca</item>
|
||||
<item name="android:colorPrimaryDark">#1e1b4b</item>
|
||||
<item name="android:colorAccent">#4f46e5</item>
|
||||
<item name="android:windowBackground">#f1f5f9</item>
|
||||
<item name="android:statusBarColor">#1e1b4b</item>
|
||||
</style>
|
||||
</resources>
|
||||
367
android/src/com/pca/phobos/MainActivity.java
Normal file
367
android/src/com/pca/phobos/MainActivity.java
Normal file
@@ -0,0 +1,367 @@
|
||||
package com.pca.phobos;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* PCA Phobos — admin app: a list of your Phobos panels, each opens in a WebView.
|
||||
* phobos:// links are handed to PhobosWG, downloads go to the system browser.
|
||||
*/
|
||||
public class MainActivity extends Activity {
|
||||
private SharedPreferences prefs;
|
||||
private WebView web;
|
||||
private ProgressBar progress;
|
||||
private TextView title;
|
||||
private boolean dark;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
prefs = getSharedPreferences("phobos", MODE_PRIVATE);
|
||||
dark = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
|
||||
== Configuration.UI_MODE_NIGHT_YES;
|
||||
JSONArray list = panels();
|
||||
String last = prefs.getString("last", "");
|
||||
if (!last.isEmpty() && b == null) openPanel(last, nameFor(last));
|
||||
else if (list.length() == 0) showEditor(null, -1);
|
||||
else showList();
|
||||
}
|
||||
|
||||
// ── colors ────────────────────────────────────────────────────────────
|
||||
private int bg() { return dark ? 0xFF0F172A : 0xFFF1F5F9; }
|
||||
private int card() { return dark ? 0xFF1E293B : 0xFFFFFFFF; }
|
||||
private int text() { return dark ? 0xFFE2E8F0 : 0xFF0F172A; }
|
||||
private int muted() { return dark ? 0xFF94A3B8 : 0xFF64748B; }
|
||||
private static final int ACCENT = 0xFF4F46E5;
|
||||
private static final int HEADER = 0xFF1E1B4B;
|
||||
|
||||
private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); }
|
||||
|
||||
private GradientDrawable round(int color, int radius) {
|
||||
GradientDrawable g = new GradientDrawable();
|
||||
g.setColor(color);
|
||||
g.setCornerRadius(dp(radius));
|
||||
return g;
|
||||
}
|
||||
|
||||
private Button button(String label, int color, View.OnClickListener l) {
|
||||
Button btn = new Button(this);
|
||||
btn.setText(label);
|
||||
btn.setAllCaps(false);
|
||||
btn.setTextColor(Color.WHITE);
|
||||
btn.setBackground(round(color, 10));
|
||||
btn.setOnClickListener(l);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private LinearLayout header(String text, View right) {
|
||||
LinearLayout h = new LinearLayout(this);
|
||||
h.setBackgroundColor(HEADER);
|
||||
h.setGravity(Gravity.CENTER_VERTICAL);
|
||||
h.setPadding(dp(16), dp(12), dp(8), dp(12));
|
||||
TextView t = new TextView(this);
|
||||
t.setText("🛡 " + text);
|
||||
t.setTextColor(0xFFC7D2FE);
|
||||
t.setTextSize(18);
|
||||
t.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
t.setSingleLine(true);
|
||||
h.addView(t, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||
if (right != null) h.addView(right);
|
||||
title = t;
|
||||
return h;
|
||||
}
|
||||
|
||||
// ── storage ───────────────────────────────────────────────────────────
|
||||
private JSONArray panels() {
|
||||
try { return new JSONArray(prefs.getString("panels", "[]")); }
|
||||
catch (Exception e) { return new JSONArray(); }
|
||||
}
|
||||
|
||||
private void savePanels(JSONArray a) { prefs.edit().putString("panels", a.toString()).apply(); }
|
||||
|
||||
private String nameFor(String url) {
|
||||
JSONArray a = panels();
|
||||
for (int i = 0; i < a.length(); i++) {
|
||||
JSONObject o = a.optJSONObject(i);
|
||||
if (o != null && url.equals(o.optString("url"))) return o.optString("name", url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
static String normalize(String raw) {
|
||||
String u = raw.trim();
|
||||
if (u.isEmpty()) return "";
|
||||
if (!u.startsWith("http://") && !u.startsWith("https://")) u = "http://" + u;
|
||||
while (u.endsWith("/")) u = u.substring(0, u.length() - 1);
|
||||
return u;
|
||||
}
|
||||
|
||||
// ── screens ───────────────────────────────────────────────────────────
|
||||
private void showList() {
|
||||
web = null;
|
||||
prefs.edit().remove("last").apply();
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setBackgroundColor(bg());
|
||||
root.addView(header("PCA Phobos", null));
|
||||
|
||||
ScrollView sv = new ScrollView(this);
|
||||
LinearLayout box = new LinearLayout(this);
|
||||
box.setOrientation(LinearLayout.VERTICAL);
|
||||
box.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||
sv.addView(box);
|
||||
|
||||
TextView hint = new TextView(this);
|
||||
hint.setText("Ваши серверы Phobos. Нажмите — откроется панель. Долгое нажатие — изменить.");
|
||||
hint.setTextColor(muted());
|
||||
hint.setPadding(0, 0, 0, dp(12));
|
||||
box.addView(hint);
|
||||
|
||||
final JSONArray a = panels();
|
||||
for (int i = 0; i < a.length(); i++) {
|
||||
final int idx = i;
|
||||
final JSONObject o = a.optJSONObject(i);
|
||||
if (o == null) continue;
|
||||
LinearLayout c = new LinearLayout(this);
|
||||
c.setOrientation(LinearLayout.VERTICAL);
|
||||
c.setBackground(round(card(), 12));
|
||||
c.setPadding(dp(16), dp(14), dp(16), dp(14));
|
||||
TextView n = new TextView(this);
|
||||
n.setText(o.optString("name"));
|
||||
n.setTextColor(text());
|
||||
n.setTextSize(17);
|
||||
n.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
TextView u = new TextView(this);
|
||||
u.setText(o.optString("url"));
|
||||
u.setTextColor(muted());
|
||||
c.addView(n);
|
||||
c.addView(u);
|
||||
c.setOnClickListener(v -> openPanel(o.optString("url"), o.optString("name")));
|
||||
c.setOnLongClickListener(v -> { showEditor(o, idx); return true; });
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.bottomMargin = dp(10);
|
||||
box.addView(c, lp);
|
||||
}
|
||||
box.addView(button("+ Добавить сервер", ACCENT, v -> showEditor(null, -1)));
|
||||
root.addView(sv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
|
||||
setContentView(root);
|
||||
}
|
||||
|
||||
private void showEditor(final JSONObject existing, final int idx) {
|
||||
web = null;
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setBackgroundColor(bg());
|
||||
root.addView(header(existing == null ? "Новый сервер" : "Изменить сервер", null));
|
||||
LinearLayout box = new LinearLayout(this);
|
||||
box.setOrientation(LinearLayout.VERTICAL);
|
||||
box.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||
|
||||
final EditText name = field("Название (например: Амстердам)", existing == null ? "" : existing.optString("name"));
|
||||
final EditText url = field("Адрес панели: http://IP:порт или https://домен", existing == null ? "" : existing.optString("url"));
|
||||
url.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
box.addView(label("Название"));
|
||||
box.addView(name);
|
||||
box.addView(label("Адрес панели"));
|
||||
box.addView(url);
|
||||
TextView tip = new TextView(this);
|
||||
tip.setText("Адрес и порт панели печатает установщик в конце установки (или: cat /opt/phobos-panel/.port). "
|
||||
+ "Если задан домен в «Настройках» панели — используйте https://домен.");
|
||||
tip.setTextColor(muted());
|
||||
tip.setPadding(0, dp(4), 0, dp(16));
|
||||
box.addView(tip);
|
||||
|
||||
box.addView(button("Сохранить и открыть", ACCENT, v -> {
|
||||
String u = normalize(url.getText().toString());
|
||||
if (u.isEmpty()) { toast("Введите адрес панели"); return; }
|
||||
String n = name.getText().toString().trim();
|
||||
if (n.isEmpty()) n = Uri.parse(u).getHost();
|
||||
try {
|
||||
JSONArray a = panels();
|
||||
JSONObject o = new JSONObject().put("name", n).put("url", u);
|
||||
if (idx >= 0) a.put(idx, o); else a.put(o);
|
||||
savePanels(a);
|
||||
} catch (Exception ignored) { }
|
||||
openPanel(u, n);
|
||||
}));
|
||||
if (existing != null) {
|
||||
Button del = button("Удалить из списка", 0xFFDC2626, v -> {
|
||||
JSONArray a = panels(), out = new JSONArray();
|
||||
for (int i = 0; i < a.length(); i++) if (i != idx) out.put(a.opt(i));
|
||||
savePanels(out);
|
||||
showList();
|
||||
});
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.topMargin = dp(10);
|
||||
box.addView(del, lp);
|
||||
}
|
||||
if (panels().length() > 0) {
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.topMargin = dp(10);
|
||||
box.addView(button("Назад", 0xFF475569, v -> showList()), lp);
|
||||
}
|
||||
root.addView(box);
|
||||
setContentView(root);
|
||||
}
|
||||
|
||||
private TextView label(String s) {
|
||||
TextView t = new TextView(this);
|
||||
t.setText(s);
|
||||
t.setTextColor(muted());
|
||||
t.setPadding(0, dp(8), 0, dp(4));
|
||||
return t;
|
||||
}
|
||||
|
||||
private EditText field(String hint, String value) {
|
||||
EditText e = new EditText(this);
|
||||
e.setHint(hint);
|
||||
e.setText(value);
|
||||
e.setSingleLine(true);
|
||||
e.setTextColor(text());
|
||||
e.setHintTextColor(muted());
|
||||
e.setBackground(round(card(), 10));
|
||||
e.setPadding(dp(12), dp(12), dp(12), dp(12));
|
||||
return e;
|
||||
}
|
||||
|
||||
private void openPanel(final String url, String name) {
|
||||
prefs.edit().putString("last", url).apply();
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setBackgroundColor(bg());
|
||||
|
||||
LinearLayout actions = new LinearLayout(this);
|
||||
TextView reload = headerAction("⟳", v -> { if (web != null) web.reload(); });
|
||||
TextView servers = headerAction("☰", v -> showList());
|
||||
actions.addView(reload);
|
||||
actions.addView(servers);
|
||||
root.addView(header(name, actions));
|
||||
|
||||
progress = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
|
||||
progress.setMax(100);
|
||||
root.addView(progress, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(3)));
|
||||
|
||||
web = new WebView(this);
|
||||
WebSettings s = web.getSettings();
|
||||
s.setJavaScriptEnabled(true);
|
||||
s.setDomStorageEnabled(true);
|
||||
s.setLoadWithOverviewMode(true);
|
||||
s.setUseWideViewPort(true);
|
||||
s.setBuiltInZoomControls(true);
|
||||
s.setDisplayZoomControls(false);
|
||||
s.setUserAgentString(s.getUserAgentString() + " PCAPhobosApp/1.4.0");
|
||||
CookieManager.getInstance().setAcceptCookie(true);
|
||||
web.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest req) {
|
||||
Uri u = req.getUrl();
|
||||
String scheme = u.getScheme() == null ? "" : u.getScheme();
|
||||
if (scheme.equals("http") || scheme.equals("https")) {
|
||||
Uri base = Uri.parse(url);
|
||||
// other sites (GitHub, Boosty, Telegram…) -> system browser / apps
|
||||
if (u.getHost() != null && !u.getHost().equals(base.getHost())) { external(u); return true; }
|
||||
return false;
|
||||
}
|
||||
external(u); // phobos:// -> PhobosWG, tg:// -> Telegram
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String u) {
|
||||
progress.setVisibility(View.GONE);
|
||||
CookieManager.getInstance().flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int code, String desc, String failing) {
|
||||
if (failing != null && failing.startsWith(url)) {
|
||||
view.loadDataWithBaseURL(null, errorPage(url, desc), "text/html", "utf-8", null);
|
||||
}
|
||||
}
|
||||
});
|
||||
web.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public void onProgressChanged(WebView view, int p) {
|
||||
progress.setVisibility(p < 100 ? View.VISIBLE : View.GONE);
|
||||
progress.setProgress(p);
|
||||
}
|
||||
});
|
||||
web.setDownloadListener((dlUrl, ua, cd, mime, len) -> external(Uri.parse(dlUrl)));
|
||||
root.addView(web, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
|
||||
setContentView(root);
|
||||
web.loadUrl(url + "/");
|
||||
}
|
||||
|
||||
private TextView headerAction(String s, View.OnClickListener l) {
|
||||
TextView t = new TextView(this);
|
||||
t.setText(s);
|
||||
t.setTextColor(0xFFC7D2FE);
|
||||
t.setTextSize(22);
|
||||
t.setPadding(dp(14), 0, dp(14), 0);
|
||||
t.setOnClickListener(l);
|
||||
return t;
|
||||
}
|
||||
|
||||
private String errorPage(String url, String desc) {
|
||||
String bgc = dark ? "#0f172a" : "#f1f5f9", fg = dark ? "#e2e8f0" : "#0f172a";
|
||||
return "<html><body style='font-family:sans-serif;background:" + bgc + ";color:" + fg
|
||||
+ ";padding:24px;text-align:center'><h2>Панель недоступна</h2><p>" + url + "</p><p style='color:#64748b'>"
|
||||
+ desc + "</p><p>Проверьте адрес и порт, интернет и что сервер включён. Кнопка ⟳ — повторить.</p></body></html>";
|
||||
}
|
||||
|
||||
private void external(Uri u) {
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, u));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
if ("phobos".equals(u.getScheme())) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Нужно приложение PhobosWG")
|
||||
.setMessage("Ссылки phobos:// открывает приложение PhobosWG. Скачайте его на странице Android-клиента в панели (кнопка «Скачать приложение PhobosWG»).")
|
||||
.setPositiveButton("OK", null).show();
|
||||
} else {
|
||||
toast("Нет приложения для " + u.getScheme());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void toast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); }
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (web != null && web.canGoBack()) { web.goBack(); return; }
|
||||
if (web != null && panels().length() > 0) { showList(); return; }
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user