SkateSpots/android-templates/MainActivity.java
shakespeare.diy c7e890e65c Citrine: allow loopback ws:// in WebView so the local relay connects (1.3.5)
The real fix for "Citrine running but 0 connections": the app is served over
https in the WebView and Citrine listens on plaintext ws://127.0.0.1, which
modern Android WebViews block as mixed content BEFORE it reaches the network
layer — allowMixedContent + the loopback network-security-config don't fully
cover WebSocket mixed content.

MainActivity now sets the bridge WebView's mixed-content mode to
MIXED_CONTENT_ALWAYS_ALLOW after super.onCreate(), which lets the localhost
ws:// connection through for every user. All real traffic stays wss:///https://;
this only unblocks the loopback relay. The WebViewClient is left untouched
(overriding it previously caused a black screen). Wrapped in try/catch so it can
never crash the launcher.

Also removed the temporary on-screen probe debug readout (kept the diagnostic
fields in useCitrine, unused by UI, for future use).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 10:47:39 +07:00

78 lines
3.5 KiB
Java

package org.skatespots.app;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import com.getcapacitor.BridgeActivity;
/**
* SkateSpots launcher activity.
*
* SAFE BY DESIGN: this extends the stock Capacitor BridgeActivity and does NOT
* override the WebViewClient (an earlier custom version did and caused a
* black-screen crash). The ONLY thing we add is the registration of the
* ActivityResultLauncher used by AmberIntentPlugin.
*
* WHY THE LAUNCHER LIVES HERE
* ───────────────────────────
* AndroidX requires registerForActivityResult(...) to be called BEFORE the
* Activity is STARTED (i.e. during onCreate). A Capacitor Plugin is instantiated
* too late to register its own launcher, so we create it here and hand it to the
* plugin via AmberIntentPlugin.setLauncher(). Results are routed straight into
* AmberIntentPlugin.handleResult().
*
* This is the Amethyst NIP-55 pattern: a foreground-intent signer whose result
* comes back through a properly-registered ActivityResultLauncher — the reliable
* path that OPENS Amber's Approve/Reject screen and never hangs, on stock Android
* and on GrapheneOS alike.
*
* Injected into the generated android/ project at build time by
* scripts/inject-main-activity.mjs.
*/
public class MainActivity extends BridgeActivity {
private ActivityResultLauncher<Intent> amberLauncher;
@Override
public void onCreate(Bundle savedInstanceState) {
// Register the launcher BEFORE super.onCreate() puts us past STARTED.
amberLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> AmberIntentPlugin.handleResult(result)
);
AmberIntentPlugin.setLauncher(amberLauncher);
// Register our native plugin so the WebView can call it.
registerPlugin(AmberIntentPlugin.class);
super.onCreate(savedInstanceState);
// ── Allow the on-device Citrine relay (ws://127.0.0.1) ──────────────
// The app is served to the WebView over the https scheme, and Citrine
// listens on a PLAINTEXT WebSocket (ws://, not wss://). Modern Android
// WebViews treat "secure page → insecure ws" as mixed content and block
// it BEFORE it ever reaches the network layer — which is why the local
// relay showed 0 connections despite allowMixedContent + the loopback
// network-security-config (neither fully covers WebSocket mixed content).
//
// MIXED_CONTENT_ALWAYS_ALLOW on the bridge WebView lets the localhost
// ws:// connection through. All real traffic remains wss:// / https://;
// this only unblocks the loopback relay. We do NOT touch the
// WebViewClient (overriding it previously caused a black-screen crash).
try {
WebView webView = getBridge().getWebView();
if (webView != null) {
WebSettings settings = webView.getSettings();
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
} catch (Exception e) {
// Non-fatal: if this fails the app still runs, the local relay just
// won't connect. Never crash the launcher over an optional feature.
}
}
}