diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b17adf7..b8785d0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -131,6 +131,7 @@ android { "FIREBASE_GCM_SENDER_ID", quotedBuildConfig(firebaseSenderId.get()) ) + buildConfigField("boolean", "SAFE_PAGE_LOAD_DIAGNOSTICS", "false") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -178,6 +179,7 @@ android { "BASE_URL", "\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\"" ) + buildConfigField("boolean", "SAFE_PAGE_LOAD_DIAGNOSTICS", "true") manifestPlaceholders["usesCleartextTraffic"] = "false" manifestPlaceholders["deepLinkHost"] = releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local" @@ -197,6 +199,7 @@ android { "BASE_URL", "\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\"" ) + buildConfigField("boolean", "SAFE_PAGE_LOAD_DIAGNOSTICS", "true") manifestPlaceholders["usesCleartextTraffic"] = "false" manifestPlaceholders["deepLinkHost"] = releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local" diff --git a/android/app/src/androidTest/java/org/whoneedhelp/mobile/PublicStagingInstrumentedTest.java b/android/app/src/androidTest/java/org/whoneedhelp/mobile/PublicStagingInstrumentedTest.java index 3f09672..97a7492 100644 --- a/android/app/src/androidTest/java/org/whoneedhelp/mobile/PublicStagingInstrumentedTest.java +++ b/android/app/src/androidTest/java/org/whoneedhelp/mobile/PublicStagingInstrumentedTest.java @@ -4,6 +4,7 @@ import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches; import static androidx.test.espresso.web.sugar.Web.onWebView; import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement; import static androidx.test.espresso.web.webdriver.DriverAtoms.getText; +import static org.junit.Assert.assertTrue; import static org.hamcrest.Matchers.containsString; import android.content.Context; @@ -28,10 +29,15 @@ public final class PublicStagingInstrumentedTest { @Test public void publicHomeAndSafetyDeepLinkRenderExpectedDom() throws Exception { + assertTrue( + "Public non-production smoke builds must expose redacted page-load diagnostics", + BuildConfig.SAFE_PAGE_LOAD_DIAGNOSTICS + ); + try (ActivityScenario scenario = launch("/")) { waitForElementText( "main-content", - "Help can be closer than you think." + "Need help nearby? Ask the community." ); } diff --git a/android/app/src/main/java/org/whoneedhelp/mobile/MainActivity.java b/android/app/src/main/java/org/whoneedhelp/mobile/MainActivity.java index ebc1c4b..a3560a9 100644 --- a/android/app/src/main/java/org/whoneedhelp/mobile/MainActivity.java +++ b/android/app/src/main/java/org/whoneedhelp/mobile/MainActivity.java @@ -61,6 +61,8 @@ import org.json.JSONObject; public final class MainActivity extends ComponentActivity { private static final String LOG_TAG = "WhoNeedHelpWebView"; + private static final boolean PAGE_LOAD_DIAGNOSTICS = + BuildConfig.DEBUG || BuildConfig.SAFE_PAGE_LOAD_DIAGNOSTICS; private WebView webView; private TrustedOrigin trustedOrigin; private GeolocationPermissions.Callback pendingLocationCallback; @@ -201,13 +203,26 @@ public final class MainActivity extends ComponentActivity { } private static String safeLogPath(Uri uri) { + if (uri == null) { + return "/"; + } + String path = uri.getPath(); if (path != null && path.startsWith("/users/log-in/")) { return "/users/log-in/[redacted]"; } - return path; + return path == null || path.isEmpty() ? "/" : path; + } + + private static void logMainFrameFailure(String detail, Uri uri) { + if (PAGE_LOAD_DIAGNOSTICS) { + Log.e( + LOG_TAG, + "Main-frame load failed: " + detail + " path=" + safeLogPath(uri) + ); + } } @Override @@ -886,6 +901,10 @@ public final class MainActivity extends ComponentActivity { SslError error ) { mainFrameLoadFailed = true; + logMainFrameFailure( + "ssl_error=" + error.getPrimaryError(), + Uri.parse(error.getUrl() == null ? "" : error.getUrl()) + ); handler.cancel(); showPageLoadError(R.string.secure_connection_failed); } @@ -898,17 +917,7 @@ public final class MainActivity extends ComponentActivity { ) { if (request.isForMainFrame()) { mainFrameLoadFailed = true; - - if (BuildConfig.DEBUG) { - Log.e( - LOG_TAG, - "Main-frame load failed: code=" - + error.getErrorCode() - + " path=" - + safeLogPath(request.getUrl()) - ); - } - + logMainFrameFailure("code=" + error.getErrorCode(), request.getUrl()); showPageLoadError(R.string.page_load_failed); } } @@ -921,6 +930,10 @@ public final class MainActivity extends ComponentActivity { ) { if (request.isForMainFrame()) { mainFrameLoadFailed = true; + logMainFrameFailure( + "http_status=" + errorResponse.getStatusCode(), + request.getUrl() + ); showPageLoadError(R.string.page_load_failed); } } @@ -930,7 +943,7 @@ public final class MainActivity extends ComponentActivity { mainFrameLoadFailed = false; dismissPageLoadError(); - if (BuildConfig.DEBUG) { + if (PAGE_LOAD_DIAGNOSTICS) { Log.d(LOG_TAG, "Main-frame load started: path=" + safeLogPath(Uri.parse(url))); } @@ -946,7 +959,7 @@ public final class MainActivity extends ComponentActivity { dismissPageLoadError(); } - if (BuildConfig.DEBUG) { + if (PAGE_LOAD_DIAGNOSTICS) { Log.d(LOG_TAG, "Main-frame load finished: path=" + safeLogPath(Uri.parse(url))); scheduleMapDiagnostics(view, Uri.parse(url)); } diff --git a/android/app/src/test/java/org/whoneedhelp/mobile/BuildTypeConfigurationTest.java b/android/app/src/test/java/org/whoneedhelp/mobile/BuildTypeConfigurationTest.java new file mode 100644 index 0000000..6bba2fc --- /dev/null +++ b/android/app/src/test/java/org/whoneedhelp/mobile/BuildTypeConfigurationTest.java @@ -0,0 +1,16 @@ +package org.whoneedhelp.mobile; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public final class BuildTypeConfigurationTest { + @Test + public void safePageLoadDiagnosticsAreLimitedToPublicNonProductionBuilds() { + boolean expected = + BuildConfig.APPLICATION_ID.endsWith(".development") + || BuildConfig.APPLICATION_ID.endsWith(".staging"); + + assertEquals(expected, BuildConfig.SAFE_PAGE_LOAD_DIAGNOSTICS); + } +} diff --git a/docs/verification.md b/docs/verification.md index c69c7aa..6806156 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -1413,6 +1413,22 @@ None of the observations below describe the current delivery path. production pipeline separately passed release tests, lint, R8/resource shrinking, APK/AAB signing checks, Bundletool validation, and production App Links identity validation. +- The API 37 development emulator smoke now passes the signed + `org.whoneedhelp.mobile.development` APK against + `https://whoneedhelp.imalto.site`. It observed the home and `/safety` + main-frame loads, asserted both DOMs, rejected an unrelated HTTPS origin, + found no load/TLS error, and captured the rendered phone screenshots. + Evidence is retained at + `output/android-development-smoke/20260723225620-2568739`. Its run-scoped + container, AVD volume, and image were all absent after cleanup. +- This replay exposed and fixed two stale checks rather than treating a build as + runtime proof. Development/staging APKs deliberately have Android debugging + disabled, so their previous smoke harness waited for a debug-only page-load + message even after WebView rendered successfully. Redacted path-only load + diagnostics are now enabled only for the signed non-production public build + types, while WebView debugging and the release build remain disabled. The + public DOM assertion was also updated from the retired hero copy to the + current `Need help nearby? Ask the community.` heading. - The complete isolated quality/security gate passed with 353 ExUnit tests, ShellCheck, Hadolint, actionlint, Compose/Helm validation, format/compiler, xref, Credo, Sobelow, Dialyzer, Hex/npm audits, source scanning, and all @@ -1459,14 +1475,17 @@ None of the observations below describe the current delivery path. after that promotion; the current test origin still depends on its configured workstation/VPN/gateway path. - The final Android application ID is `org.whoneedhelp.mobile`. The application - publishes environment-specific `/.well-known/assetlinks.json`. The previous - staging identity was checked against its HTTPS response; repeat that online - check for the new `org.whoneedhelp.mobile.development` identity after the - controlled development rebuild. Before a Play release, register the - application, add the Play App Signing certificate fingerprint alongside any - sideload/upload fingerprint, repeat Android's domain verification on a - device, and complete store policy/release work. A dedicated upload key and - signed APK/AAB exist, but no Play application has been registered. + publishes environment-specific `/.well-known/assetlinks.json`. The online + development response now agrees with + `org.whoneedhelp.mobile.development` and its signed certificate, and the + explicit same-origin deep link rendered in the API 37 smoke. Android still + reported the fresh emulator's domain-verification state as `none`, so a + verified implicit App Link has not yet been observed on a device. Before a + Play release, register the application, add the Play App Signing certificate + fingerprint alongside any sideload/upload fingerprint, repeat Android's + domain verification on a device, and complete store policy/release work. A + dedicated upload key and signed APK/AAB exist, but no Play application has + been registered. - Operate PostgreSQL/PostGIS with off-site backups, recovery testing, monitoring, and the availability model selected for real usage. - The development Brevo SMTP transport and sender have completed both an