Tell the difference between a blocked app and a missing server
Two findings from dev-updater's session reading this codebase, both checked against the code here before acting on them, and both real. **A denied local-network permission was invisible.** The manifest requests ACCESS_LOCAL_NETWORK and MainActivity asks for it, but nothing ever checked whether it was granted -- and on Android 17 a denial is indistinguishable from an unreachable server at the socket, because the OS simply drops the traffic. So every screen would have shown "is ai-server running, and is this device able to reach that address (WireGuard up)?", blaming two things that were both fine. Stated once at the root as a standing condition rather than appended to each failure it might have caused: it is not a property of any one request, and repeating it per error is how a message ends up saying the same thing twice, which this app has already done once today. **The Keystore read path was creating keys.** `unseal` called the get-or-create key function, so a sealed token whose key had been lost -- a device reset, or the app's data restored onto a device the key cannot travel to -- generated a fresh key, then failed to decrypt with it, leaving a key nothing had ever sealed with. The behaviour was already right by accident (it fails soft to "not enrolled"), but the read side now asks for the key without making one, which is what it meant all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
3c144f8070
commit
9f54a80ca4
2 files changed
+48
-3
No files matched your search
@@ -1,13 +1,18 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* One `when` rather than a navigation library: four screens, with the list as the root and the back
|
||||
@@ -42,6 +47,21 @@ fun AppRoot(settingsVersion: Int) {
|
||||
// returning to it refetches instead of showing a stale list.
|
||||
var reloadToken by remember { mutableIntStateOf(0) }
|
||||
|
||||
// A standing condition rather than a per-request failure, so it is
|
||||
// stated once here instead of appended to every error that might be
|
||||
// caused by it. Without this the app is simply unreachable and every
|
||||
// screen blames the server or the tunnel for it.
|
||||
if (!localNetworkAllowed(context)) {
|
||||
Text(
|
||||
"This app is not allowed to reach local network addresses, so it cannot " +
|
||||
"connect to the backend at all. Grant \"local network\" in Android's app " +
|
||||
"settings; until then every screen here will look like the server is down.",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
val current = settings
|
||||
if (current == null) {
|
||||
// Not enrolled yet: settings is the only usable screen. The QR
|
||||
|
||||
Reference in new issue
Block a user