SkateSpots/android-templates/MainActivity.java
shakespeare.diy fea9ab3091 fix(android): native intent hand-off so Amber actually opens for signing (1.2.8)
Amber never appeared for delete/accept-invite/zap because Capacitor's default
WebView silently drops JS-initiated intent:/nostrsigner: navigations — the
request that opens Amber never reached the OS. Login worked only because it
uses the plugin's native startActivityForResult.

Add a custom MainActivity (android-templates/MainActivity.java) that overrides
shouldOverrideUrlLoading to launch intent:/nostrsigner: URLs via startActivity()
— a real native launch that opens Amber on GrapheneOS, matching login. Injected
into the generated android/ project at build time by inject-main-activity.mjs,
wired into android:build/sync/init. Simplify the JS launcher to navigate the
WebView (now intercepted natively). Bump to 1.2.8.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 21:23:38 +07:00

101 lines
3.8 KiB
Java

package org.skatespots.app;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.getcapacitor.BridgeActivity;
/**
* Custom MainActivity that lets the WebView hand OFF `intent:` / `nostrsigner:`
* URLs to the Android OS so Amber's approval screen actually opens.
*
* WHY THIS EXISTS
* ───────────────
* When the JS signer navigates to an `intent:...;scheme=nostrsigner;...;end`
* URL, Capacitor's default WebView treats it as a page navigation and silently
* DROPS it — so Amber never launches and every signing action (delete, accept
* crew invite, zap, profile edit) hangs forever. Login works only because it
* goes through the native plugin's startActivityForResult; JS-initiated intents
* were never reaching the OS. On GrapheneOS this is enforced strictly.
*
* The fix: override shouldOverrideUrlLoading. Whenever the WebView tries to load
* an `intent:` or `nostrsigner:` URL, we parse it and call startActivity()
* ourselves — a REAL native launch that opens Amber, exactly like login. The
* result comes back via the app's custom-scheme deep link
* (org.skatespots.app://amber-callback...), handled by DeepLinkHandler /
* resolveAmberIntent in the web layer.
*
* This file is injected into the generated android/ project at build time by
* scripts/inject-main-activity.mjs (android/ is not committed).
*/
public class MainActivity extends BridgeActivity {
private static final String TAG = "SkateSpotsAmber";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final WebView webView = getBridge().getWebView();
if (webView == null) {
return;
}
final WebViewClient existing = new WebViewClient();
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (handleExternalScheme(url)) {
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) {
final String url = request.getUrl() != null ? request.getUrl().toString() : null;
if (url != null && handleExternalScheme(url)) {
return true;
}
return super.shouldOverrideUrlLoading(view, request);
}
});
}
/**
* If the URL is an Amber/signer intent, launch it natively and return true.
* Returns false for normal in-app URLs so Capacitor handles them as usual.
*/
private boolean handleExternalScheme(String url) {
if (url == null) {
return false;
}
try {
if (url.startsWith("intent:")) {
// Parse the full Android intent: URI (carries scheme, extras, end).
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
return true;
}
if (url.startsWith("nostrsigner:")) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
return true;
}
} catch (Exception e) {
Log.e(TAG, "Failed to launch external signer intent: " + e.getMessage(), e);
// Fall through — let the WebView try to handle it (will no-op safely).
}
return false;
}
}