Dashboard Studio 2.6.0 and Haven 2.3.0: easier to use on a phone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="nl.melvanveen.haven"
|
||||
android:versionCode="2"
|
||||
android:versionName="2.2.0">
|
||||
android:versionCode="3"
|
||||
android:versionName="2.3.0">
|
||||
|
||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- finding Home Assistant on the home network -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<!-- installing a newer Haven that Dashboard Studio offers -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
|
||||
+53
-3
@@ -28,6 +28,21 @@
|
||||
.home .x { width: 44px; flex: none; background: transparent; color: #a9b8c2; font-size: 20px; padding: 12px 0; }
|
||||
.add { background: transparent; color: #f4a531; border: 1.5px solid rgba(244, 165, 49, .5); margin-bottom: 22px; }
|
||||
#f.hide { display: none; }
|
||||
.found { margin: 0 0 22px; }
|
||||
.found h2 { font-size: 14px; font-weight: 600; margin: 0 0 8px; }
|
||||
.found .go { display: flex; flex-direction: column; text-align: left; background: rgba(255, 255, 255, .08); color: #f4eee4; padding: 12px 14px; margin-bottom: 8px; }
|
||||
.found .go small { font-weight: 400; color: #a9b8c2; font-size: 13px; }
|
||||
.found .look { display: flex; align-items: center; gap: 10px; font-size: 14px; color: #a9b8c2; margin: 0 0 6px; }
|
||||
.found .look::before { content: ""; width: 14px; height: 14px; border-radius: 50%; border: 2px solid #f4a531; border-right-color: transparent; animation: spin .9s linear infinite; flex: none; }
|
||||
.found .look.done::before { display: none; }
|
||||
.found .look[hidden] { display: none; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.or { font-size: 14px; color: #a9b8c2; margin: 0 0 12px; }
|
||||
.home.ask { flex-wrap: wrap; background: rgba(255, 69, 58, .12); border-radius: 12px; padding: 12px 14px; }
|
||||
.home.ask p { flex-basis: 100%; margin: 0 0 10px; color: #f4eee4; }
|
||||
.home.ask button { flex: 1; padding: 10px; }
|
||||
.home.ask .keep { background: rgba(255, 255, 255, .1); color: #f4eee4; }
|
||||
.home.ask .rm { background: #ff453a; color: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -39,6 +54,7 @@
|
||||
<p class="msg" id="msg"></p>
|
||||
<div class="homes" id="homes"></div>
|
||||
<button type="button" class="add" id="add" hidden data-t="add">Add another home</button>
|
||||
<div class="found" id="found" hidden><h2 data-t="foundTitle">On this network</h2><p class="look" id="look" data-t="looking">Looking for Home Assistant…</p><div id="foundList"></div><p class="or" data-t="or">Or enter the address yourself:</p></div>
|
||||
<form id="f">
|
||||
<label for="addr" data-t="label">Home Assistant address</label>
|
||||
<input id="addr" type="text" inputmode="url" autocomplete="url" autocapitalize="off" spellcheck="false" placeholder="http://homeassistant.local:8123" required>
|
||||
@@ -58,12 +74,19 @@
|
||||
notfound: "Dashboard Studio staat niet in Home Assistant. Vraag wie Home Assistant beheert om het te installeren.",
|
||||
noaddress: "Dashboard Studio is gevonden, maar het adres kon niet worden opgevraagd. Tik in Dashboard Studio op Openen in Haven, of vraag wie Home Assistant beheert.",
|
||||
error: "Verbinden lukte niet: {0}",
|
||||
foundTitle: "Op dit netwerk",
|
||||
looking: "Home Assistant zoeken…",
|
||||
notFound: "Niet gevonden op dit netwerk.",
|
||||
or: "Of vul het adres zelf in:",
|
||||
remove: "Verwijderen", keep: "Bewaren", removeQ: "{0} uit Haven verwijderen?",
|
||||
};
|
||||
const EN = {
|
||||
offline: "Home Assistant can't be reached at this address. Check the address and your connection.",
|
||||
notfound: "Dashboard Studio isn't installed in Home Assistant. Ask whoever manages Home Assistant to install it.",
|
||||
noaddress: "Dashboard Studio was found, but its address couldn't be looked up. Tap Open in Haven in Dashboard Studio, or ask whoever manages Home Assistant.",
|
||||
error: "Couldn't connect: {0}",
|
||||
notFound: "None found on this network.",
|
||||
remove: "Remove", keep: "Keep", removeQ: "Remove {0} from Haven?",
|
||||
};
|
||||
const T = navigator.language.startsWith("nl") ? NL : EN;
|
||||
if (T === NL) document.querySelectorAll("[data-t]").forEach((el) => { el.textContent = NL[el.dataset.t] || el.textContent; });
|
||||
@@ -80,13 +103,40 @@
|
||||
const removeLabel = T === NL ? "Verwijderen" : "Remove";
|
||||
box.innerHTML = homes.map((h) => `<div class="home"><button type="button" class="go ${h.current ? "on" : ""}" data-id="${esc(h.id)}">${esc(h.name)}<small>${esc(h.ha)}</small></button>
|
||||
<button type="button" class="x" data-x="${esc(h.id)}" aria-label="${removeLabel} ${esc(h.name)}">×</button></div>`).join("");
|
||||
const words = (k) => (T[k] || EN[k] || k);
|
||||
box.onclick = (e) => {
|
||||
const go = e.target.closest("[data-id]"), x = e.target.closest("[data-x]");
|
||||
const go = e.target.closest("[data-id]"), x = e.target.closest("[data-x]"), b = e.target.closest("[data-rm], [data-keep]");
|
||||
if (go) app.switchHome(go.dataset.id);
|
||||
if (x && confirm((T === NL ? "Verwijderen: " : "Remove ") + x.closest(".home").querySelector(".go").firstChild.textContent + "?")) app.removeHome(x.dataset.x);
|
||||
// removing asks first, right there in the list
|
||||
if (x) {
|
||||
const row = x.closest(".home"), name = row.querySelector(".go").firstChild.textContent;
|
||||
row.classList.add("ask");
|
||||
row.innerHTML = `<p>${esc(words("removeQ").replace("{0}", name))}</p><button type="button" class="keep" data-keep>${words("keep")}</button><button type="button" class="rm" data-rm="${esc(x.dataset.x)}">${words("remove")}</button>`;
|
||||
}
|
||||
if (b?.dataset.rm) app.removeHome(b.dataset.rm);
|
||||
if (b && "keep" in b.dataset) location.reload();
|
||||
};
|
||||
if (homes.length && homes.some((h) => h.current) && !why) { form.classList.add("hide"); add.hidden = false; }
|
||||
const adding = !(homes.length && homes.some((h) => h.current) && !why);
|
||||
if (!adding) { form.classList.add("hide"); add.hidden = false; }
|
||||
add.onclick = () => app.addHome();
|
||||
|
||||
// Home Assistant announces itself on the home network: list what is found, to tap instead of typing.
|
||||
if (adding && app && app.findHomes) {
|
||||
const wrap = document.getElementById("found"), list = document.getElementById("foundList"), look = document.getElementById("look");
|
||||
const known = new Set(homes.map((h) => h.ha.toLowerCase()));
|
||||
app.findHomes();
|
||||
wrap.hidden = false;
|
||||
let n = 0;
|
||||
const tick = () => {
|
||||
const found = JSON.parse(app.found()).filter((f) => !known.has(f.url.toLowerCase()));
|
||||
list.innerHTML = found.map((f) => `<button type="button" class="go" data-url="${esc(f.url)}">${esc(f.name)}<small>${esc(f.url)}</small></button>`).join("");
|
||||
if (++n < 30) setTimeout(tick, 1000);
|
||||
else { look.classList.add("done"); if (!found.length) look.textContent = words("notFound"); else look.hidden = true; }
|
||||
if (found.length) look.hidden = true;
|
||||
};
|
||||
tick();
|
||||
list.onclick = (e) => { const b = e.target.closest("[data-url]"); if (b) app.setup(b.dataset.url); };
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,10 +12,15 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.net.nsd.NsdManager;
|
||||
import android.net.nsd.NsdServiceInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.webkit.CookieManager;
|
||||
@@ -28,11 +33,18 @@ import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Haven: the dashboards of Dashboard Studio as an Android app, for everyone in the home. Full screen, and only
|
||||
* the dashboards: Dashboard Studio leaves out its builder and admin screen when window.HavenApp is there.
|
||||
@@ -49,6 +61,9 @@ public class MainActivity extends Activity {
|
||||
private WebView web;
|
||||
private SharedPreferences prefs;
|
||||
private String message = "", detail = "";
|
||||
private String tried = ""; // the address last tried, to fill in again when it didn't work
|
||||
private TextView status, another; // on the loading screen: what Haven is doing, and a way out
|
||||
private static final boolean NL = Locale.getDefault().getLanguage().equals("nl");
|
||||
private long connectedAt = 0;
|
||||
private ValueCallback<Uri[]> fileCallback;
|
||||
|
||||
@@ -101,7 +116,28 @@ public class MainActivity extends Activity {
|
||||
|
||||
web = new WebView(this);
|
||||
web.setBackgroundColor(Color.TRANSPARENT); // the loading screen shows through until a page is ready
|
||||
setContentView(web);
|
||||
FrameLayout root = new FrameLayout(this);
|
||||
root.addView(web);
|
||||
// Under the icon of the loading screen: what Haven is doing, and after a while a way to choose another home.
|
||||
LinearLayout box = new LinearLayout(this);
|
||||
box.setOrientation(LinearLayout.VERTICAL);
|
||||
box.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
box.setPadding(dp(24), dp(130), dp(24), 0);
|
||||
status = new TextView(this);
|
||||
status.setTextColor(Color.parseColor("#a9b8c2"));
|
||||
status.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
status.setGravity(Gravity.CENTER);
|
||||
another = new TextView(this);
|
||||
another.setText(NL ? "Ander huis of adres kiezen" : "Choose another home or address");
|
||||
another.setTextColor(Color.parseColor("#f4a531"));
|
||||
another.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
another.setPadding(dp(16), dp(20), dp(16), dp(12));
|
||||
another.setOnClickListener((v) -> showSetup("", ""));
|
||||
box.addView(status);
|
||||
box.addView(another);
|
||||
root.addView(box, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
showStatus(false);
|
||||
setContentView(root);
|
||||
WebSettings s = web.getSettings();
|
||||
s.setJavaScriptEnabled(true);
|
||||
s.setDomStorageEnabled(true);
|
||||
@@ -140,7 +176,8 @@ public class MainActivity extends Activity {
|
||||
private void save(String ha, String app) {
|
||||
try {
|
||||
JSONObject h = Hass.current(this);
|
||||
if (h == null || !h.optString("ha").equalsIgnoreCase(ha)) h = new JSONObject().put("ha", ha);
|
||||
// a new address is only kept for good once it has connected (see Bridge.open)
|
||||
if (h == null || !h.optString("ha").equalsIgnoreCase(ha)) h = new JSONObject().put("ha", ha).put("pending", "true");
|
||||
h.put("app", app);
|
||||
Hass.setCurrent(this, Hass.save(this, h).optString("id"));
|
||||
} catch (JSONException ignored) { /* nothing to save */ }
|
||||
@@ -168,15 +205,35 @@ public class MainActivity extends Activity {
|
||||
/** While Home Assistant's page does its work, the loading screen (the window background) shows instead. */
|
||||
private void hidePage() {
|
||||
web.setAlpha(0f);
|
||||
showStatus(true);
|
||||
reveal.removeCallbacks(showPage);
|
||||
reveal.postDelayed(showPage, 6000); // still there after a while: probably something to read or answer
|
||||
}
|
||||
private final android.os.Handler reveal = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
private final Runnable showPage = () -> web.animate().alpha(1f).setDuration(150);
|
||||
private final Runnable showPage = () -> { showStatus(false); web.animate().alpha(1f).setDuration(150); };
|
||||
private final Runnable offerAnother = () -> another.setVisibility(View.VISIBLE);
|
||||
|
||||
/** "Connecting to Home…" on the loading screen; after a while also "Choose another home or address". */
|
||||
private void showStatus(boolean on) {
|
||||
reveal.removeCallbacks(offerAnother);
|
||||
another.setVisibility(View.GONE);
|
||||
if (!on) { status.setVisibility(View.GONE); return; }
|
||||
JSONObject h = Hass.current(this);
|
||||
String name = h == null || h.optString("pending").equals("true") ? "" : h.optString("name");
|
||||
status.setText(name.isEmpty() ? (NL ? "Verbinden met Home Assistant…" : "Connecting to Home Assistant…") : (NL ? "Verbinden met " : "Connecting to ") + name + "…");
|
||||
status.setVisibility(View.VISIBLE);
|
||||
reveal.postDelayed(offerAnother, 12000);
|
||||
}
|
||||
|
||||
private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); }
|
||||
private boolean offlineTried = false;
|
||||
|
||||
private void showSetup(String why, String more) {
|
||||
message = why; detail = more == null ? "" : more;
|
||||
showStatus(false);
|
||||
// An address that never connected isn't kept as a home; it is filled in again to correct it.
|
||||
JSONObject h = Hass.current(this);
|
||||
if (h != null && h.optString("pending").equals("true")) { tried = h.optString("ha"); Hass.remove(this, h.optString("id")); }
|
||||
web.loadUrl(SETUP);
|
||||
}
|
||||
|
||||
@@ -185,12 +242,12 @@ public class MainActivity extends Activity {
|
||||
public void onPageStarted(WebView view, String url, android.graphics.Bitmap icon) {
|
||||
boolean haPage = url != null && !ha().isEmpty() && url.startsWith(ha()) && !isIngress(url) && !url.equals(app().isEmpty() ? "-" : app());
|
||||
if (haPage && !url.contains("/auth/")) hidePage();
|
||||
else if (url != null && url.contains("/auth/")) { reveal.removeCallbacks(showPage); web.setAlpha(1f); } // signing in
|
||||
else if (url != null && url.contains("/auth/")) { reveal.removeCallbacks(showPage); showStatus(false); web.setAlpha(1f); } // signing in
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
if (url != null && (isIngress(url) || url.startsWith("file:"))) { reveal.removeCallbacks(showPage); web.animate().alpha(1f).setDuration(150); }
|
||||
if (url != null && (isIngress(url) || url.startsWith("file:"))) { reveal.removeCallbacks(showPage); showStatus(false); web.animate().alpha(1f).setDuration(150); }
|
||||
if (url == null || ha().isEmpty() || !url.startsWith(ha())) return;
|
||||
String page = app().replaceAll("[?#].*", "");
|
||||
if (isIngress(url) || (!page.isEmpty() && url.startsWith(page))) {
|
||||
@@ -216,7 +273,7 @@ public class MainActivity extends Activity {
|
||||
if (!req.isForMainFrame() || req.getUrl().toString().startsWith("file:")) return;
|
||||
// Home Assistant can't be reached: Dashboard Studio's offline copy may still open, with the last known states.
|
||||
if (!isIngress(req.getUrl().toString()) && !app().isEmpty() && !offlineTried) { offlineTried = true; web.loadUrl(app()); }
|
||||
else { reveal.removeCallbacks(showPage); web.setAlpha(1f); showSetup("offline", ""); }
|
||||
else { reveal.removeCallbacks(showPage); showStatus(false); web.setAlpha(1f); showSetup("offline", ""); }
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -261,8 +318,12 @@ public class MainActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (web.canGoBack()) web.goBack();
|
||||
else super.onBackPressed();
|
||||
// The page first: back closes an open sheet or menu there. Then its history; then Haven goes to the background.
|
||||
web.evaluateJavascript("(window.havenBack && window.havenBack()) ? 1 : 0", (v) -> {
|
||||
if ("1".equals(v)) return;
|
||||
if (web.canGoBack()) web.goBack();
|
||||
else moveTaskToBack(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -340,6 +401,90 @@ public class MainActivity extends Activity {
|
||||
} catch (ActivityNotFoundException ignored) { /* no installer */ }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- finding Home Assistant
|
||||
// Home Assistant announces itself on the home network (zeroconf, "_home-assistant._tcp"), with its name and
|
||||
// address. The setup page lists what is found, so nobody has to know the address.
|
||||
private NsdManager nsd;
|
||||
private NsdManager.DiscoveryListener finder;
|
||||
private WifiManager.MulticastLock multicast;
|
||||
private final JSONArray found = new JSONArray(); // {name, url}
|
||||
private final ArrayDeque<NsdServiceInfo> toResolve = new ArrayDeque<>();
|
||||
private boolean resolving = false;
|
||||
private final Runnable stopFinding = this::stopFinding;
|
||||
|
||||
private void startFinding() {
|
||||
if (finder != null) return;
|
||||
nsd = (NsdManager) getSystemService(NSD_SERVICE);
|
||||
if (nsd == null) return;
|
||||
try {
|
||||
multicast = ((WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE)).createMulticastLock("haven");
|
||||
multicast.setReferenceCounted(false);
|
||||
multicast.acquire();
|
||||
} catch (Exception ignored) { multicast = null; /* finding may still work */ }
|
||||
finder = new NsdManager.DiscoveryListener() {
|
||||
@Override public void onServiceFound(NsdServiceInfo info) { synchronized (toResolve) { toResolve.add(info); } resolveNext(); }
|
||||
@Override public void onServiceLost(NsdServiceInfo info) {}
|
||||
@Override public void onDiscoveryStarted(String type) {}
|
||||
@Override public void onDiscoveryStopped(String type) {}
|
||||
@Override public void onStartDiscoveryFailed(String type, int code) { finder = null; }
|
||||
@Override public void onStopDiscoveryFailed(String type, int code) {}
|
||||
};
|
||||
try { nsd.discoverServices("_home-assistant._tcp", NsdManager.PROTOCOL_DNS_SD, finder); } catch (Exception e) { finder = null; }
|
||||
reveal.postDelayed(stopFinding, 60000);
|
||||
}
|
||||
|
||||
private void stopFinding() {
|
||||
reveal.removeCallbacks(stopFinding);
|
||||
if (finder != null) try { nsd.stopServiceDiscovery(finder); } catch (Exception ignored) { /* already stopped */ }
|
||||
finder = null;
|
||||
if (multicast != null) try { multicast.release(); } catch (Exception ignored) { /* not held */ }
|
||||
multicast = null;
|
||||
}
|
||||
|
||||
/** One at a time: older Android resolves only one service at once. */
|
||||
private void resolveNext() {
|
||||
NsdServiceInfo next;
|
||||
synchronized (toResolve) {
|
||||
if (resolving || toResolve.isEmpty()) return;
|
||||
resolving = true;
|
||||
next = toResolve.poll();
|
||||
}
|
||||
nsd.resolveService(next, new NsdManager.ResolveListener() {
|
||||
@Override public void onResolveFailed(NsdServiceInfo info, int code) { synchronized (toResolve) { resolving = false; } resolveNext(); }
|
||||
@Override public void onServiceResolved(NsdServiceInfo info) { addFound(info); synchronized (toResolve) { resolving = false; } resolveNext(); }
|
||||
});
|
||||
}
|
||||
|
||||
private void addFound(NsdServiceInfo info) {
|
||||
Map<String, byte[]> txt = info.getAttributes();
|
||||
String url = text(txt, "internal_url");
|
||||
if (url.isEmpty()) url = text(txt, "base_url");
|
||||
if (url.isEmpty() && info.getHost() != null) {
|
||||
String host = info.getHost().getHostAddress();
|
||||
if (host == null) return;
|
||||
if (host.contains(":")) host = "[" + host.replaceAll("%.*", "") + "]"; // IPv6
|
||||
url = "http://" + host + ":" + info.getPort();
|
||||
}
|
||||
if (!url.matches("(?i)https?://.+")) return;
|
||||
url = url.replaceAll("/+$", "");
|
||||
String name = text(txt, "location_name");
|
||||
if (name.isEmpty()) name = info.getServiceName();
|
||||
try {
|
||||
synchronized (found) {
|
||||
for (int i = 0; i < found.length(); i++) if (found.getJSONObject(i).optString("url").equalsIgnoreCase(url)) return;
|
||||
found.put(new JSONObject().put("name", name).put("url", url));
|
||||
}
|
||||
} catch (JSONException ignored) { /* skip this one */ }
|
||||
}
|
||||
|
||||
private static String text(Map<String, byte[]> txt, String key) {
|
||||
byte[] v = txt == null ? null : txt.get(key);
|
||||
return v == null ? "" : new String(v, java.nio.charset.StandardCharsets.UTF_8).trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() { stopFinding(); super.onDestroy(); }
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); web.saveState(out); }
|
||||
|
||||
@@ -351,7 +496,7 @@ public class MainActivity extends Activity {
|
||||
|
||||
/** What the pages can call, as window.HavenApp. */
|
||||
private class Bridge {
|
||||
@JavascriptInterface public String address() { return ha(); }
|
||||
@JavascriptInterface public String address() { return tried.isEmpty() ? ha() : tried; }
|
||||
@JavascriptInterface public String appUrl() { return app(); }
|
||||
@JavascriptInterface public String message() { return message; }
|
||||
@JavascriptInterface public String detail() { return detail; }
|
||||
@@ -364,6 +509,7 @@ public class MainActivity extends Activity {
|
||||
if (!text.matches("(?i)https?://.*")) text = "http://" + text;
|
||||
Uri u = Uri.parse(text);
|
||||
if (u.getHost() == null || u.getHost().isEmpty()) { showSetup("offline", ""); return; }
|
||||
stopFinding();
|
||||
save(origin(u), isIngress(text) ? text : "");
|
||||
start();
|
||||
});
|
||||
@@ -373,6 +519,9 @@ public class MainActivity extends Activity {
|
||||
runOnUiThread(() -> {
|
||||
if (url == null || !url.startsWith(ha())) return;
|
||||
save(ha(), url);
|
||||
JSONObject h = Hass.current(MainActivity.this);
|
||||
if (h != null && h.optString("pending").equals("true")) { try { Hass.save(MainActivity.this, h.put("pending", "false")); } catch (JSONException ignored) { /* kept as it was */ } } // it connected: keep it
|
||||
tried = "";
|
||||
web.loadUrl(url);
|
||||
});
|
||||
}
|
||||
@@ -401,12 +550,17 @@ public class MainActivity extends Activity {
|
||||
try {
|
||||
for (int i = 0; i < list.length(); i++) {
|
||||
JSONObject h = list.getJSONObject(i);
|
||||
if (h.optString("pending").equals("true")) continue;
|
||||
out.put(new JSONObject().put("id", h.optString("id")).put("name", h.optString("name")).put("ha", h.optString("ha")).put("current", cur.equals(h.optString("id"))));
|
||||
}
|
||||
} catch (JSONException ignored) { /* an empty list */ }
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/** The setup page: look for Home Assistant on this network, and what was found so far. */
|
||||
@JavascriptInterface public void findHomes() { runOnUiThread(MainActivity.this::startFinding); }
|
||||
@JavascriptInterface public String found() { synchronized (found) { return found.toString(); } }
|
||||
|
||||
@JavascriptInterface public void switchHome(String id) { runOnUiThread(() -> { Hass.setCurrent(MainActivity.this, id); start(); }); }
|
||||
|
||||
@JavascriptInterface public void addHome() { runOnUiThread(() -> { Hass.setCurrent(MainActivity.this, ""); showSetup("", ""); }); }
|
||||
|
||||
Reference in New Issue
Block a user