Skip to content
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

## Unreleased

Runtime Web Overrides parity.

- Applies configured Custom CSS to trusted app pages without modifying external pages.
- Executes configured Custom JavaScript on trusted pages after navigation completes.
- Adds cookie persistence modes for encrypted persistent sessions, website/server defaults and session-only starts.
- Preserves legacy Quantum behavior by treating older `default` cookie settings as persistent.
- Keeps Android 6 / API 23 compatibility and the existing trusted-origin boundary.

Runtime Web Overrides parity.

- Applies configured Custom CSS to trusted app pages without modifying external pages.
- Executes configured Custom JavaScript on trusted pages after navigation completes.
- Adds cookie persistence modes for encrypted persistent sessions, website/server defaults and session-only starts.
- Preserves legacy Quantum behavior by treating older `default` cookie settings as persistent.
- Keeps Android 6 / API 23 compatibility and the existing trusted-origin boundary.

Configurable native WebView loading indicators.

- Preserves the current top loading bar as the default for existing apps.
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/de/starlightunit/wrapper/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import de.starlightunit.wrapper.launch.QuantumIntroController;
import de.starlightunit.wrapper.media.QuantumNativeMediaPlayer;
import de.starlightunit.wrapper.navigation.NavigationPolicy;
import de.starlightunit.wrapper.session.CookiePersistencePolicy;
import de.starlightunit.wrapper.session.QuantumSessionCookieStore;
import de.starlightunit.wrapper.ui.LoadingIndicatorController;
import de.starlightunit.wrapper.web.GameWebChromeClient;
Expand Down Expand Up @@ -84,8 +85,13 @@ protected void onCreate(Bundle savedInstanceState) {
introController.start(savedInstanceState != null);

WebViewConfigurator.configure(this, webView);
sessionCookieStore = new QuantumSessionCookieStore(this);
sessionCookieStore.restore();
if (CookiePersistencePolicy.usesEncryptedPersistence(AppConfig.COOKIE_PERSISTENCE_MODE)) {
sessionCookieStore = new QuantumSessionCookieStore(this);
sessionCookieStore.restore();
} else if (savedInstanceState == null
&& CookiePersistencePolicy.startsFreshSession(AppConfig.COOKIE_PERSISTENCE_MODE)) {
Comment on lines +91 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear session cookies after process-state restoration

When Android kills the app in the background and later recreates this Activity, it supplies a non-null savedInstanceState even though a new app process and cookie session have started. In session mode this condition therefore skips the fresh-session clear; any server-persistent authentication cookie remains available while restoreState() resumes the prior page, defeating session-only behavior after a common process-death restore. Session initialization must distinguish a new process from an in-process Activity recreation rather than relying solely on savedInstanceState.

Useful? React with 👍 / 👎.

CookiePersistencePolicy.clearForFreshSession();
Comment on lines +88 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the encrypted cookie backup when leaving persistent mode

When an installed app changes from the legacy/persistent mode to server or session, this branch stops constructing QuantumSessionCookieStore but leaves its previously captured encrypted payload in SharedPreferences. If a later app update selects persistent again, restore() revives that stale session cookie, including one that the session-mode startup was supposed to discard. Non-persistent modes should delete the custom store's saved payload as part of the mode transition.

Useful? React with 👍 / 👎.

}

navigationPolicy = new NavigationPolicy(AppConfig.TRUSTED_DOMAIN);
requestHeaders = WrapperRequestHeaders.create();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public final class AppConfig {
public static final String APP_HEADER_NAME = "X-Starlight-App";
public static final String APP_HEADER_VALUE = "starlight-unit-game";
public static final String CUSTOM_REQUEST_HEADERS_JSON = "{}";
public static final String CUSTOM_CSS = "";
public static final String CUSTOM_JAVASCRIPT = "";
public static final String COOKIE_PERSISTENCE_MODE = "persistent";

public static final String LOADING_INDICATOR_STYLE = "top-bar";
public static final String LOADING_INDICATOR_COLOR = "#6fc7ff";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.starlightunit.wrapper.session;

import android.webkit.CookieManager;

import java.util.Locale;

public final class CookiePersistencePolicy {
public static final String MODE_PERSISTENT = "persistent";
public static final String MODE_SERVER = "server";
public static final String MODE_SESSION = "session";

private CookiePersistencePolicy() {
}

public static String normalize(String configuredMode) {
String mode = configuredMode == null
? ""
: configuredMode.trim().toLowerCase(Locale.ROOT);
switch (mode) {
case MODE_SERVER:
case MODE_SESSION:
case MODE_PERSISTENT:
return mode;
case "default":
case "":
default:
// Legacy Quantum builds always persisted trusted-origin session
// cookies. Keep that behavior unless a profile explicitly opts
// into server defaults or session-only mode.
return MODE_PERSISTENT;
}
}

public static boolean usesEncryptedPersistence(String configuredMode) {
return MODE_PERSISTENT.equals(normalize(configuredMode));
}

public static boolean startsFreshSession(String configuredMode) {
return MODE_SESSION.equals(normalize(configuredMode));
}

public static void clearForFreshSession() {
CookieManager cookies = CookieManager.getInstance();
cookies.removeAllCookies(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for cookie deletion before starting navigation

When COOKIE_PERSISTENCE_MODE is session and cookies remain from a previous Activity instance, removeAllCookies(null) performs deletion asynchronously, but MainActivity.onCreate() proceeds directly to the initial loadUrl. That first request can therefore still carry the previous authentication cookie, and deletion may then occur after the new page has begun establishing its session. Supply a callback and defer initial navigation until removal completes.

Useful? React with 👍 / 👎.

cookies.flush();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) {
@Override
public void onPageFinished(WebView view, String url) {
if (navigationPolicy.isTrustedHttps(url)) {
WebOverrides.apply(view);
Comment on lines 81 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard overrides against the document active at execution

When a trusted page rapidly navigates to an in-WebView data:, about:, or blob: document, this callback can still arrive with the completed trusted URL while evaluateJavascript() asynchronously targets the newer document. Because those schemes are deliberately allowed to remain in the WebView, the configured CSS and arbitrary JavaScript can execute on content outside the trusted HTTPS boundary. The injected script itself should verify the active document's origin at execution time rather than relying only on the callback URL.

Useful? React with 👍 / 👎.

campaignAudioHandoff.inject(view);
if (assetStore != null && AppConfig.ASSET_STORE_PAGE_WARMUP_ENABLED) {
view.postDelayed(
Expand Down
46 changes: 46 additions & 0 deletions app/src/main/java/de/starlightunit/wrapper/web/WebOverrides.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package de.starlightunit.wrapper.web;

import android.webkit.WebView;

import org.json.JSONObject;

import de.starlightunit.wrapper.config.AppConfig;

/**
* Applies profile-provided CSS and JavaScript only after the caller has
* confirmed that the current page belongs to the trusted app origin.
*/
public final class WebOverrides {
private static final String STYLE_ELEMENT_ID = "quantum-profile-custom-css";

private WebOverrides() {
}

public static void apply(WebView webView) {
String css = AppConfig.CUSTOM_CSS;
if (css != null && !css.trim().isEmpty()) {
webView.evaluateJavascript(cssInjectionScript(css), null);
}

String javascript = AppConfig.CUSTOM_JAVASCRIPT;
if (javascript != null && !javascript.trim().isEmpty()) {
webView.evaluateJavascript(javascriptInjectionScript(javascript), null);
}
}

static String cssInjectionScript(String css) {
String quotedCss = JSONObject.quote(css == null ? "" : css);
return "(function(){"
+ "var d=document;"
+ "var s=d.getElementById('" + STYLE_ELEMENT_ID + "');"
+ "if(!s){s=d.createElement('style');s.id='" + STYLE_ELEMENT_ID + "';"
+ "(d.head||d.documentElement).appendChild(s);}"
+ "s.textContent=" + quotedCss + ";"
+ "})();";
}

static String javascriptInjectionScript(String javascript) {
String source = javascript == null ? "" : javascript;
return source + "\n//# sourceURL=quantum-profile-custom.js";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package de.starlightunit.wrapper.session;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public final class CookiePersistencePolicyTest {

@Test
public void persistentIsDefaultAndLegacyDefaultRemainsPersistent() {
assertEquals("persistent", CookiePersistencePolicy.normalize(null));
assertEquals("persistent", CookiePersistencePolicy.normalize(""));
assertEquals("persistent", CookiePersistencePolicy.normalize("default"));
assertTrue(CookiePersistencePolicy.usesEncryptedPersistence("persistent"));
assertTrue(CookiePersistencePolicy.usesEncryptedPersistence("default"));
}

@Test
public void serverAndSessionModesRemainDistinct() {
assertEquals("server", CookiePersistencePolicy.normalize("server"));
assertEquals("session", CookiePersistencePolicy.normalize("session"));
assertFalse(CookiePersistencePolicy.usesEncryptedPersistence("server"));
assertFalse(CookiePersistencePolicy.usesEncryptedPersistence("session"));
assertTrue(CookiePersistencePolicy.startsFreshSession("session"));
assertFalse(CookiePersistencePolicy.startsFreshSession("server"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.starlightunit.wrapper.web;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public final class WebOverridesTest {

@Test
public void cssInjectionEscapesContentAndUsesStableStyleElement() {
String script = WebOverrides.cssInjectionScript("body { color: red; }\n.x { display: block; }");
assertTrue(script.contains("quantum-profile-custom-css"));
assertTrue(script.contains("body { color: red; }\\n.x { display: block; }"));
assertTrue(script.contains("s.textContent="));
}

@Test
public void javascriptInjectionPreservesSourceAndAddsDebugSourceName() {
String source = "window.quantumTest = (window.quantumTest || 0) + 1;";
String script = WebOverrides.javascriptInjectionScript(source);
assertTrue(script.startsWith(source));
assertTrue(script.endsWith("//# sourceURL=quantum-profile-custom.js"));
}

@Test
public void nullJavascriptStillProducesValidDebugSource() {
assertEquals("\n//# sourceURL=quantum-profile-custom.js", WebOverrides.javascriptInjectionScript(null));
}
}
Loading