Commit Graph
39 Commits
Author SHA1 Message Date
Vitor PamplonaandClaude Opus 4.8 cad987a99e fix(embed): napplet/nsite load overlay + draw it over the surface
Mirrors the web-app load-recovery fix to the napplet/nsite path and fixes a
layer bug that kept the overlay from ever showing.

The embedded surface (SandboxedSdkView) is drawn by EmbeddedTabLayer, which
sits *above* the nav screens in the shell. So a loading/error overlay placed in
the favorite screen was covered by the surface's opaque pre-first-frame
background — the black void persisted. Move the overlay into EmbeddedTabLayer,
drawn over the active tab's bounds (where the chrome sheet already lives), so it
actually covers the surface. Also fixes the overlay sizing (fillMaxSize, not
matchParentSize, which collapsed to zero inside the reserved Box).

- Promote load state to the EmbeddedSurfaceController interface (loadStatus /
  onLoadStatusChanged / retry), so EmbeddedTabLayer renders one overlay for both
  the browser and napplet controllers. Shared EmbeddedLoadStatus +
  EmbeddedLoadOverlay.
- NappletHostService now reports main-frame load state (start/finish/error) over
  a new MSG_LOAD_STATE; EmbeddedNappletController relays it and exposes retry()
  (= reload the verified content).
- The web-app path keeps its about:blank → canonical-URL self-heal; the napplet
  path has no client-supplied URL to drop, so retry = reload.

Verified on device: with the network cut, the brainstorm tab shows
"Couldn't load this app." + Retry; restoring the network and tapping Retry loads
the page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:35:44 -04:00
Vitor PamplonaandClaude Opus 4.8 fb4a2e0858 fix(browser): recover embedded web-app tab stuck on about:blank
A favorite web app pinned to the bottom bar at runtime could come up on a
blank surface (black, then white after a manual reload) and never recover.
Its warm browser session settled on about:blank — its real URL was dropped on
the way in — and the chrome Reload button calls WebView.reload(), which just
re-loads about:blank instead of the favorite's page.

Fixes:
- The provider now reports main-frame load state (start/finish/error) over a
  new MSG_LOAD_STATE. When a favorite session settles on about:blank while it
  has a real URL, the controller re-navigates to the canonical URL once.
  Gated on a real startUrl, so the generic browser's intentional about:blank
  new-tab page is left alone. Adds controller.retry() (navigate-to-canonical,
  not reload) for the chrome retry path.
- FavoriteWebAppScreen now draws a loading spinner until a real page paints,
  and an error + Retry overlay when the main frame fails or the load stalls
  (12s) — so a slow, blank, or failed load is no longer a silent black/white
  void.

Scoped to the browser/WebUrl path; the napplet/nsite path is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:03:33 -04:00
Claude 4f6a21c55d feat: favorites-first browser home + recents, with captured favicons
Builds on the omnibox work to modernize the launcher list now that favorites
and visit history both exist:

- Idle browser home (BrowserHome): pinned favorites on top under a "Favorites"
  header, then a "Recent" section from the visit history — all in one grid so
  they scroll together. Long-press a recent to drop it.
- Typed suggestions are grouped: a highlighted "Favorites" group first (subtle
  primary-container tint + medium weight), then "Recent". Favorites still rank
  first via the existing frecency boost.
- Real favicons: captured from the WebView that already loaded the page in the
  keyless :napplet browser host (so they ride the page's own Tor-routed network
  path — the main app never fetches host/favicon.ico itself), scaled and
  relayed as PNG bytes over a new MSG_RECORD_ICON IPC, and stored per-host by
  BrowserIconRegistry (main process, filesDir). Favorite cards, suggestion
  rows, and recent rows all show them, falling back to a glyph.

FavoriteAppIcon gains an optional iconModel; FavoriteAppCell is reusable via a
new LazyGridScope.favoriteAppItems extension so the browser home and the
Favorite Apps tab share one cell. Thumbnails deferred to a follow-up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
2026-06-24 15:35:40 +00:00
Claude 7a03a47d1d feat: omnibox autocomplete, visit history, and in-page address bar for the browser
Refines the browser URL-bar experience across the launcher and the in-page
browser chrome:

- Shared URL normalization (commons OmniboxInput): dedupes the logic that was
  copied between BrowserScreen and NappletBrowserService, recognizes bare
  domains/localhost/IPs, falls back to a (configurable) DuckDuckGo search, and
  flags .onion as Tor-only so the launcher forces Tor for it.
- Omnibox suggestions (commons OmniboxSuggestions): ranks favorites + visit
  history by prefix/substring match, favorite boost, and frecency; deduped by
  host. The launcher body turns into a suggestion list as you type.
- Inline ghost-text completion in the address field (TextFieldValue selection),
  completing a typed host fragment to the top-ranked host.
- Visit history (BrowserHistoryRegistry, main process): a device-local,
  bounded, DataStore-backed store. Pages are recorded ONLY on a clean
  main-frame load — relayed from the keyless :napplet browser host over a new
  MSG_RECORD_HISTORY IPC — so misspelled/unresolved addresses never enter it.
- In-page editable address bar (websites only) in NappletControlSheet, showing
  the live URL + a security glyph (Tor/https/plain) and loading what the user
  types. nsite/napplet hosts pass no navigate callback, so they never get one.

Pure logic is covered by unit tests in commons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
2026-06-24 15:02:01 +00:00
Claude 8b9fa10a1b style: standardize the two top-sheet twins (spacing, colors, Tor switch)
The embedded surfaces' Compose TopControlSheet and the full-screen activities'
native NappletControlSheet are deliberate twins in different processes/modules
(Compose in the main app vs hand-built Views in the Compose-free :napplet
sandbox host), so they can't share a composable — but they should render
identically. Bring them in line:

- Uniform row rhythm: every action/Tor row now uses the same 10dp vertical
  padding in both. The Compose switch row was 6dp while items were 12dp; both
  are now 10dp, matching the native rows.
- Native Tor row now uses a real framework Switch as the state indicator (like
  the embedded row) instead of an icon whose tint/alpha encoded on/off; the icon
  is a steady muted tint and the whole row is the toggle target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 13:20:25 +00:00
Claude 22c8c45190 fix: harden embedded napplet/nsite/browser hosts against audit-found races and IME bugs
Sandbox host services (per-session correctness + leaks):
- NappletHostService/NappletBrowserService: guard broker-reply delivery against
  a stale tab (drop replies whose session was replaced) and wrap postMessage in
  runCatching so a torn-down WebView can't crash the relay; tear down each tab's
  content server + WebView on session close and in onDestroy; refuse to build an
  orphan WebView for an unknown session.
- NappletContentServer.close(): shut the OkHttp dispatcher + evict the pool off
  the hot path so a closed tab doesn't leak connections/threads.
- NappletBlobHttp: bound a blob fetch end-to-end with a callTimeout so a stalled
  Tor exit can't pin the WebView worker thread indefinitely.
- UiAdapter close(): hop to the main thread before destroying the WebView.

Embedded IME (shim.js + RemoteImeView):
- Surrogate-pair-safe diff so an edited astral char (emoji, CJK-supplement) is
  never split into a lone surrogate in the synthesized InputEvent data.
- Real contenteditable support: map char offsets through Ranges and replace in
  place instead of overwriting textContent (which destroyed structure + caret).
- Dedup selectionchange against the last applied selection so our own setSel
  doesn't echo back to the host as a fresh edit.
- RemoteImeView flushes synchronously at the outermost batch close, preserving
  the composing region across a compose+commit in the same frame.

Embedded layer + preloader:
- Resize the cross-process surface to the snapped imeAnimationTarget instead of
  the animated ime inset, so it doesn't reconfigure every keyboard-slide frame.
- yield() between favorites in the startup sweep so building WebViews doesn't
  monopolize the frame.
- Per-site Tor/open-web registries expose awaitReady(); the preloader awaits
  hydration before its first routing decision so a cold start can't route a
  site the user pinned to the open web through Tor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 04:27:50 +00:00
Claude 5eb39b141a refactor: session-scope NappletHostService so multiple napplet/nsite tabs are correct
Mirror the browser host's per-session refactor for the napplet/nsite embed
provider. A single NappletHostService instance is shared by every embedded tab
(same bound Intent), but it kept one set of fields (client messenger, config,
content server, WebView, broker bridge), so with >1 napplet tab open the
controls (reload/back/pause/resume), navigation state, "allow always" notices,
NIP-07 traffic, and IME all routed to whichever tab was created last.

Collect all per-surface state into a NappletTab keyed by a client-stamped
session id (KEY_SESSION_ID on MSG_CREATE_SESSION and every control message):

- Controls/pause/resume resolve the target tab by id and act on its own WebView.
- Content server, shell handshake (declaredDomains), launch token, and page
  state/notices are per tab; onShellMessage resolves the tab by its WebView.
- Each tab gets its OWN reply Messenger, so broker responses AND unsolicited
  relay pushes come back tagged to the right tab — no id rewriting, per-tab
  origin/fire-seq state.
- onSessionClosed drops the tab and destroys only its own WebView; the broker is
  bound once for the whole service.

EmbeddedNappletController generates a unique id and stamps it on all messages.
Both embed hosts (browser + napplet) are now fully per-tab correct, including the
keyboard for multiple simultaneous tabs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 04:00:54 +00:00
Claude 09cd69ba7c feat: soft keyboard in embedded napplet/nsite tabs (same proxy, via the shell)
Extend the embedded keyboard to napplet/nsite surfaces. The shim's IME agent now
installs on any embedded surface (gated by __nappletImeProxy), reaching native
over whichever transport it has — the direct bridge for the browser, the trusted
shell relay for napplets — both through send().

- NappletContentServer gains an imeProxy flag that injects __nappletImeProxy
  before the shim; NappletHostService sets it (embedded), the full-screen
  NappletHostActivity leaves it off (native keyboard).
- NappletHostService relays ime.* between the applet (via the shell bridge) and
  the client (MSG_IME_EVENT / MSG_IME_OP) — the shell already forwards all
  message types, so no change to the trusted shell page.
- EmbeddedNappletController implements EmbeddedImeBridge, so EmbeddedTabLayer's
  RemoteImeView drives it exactly like the browser.

Correct for a single nsite/napplet tab. Multiple simultaneous napplet tabs share
the host service's single client/bridge pointer (same limitation as reload/back/
NIP-07 there) — making that per-tab needs the session-scoping the browser host
already got; tracked as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 03:50:47 +00:00
Claude f0d5e7a626 feat: soft keyboard in the embedded browser via a host-window input proxy
The embedded browser renders cross-process through SurfaceControlViewHost, which
forwards touch but not the soft keyboard (the embedded window can't be an IME
target, and androidx.privacysandbox.ui never wires IME). So focusing a field in
an embedded page did nothing.

Bridge the keyboard instead: host it in the main app window and relay editing to
the page.

- Shim IME agent (embedded browser only, gated by __nappletImeProxy): tracks the
  focused editable, reports focus/blur/external-change, and applies host ops
  (commit / compose / delete / key / editor-action) with real input & composition
  events. Scrolls the field into view on focus.
- NappletBrowserService relays ime.* envelopes between the page bridge and the
  client (MSG_IME_EVENT / MSG_IME_OP), per tab.
- EmbeddedBrowserController implements EmbeddedImeBridge (parses events, sends ops).
- RemoteImeView: an invisible EditText in the main window that takes the keyboard
  for the active tab. Keeps a real local Editable (so the platform handles
  composing/suggestions/selection) while an InputConnection wrapper forwards every
  op to the page. Maps web input types / enterKeyHint to inputType/IME action.
- EmbeddedTabLayer hosts the proxy bound to the active tab and shrinks the active
  surface by the IME height so the page can scroll the field clear of the keyboard.

Covers <input>/<textarea> fully and contenteditable best-effort (plain text).
Napplet/nsite embeds still need their own wiring (the shell path); this is the
browser surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 03:13:25 +00:00
Claude 408eb3a948 refactor: scope embedded browser sessions per-tab in the shared service
A single NappletBrowserService instance is shared by every embedded browser tab
(they bind the same Intent), but it kept one set of fields (webView, client
messenger, NIP-07 bridge state, reply messenger), so with >1 browser tab open:
controls (reload/Tor/back/navigate) hit whichever tab opened last, URL updates
went to the wrong address bar, and NIP-07 responses/pushes could land in the
wrong page.

Collect all per-surface state into a BrowserTab keyed by a client-stamped
session id (KEY_SESSION_ID on MSG_CREATE_SESSION and every control message):

- Controls resolve the target tab by session id and act on its own WebView.
- pushUrl delivers to that tab's own client messenger.
- Each tab gets its OWN reply Messenger, so broker responses AND unsolicited
  relay pushes come back already tagged to the right tab (the broker just echoes
  replyTo) — no id rewriting, and per-tab originTokens/mint state.
- onSessionClosed drops the tab and destroys only its own WebView.

EmbeddedBrowserController generates a unique session id and stamps it on all
messages. Tor note: the WebView proxy override is process-global (Android has no
per-WebView proxy), so toggling Tor still affects every tab; only the toggled
tab is reloaded. The napplet host service has the analogous shared-instance
shape (black-out already fixed) but isn't session-scoped yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 02:02:37 +00:00
Claude b474869213 fix: second embedded browser/napplet tab no longer blacks out the first
A single NappletBrowserService / NappletHostService instance is shared by every
embedded tab (they bind the same Intent), but each tab opens its own session
with its own WebView; the service's `webView` field is only a "latest" pointer.

The audit-batch "destroy stale WebView before rebuild" in createXWebView was
therefore destroying a *sibling* tab's live WebView whenever another browser/
napplet tab opened a session — exactly the repro: open A, switch to B (B's
create destroys A's WebView), back to A → black, B (created last) stays fine.

- Drop the destroy-on-create entirely; each session's WebView lives until that
  session closes.
- onSessionClosed now destroys the closing session's OWN WebView (passed in)
  and clears the shared pointer only if it still referenced it, so evicting one
  tab can't tear down another either.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 01:31:15 +00:00
Claude a0ad9082e5 fix: don't stretch the full-screen pull-down grabber across the whole width
A vertical LinearLayout defaults its children to MATCH_PARENT width, so the
collapsed grabber chip's rounded background spanned the entire screen. Give the
grabber explicit WRAP_CONTENT layout params (centered) so only the chip shows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 01:02:27 +00:00
Claude ba2af75b4c feat: replace full-screen sandbox corner chip with top pull-down sheet
The full-screen browser/napplet-host activities still showed the old corner
pill/globe — which sits exactly where a site puts its own login avatar. Add a
native-View NappletControlSheet (the twin of the embedded tabs' Compose
TopControlSheet): a small grabber at the top edge that pulls down to the page's
controls, and wire both :napplet full-screen activities to it.

- NappletControlSheet: title row (shield/globe), optional Tor row, reload, and
  optional "what it can access". Tor supports an inline toggle (browser) or a
  tap-through to a confirm dialog (nSite host, where switching rebuilds the
  session). Tap or vertical drag to expand/collapse.
- NappletBrowserActivity / NappletHostActivity: drop buildFloatingChip/chipGlyph
  for buildControlSheet(); attach at Gravity.TOP, full width.
- Add short napplet_net_tor_label / napplet_net_open_label strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:10:29 +00:00
Claude af9e8b885d refactor: apply embedded-tab audit fixes (durability, perf, leaks, cleanup)
Durability & correctness:
- FavoriteAppsRegistry: tombstone removals made before async hydration so a
  just-deleted favorite can't be resurrected by the disk merge.
- BottomBarEntry: stable @SerialName discriminators so persisted bottom-bar
  configs survive class renames/moves.
- EmbeddedTabHost: guard reportBounds/setActiveChrome by active id so a
  cross-fading outgoing screen can't clobber the incoming tab's bounds/chrome.
- EmbeddedNappletController: replay a parked-before-bound pause after session
  create so a never-shown applet doesn't come up running.
- NappletBrowserService: bind the broker once (no leaked binding on re-create),
  destroy a stale WebView before rebuilding, and reload only after the async
  proxy override actually applies. NappletHostService: same WebView-reuse guard.
- NappletBrokerService: cap concurrent foreground leases so a misbehaving
  sandbox can't pin Tor/relays with unbounded arbitrary keys.

Perf:
- Screens publish a remembered EmbeddedTabChrome; host short-circuits identical
  publishes so the tab layer isn't recomposed every frame.
- AppBottomBar resolves favorites via an id-indexed map, not a per-entry scan.
- TopControlSheet keyed on the active tab so its expand state resets per tab.

Cleanup:
- Delete dead BrowserHostActivity + EmbeddedBrowserSurface + AppControlPuck and
  their manifest entry; drop the unused `ready` surface state; null controller
  refs on unbind; refresh stale z-order/host docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:05:34 +00:00
Claude 89a32e078f fix: audit batch 1 — drop diagnostics, browser foreground heartbeat, Tor host key
- Remove the shipped scroll/zoom diagnostics from NappletBrowserService (per-touch
  MotionEvent log and per-page zoom log).
- NappletBrowserActivity now renews its foreground lease on a 30s heartbeat like
  NappletHostActivity, so the broker's 90s watchdog can't reap it (tearing down
  Tor/relays) while the browser is genuinely foreground.
- Persist the per-host Tor choice against the host actually displayed (webView.url),
  not the start URL, so an in-page navigation doesn't save the choice to the wrong site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:51:18 +00:00
Claude 90150e72e4 fix: upgrade privacysandbox.ui to alpha17 (embedded drag/scroll input)
The embedded surface forwarding taps but cancelling drags was a known bug in
androidx.privacysandbox.ui alpha10: with the provider surface z-ordered above,
"the gesture is exclusively received by the provider window and not transferred
to the client window" (alpha13 release notes). alpha15 then "set the default
Z-ordering to below" and "added support for the UI provider to receive
MotionEvents in this mode after being received by the client window" — i.e. the
drag-input path we needed.

Bump alpha10 → alpha17 and adapt the changed API: openSession takes SessionData
instead of a windowInputToken IBinder, Session adds notifySessionRendered, and
the session-state listener became setEventListener(SandboxedSdkViewEventListener)
(ready now flips on onUiDisplayed). The direct-WebView browser is unaffected
(it doesn't use this library).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:16:02 +00:00
Claude 1531f3baad feat: direct-WebView browser activity (scroll, zoom, keyboard)
Confirmed on a real device: the streamed SurfaceControlViewHost surface forwards
taps but drops scroll/zoom/keyboard gestures — a hard limitation of
androidx.privacysandbox.ui on current Android. No tweak fixes it.

Add NappletBrowserActivity: a full-screen browser that hosts the WebView
*directly* in its own window in the keyless :napplet process, so scrolling,
pinch-zoom, and the soft keyboard (windowSoftInputMode=adjustResize) all work
natively. It carries over NappletBrowserService's per-origin NIP-07 bridge and
Tor proxy, plus NappletHostActivity's trusted chip, loading screen, and
foreground hold — so it stays just as keyless (page JS runs in :napplet, every
window.nostr call is brokered + consent-gated per origin in the main process).

Web favorites and URL launches now open this activity instead of the streamed
BrowserHostActivity. Per-host Tor choice persists via a new MSG_SET_WEB_TOR
broker message (the :napplet process relays it to WebUrlNetworkRegistry).

The embedded bottom-row web tab still uses the streamed surface (it must, to
live inside MainActivity) and so still can't scroll — that's a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 22:46:31 +00:00
Claude cb8bbad38d feat: floating control puck for running app surfaces + remember Tor per web client
Replace the full-width top bar on every running app surface (embedded web/nsite/
napplet tabs and the full-screen browser + napplet activities) with a small
floating control puck. Apps already title themselves, so instead of repeating
the name we keep one always-visible trusted marker — the sandbox shield for
napplets/nsites (the anti-phishing affordance the page can't draw over), a globe
for the plain browser — that expands on tap to reveal the actions (Tor, reload,
pop-out, access sheet, close).

- New shared AppControlPuck composable backs the three Compose surfaces;
  NappletHostActivity gets the native-View equivalent.
- Embedded tabs inset their reserved surface bounds by the puck height
  (AppControlPuckReserve) so the warm surface, drawn over those bounds above the
  nav tree, doesn't cover the puck. Full-screen activities float it on top.
- EmbeddedTabTopBar is removed (no longer used).

Also remember the Tor on/off choice per web client: some sites' servers reject
Tor exits, so a user opting one out must have it stick. New WebUrlNetworkRegistry
(main process, keyed by host, device-local) mirrors NappletNetworkRegistry; the
browser reads it for the initial route and writes on toggle, in both the embedded
tab and the full-screen browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 21:29:15 +00:00
Claude 2115129a72 chore: add zoom diagnostic to embedded browser
Logs the WebView's current scale, density, and pixel widths on page finish, to
confirm whether the 400% zoom is a density/viewport mismatch from streaming the
WebView through SurfaceControlViewHost. Temporary, alongside the touch/session
diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 21:03:17 +00:00
Claude 0b5d7925fe chore: temporary diagnostics for embedded browser scrolling
Adds two temporary logs to pin down why the embedded browser doesn't scroll:
- provider side (NappletBrowserService): logs each MotionEvent that reaches the
  remote WebView, so we can see if touch crosses the SurfaceControlViewHost
  boundary at all (returns false, never consumes).
- client side (EmbeddedBrowserController): logs the SandboxedSdkView session
  state transitions (Idle/Loading/Active/Error).

To be reverted once the cause is confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:58:09 +00:00
Claude 17c6aba934 fix: theme the sandbox WebView background instead of flashing white
The embedded browser / nsite / napplet WebView runs in the keyless :napplet
process, which has no access to the main app's Compose theme, so before a page
painted it showed the WebView default white — jarring against Amethyst's (often
dark) background.

Pass the theme background color (MaterialTheme.colorScheme.background) across the
process boundary and apply it to the WebView, and paint the SandboxedSdkView
placeholder with it too so there's no white flash before the first frame. The
full-screen NappletHostActivity resolves the color locally from its themed
context. Covers the embedded browser tab, the full-screen browser activity, and
the embedded nsite/napplet tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:52:18 +00:00
Claude d99ae48847 feat: lease-expire sandbox foreground holds so a dead host can't pin the network
The cross-process foreground hold relied on the `:napplet` host delivering its
onPause "false" to release. If that process dies while foreground (a crash, an
OS kill) it never sends it, and the broker would hold the main process resumed —
Tor/relays/AUTH up — forever.

Turn the hold into a renewing lease. A resumed host re-reports foreground on a
30s heartbeat; the broker stamps each launch token's last-seen time and a
watchdog reaps any lease older than a 90s TTL, releasing its hold. A live app
keeps renewing so it's never wrongly dropped; a dead one stops renewing and is
reaped within the TTL, bounding any leak to one window instead of forever.

Only the cross-process napplet/nSite path needs this — BrowserHostActivity runs
in the main process, so if it dies the hold and every connection die with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:28:36 +00:00
Claude 62f0adaa76 feat: keep Tor/relays/AUTH up while a sandbox surface is foreground
Opening a full-screen napplet/nSite (`:napplet` process) or browser host
(main process) backgrounds MainActivity, stopping its ManageRelayServices /
ManageWebOkHttp collectors. The underlying WhileSubscribed flows then scale
everything down on their timers — Tor's port (~2s), the relay pool (~30s),
dropping the relay AUTH sessions with it — even though the user is still on a
Nostr surface that brokers NIP-07 + relays back through that very process.

Add a ref-counted SandboxForegroundHold (main process): while held it
subscribes to exactly the flows the resumed UI subscribes to, keeping Tor, the
relay pool, and AUTH up; it releases when the last surface leaves so normal
background scaling resumes. Main-process activities call it directly; the
`:napplet` host can't touch that lifecycle, so it signals foreground over a new
MSG_SET_FOREGROUND IPC and the broker holds on its behalf (token-set keyed,
released on unbind).

Switching between several open surfaces (old.onPause -> new.onResume, plus the
async IPC hop) would dip the count to 0 and back to 1. A short release linger
keeps the same collectors running straight through that dip, so swapping
between open napplets/nSites/browser apps holds the network perfectly steady.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:06:12 +00:00
Claude 2d3dd21695 fix: crash measuring embedded sandbox WebView (LayoutParams cast)
The surface adapters set the WebView's layoutParams to a plain
ViewGroup.LayoutParams, but the SurfaceControlViewHost container measures its
children with measureChildWithMargins, which casts to MarginLayoutParams —
crashing the :napplet process with a ClassCastException on first layout.

Use FrameLayout.LayoutParams (a MarginLayoutParams) in both
NappletHostUiAdapter and NappletBrowserUiAdapter (openSession + notifyResized).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:19:41 +00:00
Claude 601ba0a942 fix: stop embedded sandbox WebViews from double-applying system-bar insets
The full-screen NappletHostActivity already consumes system-bar + cutout
insets at its root so its WebView doesn't pad the page for the status/nav
bars a second time (targetSdk 35+ auto-applies received insets to web
content). The embedded surfaces — the browser tab and the new embedded
nsite/napplet tabs — have no such root: the WebView is the surface view, so
it received and re-applied those insets, leaving an empty band under the
status and navigation bars even though the host already places the surface
in the inset-free content area.

Add WebView.dropSystemBarInsets() (zeroes system-bar + display-cutout
insets, keeps IME for keyboard resize) and apply it in both
NappletBrowserService and NappletHostService when the session WebView is
built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 17:59:05 +00:00
Claude 25b530559e Merge remote-tracking branch 'origin/main' into claude/webview-menu-custom-url-ikrgbz 2026-06-23 17:38:09 +00:00
Claude e82f1057c8 feat: embed nsites/napplets as in-process tabs; pin them to the bottom bar
Completes the favorites system: a favorited nsite/napplet can now be
pinned to the bottom bar and render as an embedded, swap-in-place tab —
no longer only a full-screen activity launch.

New sandbox surface (:napplet, keyless), mirroring the browser embed:
- NappletHostService hosts the verified-blob WebView (same content server,
  shell bridge, and single launch-token broker path as NappletHostActivity)
  and ships it as a SandboxedUiAdapter surface, so applet JS still runs only
  in the keyless process — never where the keys live.
- NappletHostUiAdapter / NappletEmbedContract are the SurfaceControlViewHost
  adapter and the Messenger contract; the create-session bundle reuses
  NappletHostContract's EXTRA_* keys, so the embedded and full-screen host
  paths launch from identical, main-process-minted parameters.

Main process:
- NappletLauncher.buildLaunchParams extracts the verified param/token minting
  so both the activity intent and the embedded session share it.
- EmbeddedNappletController binds the service and attaches the surface
  (mirror of EmbeddedBrowserController).
- FavoriteNappletScreen draws the TRUSTED CHROME (sandbox shield, app name,
  "what it can access") in the main process around the surface — the sandbox
  must never draw chrome the user is meant to trust — plus a pop-out to the
  full-screen host. Capability consent still flows through the existing
  main-process broker + consent activity, unchanged and host-agnostic.

Security parity with the full-screen host:
- The applet's JS + timers are paused while the app is backgrounded
  (lifecycle ON_STOP/ON_START → MSG_PAUSE/MSG_RESUME), so an "allow always"
  napplet can't act on the user's behalf when they aren't looking.
- Granted sensitive ops (publish/upload/pay) surface a notice toast.

Both favorite kinds are now pinnable; the bottom bar routes WebUrl →
FavoriteWebApp and NostrApp → FavoriteNostrApp, each embedding in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 14:59:44 +00:00
Claude 785af41c99 refactor: remove dead full-screen browser path superseded by embedded surface
The full-screen NappletHostActivity browser-mode and NappletLauncher.launchBrowser
were added alongside the embedded browser but never wired into any UI. The embedded
surface (NappletBrowserService rendered via SurfaceControlViewHost) supersedes them
and draws the address bar in the trusted main process rather than inside the sandbox,
so this removes the weaker, unused surface.

Removed:
- NappletHostActivity browser-mode (setupBrowser, address bar, per-origin bridge) —
  reverted the host activity to its pre-browser state.
- NappletLauncher.launchBrowser and the EXTRA_BROWSER_MODE/EXTRA_BROWSER_URL extras.
- The now-unused nappletHost browser strings.

Kept (used by the embedded path): the broker per-origin token mint
(MSG_MINT_BROWSER_TOKEN / MSG_BROWSER_TOKEN / KEY_BROWSER_ORIGIN) in NappletBrokerService
and NappletIpc, so NIP-07 consent stays scoped per visited origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 13:12:00 +00:00
Claude d63bf87d2a feat: add in-app browser tab rendered from the keyless napplet sandbox
Adds a "Browser" navigation destination (drawer + pinnable bottom-nav item,
API 30+) that opens any URL. The page renders in the sandboxed, keyless
`:napplet` process and is streamed into the main activity as a cross-process
surface via androidx.privacysandbox.ui (SurfaceControlViewHost) — only pixels
and input cross the boundary, never the WebView's JS context or the NIP-07
bridge. The trusted address bar is drawn by the main process around the
embedded surface, so the sandbox can never spoof the URL.

NIP-07 `window.nostr` is injected the same way nSite website mode does it, but
scoped per visited origin: each origin gets its own broker-minted launch token
(keyed by the trusted source origin), so a grant to one site never leaks to
another.

- NappletBrowserService (`:napplet`): hosts the live-URL WebView, exposes it as
  a SandboxedUiAdapter, and relays the per-origin NIP-07 bridge to the broker.
- NappletBrowserUiAdapter: wraps the WebView session for privacysandbox.ui.
- NappletBrokerService: mints a per-origin synthetic identity so NIP-07 consent
  is scoped per host.
- EmbeddedBrowserController + BrowserScreen: bind the service, render the
  SandboxedSdkView, and drive the trusted address bar (navigate/reload/back/Tor).
- shim.js: a direct-bridge transport so the injected shim works in a top-level
  page that has no trusted shell parent.
- Browser nav item hidden below API 30 (SurfaceControlViewHost requirement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 03:39:06 +00:00
Claude 1f4ff91419 fix(build): give :nappletHost a benchmark variant for :amethyst benchmark builds
The new plain-AGP :nappletHost library only declared debug/release build
types (unlike the KMP :commons/:quartz/:nestsClient libs, which match any
build type). The :amethyst app's `benchmark` build type therefore had no
matching :nappletHost variant, breaking fdroidBenchmark/playBenchmark
resolution and Test/Build CI on main.

Declare a matching `benchmark` build type (initWith release) in :nappletHost
so the app resolves a real variant. Library AARs aren't signed, so no
signing config is needed here. :nappletHost is the only plain-AGP library
:amethyst depends on, so no consumer-side matchingFallbacks is required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tzXcqVUcAPhyDDFak4SQ8
2026-06-23 02:50:48 +00:00
Claude 20b42d3443 fix: prefetch off main thread; use real Tor logo in sandbox bar
- StrictMode DiskReadViolation: PrefetchManifestBlobs read context.cacheDir
  (ensurePrivateCacheDirExists touches disk) on the composition dispatcher.
  Move it into withContext(Dispatchers.IO); the prefetch was already IO-bound.
- Sandbox top-bar network indicator now uses the app's real Tor logo
  (ic_tor, copied into :nappletHost since it can't depend on :amethyst) via an
  ImageView, instead of the onion/globe emoji. Lit when routing through Tor,
  dimmed when the site loads over the open web.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 23:30:30 +00:00
Claude 7eb0949e5c feat: route nSite WebView traffic through Tor by default, per-site opt-out
nSites now load over Tor by default when Tor is active — closing the gap where
only blob fetches were Tor-routed while the site's own web traffic (fetch/img/
script) went out the system network and could leak the user's IP. Users can opt
a specific site out to the open web (e.g. a site that breaks or is slow over
Tor); the choice is remembered per site and makes everything for that site
direct (web + blobs).

- NappletHostActivity sets a process-wide WebView SOCKS proxy override
  (socks5://127.0.0.1:<torPort>) for website-mode nSites, or clears it for open
  web. Best-effort + on-device-verifiable: SOCKS-over-WebView support varies by
  WebView version, so it's isolated to applyWebViewProxy() and never breaks the
  site if unsupported.
- Top-bar onion (🧅 Tor / 🌐 open web) shows the routing and toggles it; the
  dialog explains the IP-privacy trade. Shown only for nSites when Tor is active.
- NappletNetworkRegistry: main-process per-site preference (coordinate-keyed,
  DataStore-backed, Tor-default). The launcher reads it; the broker persists the
  sandbox's toggle resolved through the launch token. The key-free sandbox never
  touches it.
- Toggling persists via a new MSG_SET_NETWORK_MODE IPC, then relaunches the host
  so the proxy + content server rebuild cleanly for the new mode.
- "Open web" also routes blob fetches direct (effective proxy -1); locked
  napplets always keep Tor for blobs (no toggle exposed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 23:10:12 +00:00
Vitor PamplonaandClaude Opus 4.8 d0d1577bfe fix(napplets): remove the stray white band at the bottom of the applet WebView
Two edge-to-edge issues on the napplet/nsite host, both surfacing as a white strip below
the applet on a light page:

- Double bottom inset. The root inset listener turned the system-bar/cutout insets into
  padding but returned them un-consumed, so the child WebView (which on targetSdk 35+
  auto-applies any insets it receives to its web content) padded the bottom a SECOND time.
  Zero those types before they reach the WebView, keeping IME flowing so keyboard resize
  still works.

- Over-scroll reveal. Forcing a scroll past the content edge stretched the view and
  exposed the shell document's background behind the applet iframe. WebView.overScrollMode
  only governs the outer frame, so also inject `overscroll-behavior: none` into the applet
  document (alongside the shim) to kill the stretch at the source, theme-independently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:01:42 -04:00
Claude c7d876096f feat: NIP-07 window.nostr provider for nSites
nSites now open in "website mode": a normal web app with normal network
access plus a NIP-07 window.nostr provider, so standard Nostr web apps can
"log in with Amethyst" and sign as the active user. Napplets are unchanged
(locked, declared-only sandbox).

- window.nostr (shim.js) installs only when the host sets __nappletNip07
  (website mode). getPublicKey/getRelays reuse the existing consent-gated
  identity reads; signEvent is a new sign-only op honoring the app-supplied
  created_at (no publish — the web app sends to relays itself).
- NappletRequest.SignEvent + nostr.signEvent decode; broker signs as the
  user and returns the signed event without publishing. pubkey is still
  fixed by the signer, so the app can never sign as another identity.
- Website mode: content server defers off-origin requests to the WebView
  and drops the app CSP (normal network); locked napplets keep connect-src
  'none' and 404 off-origin.
- Launcher grants IDENTITY + RELAY (consent-gated) for website mode,
  independent of the nSite's empty manifest requires.
- Consent dialog shows the kind + content preview for a sign request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:49:58 +00:00
Claude 42edc13be2 feat(napplets): back gesture pages back in the WebView, then exits to Amethyst
The sandbox host now routes the back gesture into the applet/site's own history
first: an OnBackPressedCallback calls webView.goBack() while there's history to
pop (in-page links, iframe navigations, and history.pushState all count), and
only disables itself — letting system back return to Amethyst — once the WebView
is at its first page. The callback's enabled state tracks canGoBack() via
doUpdateVisitedHistory / onPageFinished. Works with predictive back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:23:36 +00:00
Vitor PamplonaandClaude Opus 4.8 53d6a2f8b4 fix(napplets/nsites): render real SPAs in the sandbox (blank-page + reload-loop)
Static-site / SPA nApplets & nSites (Vite/nsyte/CRA/webpack output) rendered as a
blank page, then — once that was fixed — as a fast reload-loop blink. Three layered
causes, each found on-device via logcat:

1. Sub-path serving. The applet loaded under https://napplet.local/app/, but bundlers
   emit absolute asset URLs (/assets/app.js, /fonts/…) that resolve against the origin
   ROOT, so every script/style/font 404'd. nSites are defined to be hosted at the domain
   root; serve there.

2. Opaque-origin storage. The applet ran in an `allow-scripts`-only iframe, so its origin
   was opaque ("null"): module scripts + asset fetches were CORS-blocked, and reading
   localStorage/IndexedDB/serviceWorker threw SecurityError — which crash-loops every SPA
   (gruuv: "cache version 0 < 23 → reset → reload", forever, because IndexedDB never
   worked so the version never persisted).

Fix: give each applet its OWN real, persistent, isolated origin — a per-applet subdomain
https://<id>.napplet.local (id = sha256(author:identifier)), framed by the shell with
`allow-scripts allow-same-origin`. A real origin restores localStorage/IndexedDB/SW and
makes the applet's own assets same-origin (no CORS). Isolation is preserved because the
origin is DISTINCT from the shell's: the native bridge stays origin-restricted to the
shell (napplet.local), so the cross-origin applet still can't reach it or read the shell
DOM, and per-applet subdomains keep applets' storage isolated from each other. The shell
HTML's iframe src + CSP frame-src are bound to the specific applet origin at serve time.

Also add an in-memory localStorage/sessionStorage polyfill to the injected shim as
belt-and-suspenders for any context where DOM storage is still unavailable.

By-design sandbox enforcement is unchanged and correctly blocks the rest (external CDN
scripts, direct relay WebSockets via connect-src 'none', external images) — apps must go
through the napplet SDK for those.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:18:56 -04:00
Claude 176c96ef01 perf(napplets): faster opens — overlap WebView init, parallel prefetch, CAS fast-serve
Three load-time wins on the nApplet/nSite open path:

1. Overlap WebView/Chromium init with the index probe. The host now creates +
   warms the WebView (and binds the broker) in onCreate, so its slow first-in-
   process init runs concurrently with the IO availability probe instead of
   serially after it; the probe just attaches the ready WebView.
2. Prefetch in parallel, index-first. NappletBlobPrefetcher downloads a
   manifest's blobs with bounded concurrency (5) and fetches index.html first,
   instead of strictly sequentially — faster first paint on cold opens.
3. Serve CAS hits without re-hashing. The content server short-circuits a
   content-addressed cache hit (verified on write, addressed by sha256) straight
   to the response, skipping the resolver's per-serve sha256 over the whole blob
   — the dominant CPU cost for large bundles. Network (cache-miss) path still
   fully verifies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:07:09 +00:00
Claude 31bd34e838 feat(napplets): prefetch blobs to disk on render + loading/unavailable screen
Faster opens: while an nApplet/nSite card is on screen, eagerly download +
sha256-verify all of its blobs (Tor-routed) into a shared content-addressed
cache, so tapping Open serves from disk. Prefetch is de-duplicated (in-flight +
on-disk) and cancellation-aware (stops when the card scrolls away).

- New :nappletHost pieces: NappletBlobCache (content-addressed, multi-process-safe
  atomic store, replacing the single-process OkHttp DiskLruCache), NappletBlobHttp
  (shared Tor OkHttp + size-capped download), NappletBlobPrefetcher.
- NappletContentServer now reads the shared cache first, falling back to a Tor
  download that refills it; still re-verifies every blob on serve.
- Cards trigger prefetch via a LaunchedEffect in the StaticWebsite render fns.

Nicer launch: NappletHostActivity probes the index (resolving + caching it)
behind a loading screen (monogram + title + spinner), then shows the app, or a
clear "couldn't load — try again" screen if the publisher's servers are offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 21:32:48 +00:00
Claude 4218a7c1af refactor(napplets): extract sandbox runtime into :nappletHost module
The `:napplet` sandbox runtime (NappletHostActivity, NappletContentServer,
NappletIpc, NappletKeyActions) now lives in a new :nappletHost Android library
that depends only on :commons + :quartz — NEVER :amethyst. So the sandbox code
is compile-time incapable of importing Amethyst.instance / LocalCache / Account,
turning the "two-process, no secrets in the sandbox" rule from a convention into
a build-graph guarantee.

- New module + NappletHostContract (Intent-extra keys + broker service FQN), so
  the launcher (amethyst) and activity (module) share the launch contract with no
  dependency cycle. The activity binds the broker by class name.
- Capability labels for the "what it can access" sheet are resolved by the
  launcher (which has app resources) and passed in, so the module needs no
  capability string resources. Host-only strings moved into the module.
- amethyst depends on :nappletHost; the broker-side (NappletBrokerService,
  gateways, NappletLaunchRegistry) stays in :amethyst. Manifest declares the
  activity by FQN (keeps @style/Theme.Amethyst resolvable).
- Docs updated (CLAUDE.md + security plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 20:45:19 +00:00