Merge remote-tracking branch 'gitea-ai/main'
This commit is contained in:
commit
b28e5f2ef6
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.archipelago.app"
|
applicationId = "com.archipelago.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 25
|
versionCode = 26
|
||||||
versionName = "0.5.5"
|
versionName = "0.5.6"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
@ -87,6 +87,28 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/** True when a TCP listener answers at [base]'s host:port within [timeoutMs]. */
|
||||||
|
private fun tcpAnswers(base: String, timeoutMs: Int): Boolean = try {
|
||||||
|
val u = android.net.Uri.parse(base)
|
||||||
|
val port = if (u.port != -1) u.port else if (u.scheme == "https") 443 else 80
|
||||||
|
java.net.Socket().use {
|
||||||
|
it.connect(java.net.InetSocketAddress(u.host, port), timeoutMs)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fastest answering origin: LAN inside a short window, else the mesh ULA
|
||||||
|
* (patient — a cold session may still be establishing), else LAN anyway so
|
||||||
|
* the existing error/fallback path handles it. */
|
||||||
|
private suspend fun pickStartUrl(lanUrl: String, meshUrl: String?): String =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (tcpAnswers(lanUrl, 2500)) return@withContext lanUrl
|
||||||
|
if (meshUrl != null && tcpAnswers(meshUrl, 12_000)) return@withContext meshUrl
|
||||||
|
lanUrl
|
||||||
|
}
|
||||||
|
|
||||||
/** Open a URL in the phone's default browser (genuinely external links). */
|
/** Open a URL in the phone's default browser (genuinely external links). */
|
||||||
private fun openExternalUrl(context: android.content.Context, url: String) {
|
private fun openExternalUrl(context: android.content.Context, url: String) {
|
||||||
try {
|
try {
|
||||||
@ -168,6 +190,20 @@ fun WebViewScreen(
|
|||||||
var hasError by remember { mutableStateOf(false) }
|
var hasError by remember { mutableStateOf(false) }
|
||||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||||
|
|
||||||
|
// Race LAN vs mesh BEFORE the WebView exists: Chromium pointed at an
|
||||||
|
// unreachable LAN IP burns a minute+ in connect retries before
|
||||||
|
// onReceivedError fires the mesh fallback — the "stuck connecting"
|
||||||
|
// stall. A raw TCP probe answers in milliseconds at home and fails in
|
||||||
|
// ~2.5s off-LAN, so startup lands on the right origin in seconds.
|
||||||
|
var startUrl by remember(serverUrl) { mutableStateOf<String?>(null) }
|
||||||
|
var raceNonce by remember { mutableIntStateOf(0) }
|
||||||
|
LaunchedEffect(serverUrl, meshFallbackUrl, raceNonce) {
|
||||||
|
val picked = pickStartUrl(serverUrl, meshFallbackUrl)
|
||||||
|
// Starting on the mesh: don't bounce back to it on error (it IS it).
|
||||||
|
if (picked != serverUrl) triedMeshFallback = true
|
||||||
|
startUrl = picked
|
||||||
|
}
|
||||||
|
|
||||||
// Web-page camera access (wallet QR scanner). The WebView's default
|
// Web-page camera access (wallet QR scanner). The WebView's default
|
||||||
// WebChromeClient silently denies getUserMedia, so grant video capture —
|
// WebChromeClient silently denies getUserMedia, so grant video capture —
|
||||||
// asking for the app-level CAMERA permission first when needed.
|
// asking for the app-level CAMERA permission first when needed.
|
||||||
@ -187,6 +223,14 @@ fun WebViewScreen(
|
|||||||
// while this is shown, so closing it returns instantly with no reload.
|
// while this is shown, so closing it returns instantly with no reload.
|
||||||
var inAppUrl by remember { mutableStateOf<String?>(null) }
|
var inAppUrl by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// Same node = EITHER of its addresses. Over the mesh the kiosk's host is
|
||||||
|
// the ULA while app links may carry the LAN IP (and vice versa) —
|
||||||
|
// comparing against one host bounced same-node apps (Pine, Home
|
||||||
|
// Assistant) out to the phone's external browser.
|
||||||
|
fun isSameNode(url: String): Boolean =
|
||||||
|
isSameHost(url, serverUrl) ||
|
||||||
|
(meshFallbackUrl != null && isSameHost(url, meshFallbackUrl))
|
||||||
|
|
||||||
// Native wallet QR scanner, opened by the web UI via the ArchipelagoQr
|
// Native wallet QR scanner, opened by the web UI via the ArchipelagoQr
|
||||||
// bridge; status lines stream back from the page while it's up.
|
// bridge; status lines stream back from the page while it's up.
|
||||||
var walletScannerVisible by remember { mutableStateOf(false) }
|
var walletScannerVisible by remember { mutableStateOf(false) }
|
||||||
@ -268,9 +312,13 @@ fun WebViewScreen(
|
|||||||
GlassButton(
|
GlassButton(
|
||||||
text = stringResource(R.string.retry),
|
text = stringResource(R.string.retry),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
// Re-race LAN vs mesh — the network we're on may have
|
||||||
|
// changed since the last pick.
|
||||||
hasError = false
|
hasError = false
|
||||||
isLoading = true
|
isLoading = true
|
||||||
webView?.loadUrl(serverUrl)
|
triedMeshFallback = false
|
||||||
|
startUrl = null
|
||||||
|
raceNonce++
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
)
|
)
|
||||||
@ -283,9 +331,16 @@ fun WebViewScreen(
|
|||||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (startUrl == null) {
|
||||||
|
// Racing LAN vs mesh (≤2.5s at home, a few seconds off-LAN) —
|
||||||
|
// far cheaper than letting Chromium retry a dead LAN IP.
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(color = BitcoinOrange)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Edge-to-edge WebView — background bleeds behind status bar.
|
// Edge-to-edge WebView — background bleeds behind status bar.
|
||||||
// Safe area values injected as CSS env() polyfill on each page load.
|
// Safe area values injected as CSS env() polyfill on each page load.
|
||||||
|
val initialUrl = startUrl ?: serverUrl
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
@ -319,7 +374,7 @@ fun WebViewScreen(
|
|||||||
// kiosk couldn't iframe — keep the user inside the app)
|
// kiosk couldn't iframe — keep the user inside the app)
|
||||||
// - different host → the phone's real browser
|
// - different host → the phone's real browser
|
||||||
fun routeOutbound(url: String) {
|
fun routeOutbound(url: String) {
|
||||||
if (isSameHost(url, serverUrl)) {
|
if (isSameNode(url)) {
|
||||||
inAppUrl = url
|
inAppUrl = url
|
||||||
} else {
|
} else {
|
||||||
openExternalUrl(context, url)
|
openExternalUrl(context, url)
|
||||||
@ -458,7 +513,7 @@ fun WebViewScreen(
|
|||||||
error: android.net.http.SslError?,
|
error: android.net.http.SslError?,
|
||||||
) {
|
) {
|
||||||
val u = error?.url
|
val u = error?.url
|
||||||
if (u != null && isSameHost(u, serverUrl)) {
|
if (u != null && isSameNode(u)) {
|
||||||
handler?.proceed()
|
handler?.proceed()
|
||||||
} else {
|
} else {
|
||||||
handler?.cancel()
|
handler?.cancel()
|
||||||
@ -595,7 +650,7 @@ fun WebViewScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
webView = this
|
webView = this
|
||||||
loadUrl(serverUrl)
|
loadUrl(initialUrl)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -620,6 +675,7 @@ fun WebViewScreen(
|
|||||||
InAppBrowser(
|
InAppBrowser(
|
||||||
url = target,
|
url = target,
|
||||||
serverUrl = serverUrl,
|
serverUrl = serverUrl,
|
||||||
|
meshUrl = meshFallbackUrl,
|
||||||
onClose = { inAppUrl = null },
|
onClose = { inAppUrl = null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -693,9 +749,14 @@ private fun fetchFavicon(pageUrl: String): Bitmap? {
|
|||||||
private fun InAppBrowser(
|
private fun InAppBrowser(
|
||||||
url: String,
|
url: String,
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
|
meshUrl: String? = null,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
// Same-node check across BOTH node addresses (LAN + mesh ULA) — see the
|
||||||
|
// kiosk's isSameNode; a mismatch here bounced app links to the browser.
|
||||||
|
fun isSameNode(u: String): Boolean =
|
||||||
|
isSameHost(u, serverUrl) || (meshUrl != null && isSameHost(u, meshUrl))
|
||||||
var browser by remember { mutableStateOf<WebView?>(null) }
|
var browser by remember { mutableStateOf<WebView?>(null) }
|
||||||
var title by remember { mutableStateOf(android.net.Uri.parse(url).host ?: url) }
|
var title by remember { mutableStateOf(android.net.Uri.parse(url).host ?: url) }
|
||||||
var favicon by remember { mutableStateOf<Bitmap?>(null) }
|
var favicon by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
@ -809,7 +870,7 @@ private fun InAppBrowser(
|
|||||||
error: android.net.http.SslError?,
|
error: android.net.http.SslError?,
|
||||||
) {
|
) {
|
||||||
val u = error?.url
|
val u = error?.url
|
||||||
if (u != null && isSameHost(u, serverUrl)) {
|
if (u != null && isSameNode(u)) {
|
||||||
handler?.proceed()
|
handler?.proceed()
|
||||||
} else {
|
} else {
|
||||||
handler?.cancel()
|
handler?.cancel()
|
||||||
@ -823,7 +884,7 @@ private fun InAppBrowser(
|
|||||||
val u = request?.url?.toString() ?: return false
|
val u = request?.url?.toString() ?: return false
|
||||||
// Stay in the overlay for same-node navigation;
|
// Stay in the overlay for same-node navigation;
|
||||||
// hand genuinely external links to the real browser.
|
// hand genuinely external links to the real browser.
|
||||||
if (isSameHost(u, serverUrl)) return false
|
if (isSameNode(u)) return false
|
||||||
openExternalUrl(ctx, u)
|
openExternalUrl(ctx, u)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user