From d89cff242b7162ef17f98a190ec0a1c2497ce736 Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:28:35 +0200 Subject: [PATCH 01/10] Add custom splash attribution runtime flags --- .../main/java/de/starlightunit/wrapper/config/AppConfig.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java b/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java index 4cc25d5..1151c07 100644 --- a/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java +++ b/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java @@ -50,6 +50,10 @@ public final class AppConfig { public static final int LOADING_SPINNER_SIZE_DP = 56; public static final int LOADING_OVERLAY_DIM_PERCENT = 35; + public static final boolean CUSTOM_SPLASH_ENABLED = false; + public static final boolean ATTRIBUTION_BANNER_ENABLED = false; + public static final int ATTRIBUTION_BANNER_DURATION_MS = 3000; + public static final boolean KEEP_SCREEN_ON = true; public static final boolean PAGE_TRANSITIONS_ENABLED = false; public static final boolean TOP_NAVIGATION_ENABLED = false; From b3ebc7cb6738aab2e1bcf55eeca2d7b6ee5303cf Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:28:55 +0200 Subject: [PATCH 02/10] Make custom and production splash mutually exclusive --- .../launch/QuantumIntroController.java | 98 +++++++++---------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/de/starlightunit/wrapper/launch/QuantumIntroController.java b/app/src/main/java/de/starlightunit/wrapper/launch/QuantumIntroController.java index 7aadca4..a74e6f8 100644 --- a/app/src/main/java/de/starlightunit/wrapper/launch/QuantumIntroController.java +++ b/app/src/main/java/de/starlightunit/wrapper/launch/QuantumIntroController.java @@ -10,44 +10,67 @@ import de.starlightunit.wrapper.R; /** - * Controls the mandatory Starlight Quantum production splash and the optional - * app-owner intro. The production splash is always first. A generated wrapper - * may optionally provide a drawable named {@code quantum_user_intro}; when it - * is absent the app proceeds directly to the WebView. + * Shows exactly one startup splash. Generated apps may replace the canonical + * Starlight production splash with a profile-provided drawable named + * quantum_custom_splash. The two splash variants are mutually exclusive. */ public final class QuantumIntroController { - private static final long PRODUCTION_SPLASH_MS = 2500L; - private static final long USER_INTRO_MS = 2000L; + public interface Completion { + void onIntroFinished(boolean customSplashUsed); + } + + private static final long SPLASH_MS = 2500L; private static final long FADE_MS = 180L; private final Activity activity; private final ImageView overlay; + private final boolean customSplashEnabled; + private final Completion completion; private final Handler handler = new Handler(Looper.getMainLooper()); private boolean cancelled; - - public QuantumIntroController(Activity activity, ImageView overlay) { + private boolean customSplashUsed; + private boolean completionDelivered; + + public QuantumIntroController( + Activity activity, + ImageView overlay, + boolean customSplashEnabled, + Completion completion + ) { this.activity = activity; this.overlay = overlay; + this.customSplashEnabled = customSplashEnabled; + this.completion = completion; } public void start(boolean restoredActivityState) { cancelPending(); cancelled = false; + completionDelivered = false; + customSplashUsed = false; + + int splashResource = R.drawable.quantum_production_splash; + if (customSplashEnabled) { + int custom = activity.getResources().getIdentifier( + "quantum_custom_splash", + "drawable", + activity.getPackageName() + ); + if (custom != 0) { + splashResource = custom; + customSplashUsed = true; + } + } - // The production attribution is mandatory for every newly created - // MainActivity. Do not suppress it just because Android supplied a - // restored state after an update/process recreation. overlay.animate().cancel(); - overlay.setImageResource(R.drawable.quantum_production_splash); + overlay.setImageResource(splashResource); overlay.setAlpha(1f); overlay.setVisibility(View.VISIBLE); overlay.bringToFront(); overlay.invalidate(); - // Start the 2.5 s display window only after Android is about to render - // the overlay for the first time. Previously the timer started inside - // onCreate; WebView startup could consume that interval before a frame - // reached the display, making the splash appear to be skipped. + // Start the display interval only after Android is about to render the + // chosen splash. WebView startup therefore cannot consume the interval. overlay.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { private boolean armed; @@ -63,8 +86,8 @@ public boolean onPreDraw() { } if (!cancelled) { handler.postDelayed( - QuantumIntroController.this::showOptionalUserIntroOrFinish, - PRODUCTION_SPLASH_MS + QuantumIntroController.this::fadeOut, + SPLASH_MS ); } return true; @@ -78,22 +101,10 @@ public void cancel() { overlay.animate().cancel(); } - private void showOptionalUserIntroOrFinish() { + private void fadeOut() { if (cancelled) { return; } - - int userIntro = activity.getResources().getIdentifier( - "quantum_user_intro", - "drawable", - activity.getPackageName() - ); - - if (userIntro == 0) { - fadeOut(); - return; - } - overlay.animate().cancel(); overlay.animate() .alpha(0f) @@ -102,31 +113,18 @@ private void showOptionalUserIntroOrFinish() { if (cancelled) { return; } - overlay.setImageResource(userIntro); - overlay.setAlpha(0f); - overlay.animate() - .alpha(1f) - .setDuration(FADE_MS) - .withEndAction(() -> handler.postDelayed(this::fadeOut, USER_INTRO_MS)) - .start(); + overlay.setVisibility(View.GONE); + deliverCompletion(); }) .start(); } - private void fadeOut() { - if (cancelled) { + private void deliverCompletion() { + if (completionDelivered || completion == null) { return; } - overlay.animate().cancel(); - overlay.animate() - .alpha(0f) - .setDuration(FADE_MS) - .withEndAction(() -> { - if (!cancelled) { - overlay.setVisibility(View.GONE); - } - }) - .start(); + completionDelivered = true; + completion.onIntroFinished(customSplashUsed); } private void cancelPending() { From 0337b4def1cc4cfbedd01fc8a329bf904a5dc269 Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:29:12 +0200 Subject: [PATCH 03/10] Add post-splash attribution banner controller --- .../launch/AttributionBannerController.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java diff --git a/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java b/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java new file mode 100644 index 0000000..210d400 --- /dev/null +++ b/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java @@ -0,0 +1,122 @@ +package de.starlightunit.wrapper.launch; + +import android.app.Activity; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +public final class AttributionBannerController { + private static final long FADE_IN_MS = 180L; + private static final long FADE_OUT_MS = 220L; + + private final Activity activity; + private final ImageView banner; + private final boolean enabled; + private final long durationMs; + private final Handler handler = new Handler(Looper.getMainLooper()); + + private boolean introFinished; + private boolean customSplashUsed; + private boolean firstPageReady; + private boolean shown; + private boolean cancelled; + + public AttributionBannerController( + Activity activity, + ImageView banner, + boolean enabled, + int durationMs, + int bottomInsetDp + ) { + this.activity = activity; + this.banner = banner; + this.enabled = enabled; + this.durationMs = Math.max(2000L, Math.min(4000L, durationMs)); + applyBottomInset(bottomInsetDp); + banner.setVisibility(View.GONE); + banner.setAlpha(0f); + } + + public void onIntroFinished(boolean customSplashUsed) { + this.introFinished = true; + this.customSplashUsed = customSplashUsed; + maybeShow(); + } + + public void onFirstPageReady() { + if (firstPageReady) { + return; + } + firstPageReady = true; + maybeShow(); + } + + public void cancel() { + cancelled = true; + handler.removeCallbacksAndMessages(null); + banner.animate().cancel(); + banner.setVisibility(View.GONE); + } + + private void maybeShow() { + if (cancelled + || shown + || !enabled + || !introFinished + || !customSplashUsed + || !firstPageReady) { + return; + } + + int resource = activity.getResources().getIdentifier( + "quantum_studio_attribution_banner", + "drawable", + activity.getPackageName() + ); + if (resource == 0) { + return; + } + + shown = true; + banner.setImageResource(resource); + banner.setVisibility(View.VISIBLE); + banner.setAlpha(0f); + banner.setTranslationY(dp(12)); + banner.bringToFront(); + banner.animate() + .alpha(1f) + .translationY(0f) + .setDuration(FADE_IN_MS) + .withEndAction(() -> handler.postDelayed(this::hide, durationMs)) + .start(); + } + + private void hide() { + if (cancelled) { + return; + } + banner.animate().cancel(); + banner.animate() + .alpha(0f) + .translationY(dp(8)) + .setDuration(FADE_OUT_MS) + .withEndAction(() -> { + if (!cancelled) { + banner.setVisibility(View.GONE); + } + }) + .start(); + } + + private void applyBottomInset(int bottomInsetDp) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) banner.getLayoutParams(); + params.bottomMargin = dp(Math.max(16, bottomInsetDp)); + banner.setLayoutParams(params); + } + + private int dp(int value) { + return Math.round(value * activity.getResources().getDisplayMetrics().density); + } +} From b2e6195468832e3968961cffcfd8817b8f19216e Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:29:26 +0200 Subject: [PATCH 04/10] Add floating attribution banner surface --- app/src/main/res/layout/activity_main.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 90751ca..a3be483 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -161,6 +161,21 @@ android:background="@color/black" android:visibility="gone" /> + + Date: Fri, 18 Sep 2026 12:29:43 +0200 Subject: [PATCH 05/10] Coordinate custom splash and post-load attribution --- .../starlightunit/wrapper/MainActivity.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/starlightunit/wrapper/MainActivity.java b/app/src/main/java/de/starlightunit/wrapper/MainActivity.java index 3af703e..b73acb3 100644 --- a/app/src/main/java/de/starlightunit/wrapper/MainActivity.java +++ b/app/src/main/java/de/starlightunit/wrapper/MainActivity.java @@ -31,6 +31,7 @@ import de.starlightunit.wrapper.config.AppConfig; import de.starlightunit.wrapper.download.AppDownloadListener; import de.starlightunit.wrapper.download.DownloadDestinationPolicy; +import de.starlightunit.wrapper.launch.AttributionBannerController; import de.starlightunit.wrapper.launch.QuantumIntroController; import de.starlightunit.wrapper.media.QuantumNativeMediaPlayer; import de.starlightunit.wrapper.navigation.DeepLinkResolver; @@ -74,6 +75,7 @@ public final class MainActivity extends Activity private Map requestHeaders; private QuantumNativeMediaPlayer nativeMediaPlayer; private QuantumIntroController introController; + private AttributionBannerController attributionBanner; private QuantumSessionCookieStore sessionCookieStore; @Override @@ -115,9 +117,22 @@ protected void onCreate(Bundle savedInstanceState) { introOverlay.setBackgroundColor( SystemBarStyle.parseRgb(AppConfig.SPLASH_BACKGROUND_COLOR, Color.BLACK) ); + ImageView attributionView = findViewById(R.id.attribution_banner); + attributionBanner = new AttributionBannerController( + this, + attributionView, + AppConfig.ATTRIBUTION_BANNER_ENABLED, + AppConfig.ATTRIBUTION_BANNER_DURATION_MS, + attributionBottomInsetDp() + ); Button retryButton = findViewById(R.id.retry_button); - introController = new QuantumIntroController(this, introOverlay); + introController = new QuantumIntroController( + this, + introOverlay, + AppConfig.CUSTOM_SPLASH_ENABLED, + customSplashUsed -> attributionBanner.onIntroFinished(customSplashUsed) + ); introController.start(savedInstanceState != null); WebViewConfigurator.configure(this, webView); @@ -267,6 +282,17 @@ private void requestLegacyPublicDownloadPermissionIfNeeded() { } } + private int attributionBottomInsetDp() { + int inset = 16; + if (AppConfig.BOTTOM_TABS_ENABLED) { + inset += 58; + } + if (AppConfig.CONTEXTUAL_TOOLBAR_ENABLED) { + inset += 46; + } + return inset; + } + private void configureWindow() { Window window = getWindow(); window.setStatusBarColor( @@ -389,6 +415,9 @@ public void onPageReady() { refreshLayout.setRefreshing(false); if (!mainFrameFailed) { errorPanel.setVisibility(View.GONE); + if (attributionBanner != null) { + attributionBanner.onFirstPageReady(); + } } if (sessionCookieStore != null) { sessionCookieStore.capture(); @@ -540,6 +569,10 @@ protected void onDestroy() { introController.cancel(); introController = null; } + if (attributionBanner != null) { + attributionBanner.cancel(); + attributionBanner = null; + } if (pendingFileCallback != null) { pendingFileCallback.onReceiveValue(null); pendingFileCallback = null; From 3b1aa200a731b2cb23ba1e97845afc7d4f05a6c8 Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:30:03 +0200 Subject: [PATCH 06/10] Add splash attribution policy --- .../launch/SplashAttributionPolicy.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/src/main/java/de/starlightunit/wrapper/launch/SplashAttributionPolicy.java diff --git a/app/src/main/java/de/starlightunit/wrapper/launch/SplashAttributionPolicy.java b/app/src/main/java/de/starlightunit/wrapper/launch/SplashAttributionPolicy.java new file mode 100644 index 0000000..6e8a0fd --- /dev/null +++ b/app/src/main/java/de/starlightunit/wrapper/launch/SplashAttributionPolicy.java @@ -0,0 +1,37 @@ +package de.starlightunit.wrapper.launch; + +public final class SplashAttributionPolicy { + private SplashAttributionPolicy() { + } + + public static int clampDurationMs(int requested) { + return Math.max(2000, Math.min(4000, requested)); + } + + public static int footerInsetDp(boolean bottomTabsEnabled, boolean contextualToolbarEnabled) { + int inset = 16; + if (bottomTabsEnabled) { + inset += 58; + } + if (contextualToolbarEnabled) { + inset += 46; + } + return inset; + } + + public static boolean shouldShow( + boolean enabled, + boolean introFinished, + boolean customSplashUsed, + boolean firstPageReady, + boolean shown, + boolean cancelled + ) { + return enabled + && introFinished + && customSplashUsed + && firstPageReady + && !shown + && !cancelled; + } +} From 7005eab1000f0557771433cddcf20aa87a424ffa Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:30:08 +0200 Subject: [PATCH 07/10] Test splash attribution timing policy --- .../launch/SplashAttributionPolicyTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/src/test/java/de/starlightunit/wrapper/launch/SplashAttributionPolicyTest.java diff --git a/app/src/test/java/de/starlightunit/wrapper/launch/SplashAttributionPolicyTest.java b/app/src/test/java/de/starlightunit/wrapper/launch/SplashAttributionPolicyTest.java new file mode 100644 index 0000000..600f342 --- /dev/null +++ b/app/src/test/java/de/starlightunit/wrapper/launch/SplashAttributionPolicyTest.java @@ -0,0 +1,37 @@ +package de.starlightunit.wrapper.launch; + +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 SplashAttributionPolicyTest { + + @Test + public void durationIsClampedToRequestedTwoToFourSecondWindow() { + assertEquals(2000, SplashAttributionPolicy.clampDurationMs(500)); + assertEquals(2000, SplashAttributionPolicy.clampDurationMs(2000)); + assertEquals(3000, SplashAttributionPolicy.clampDurationMs(3000)); + assertEquals(4000, SplashAttributionPolicy.clampDurationMs(4000)); + assertEquals(4000, SplashAttributionPolicy.clampDurationMs(9000)); + } + + @Test + public void footerInsetClearsNativeBottomSurfaces() { + assertEquals(16, SplashAttributionPolicy.footerInsetDp(false, false)); + assertEquals(74, SplashAttributionPolicy.footerInsetDp(true, false)); + assertEquals(62, SplashAttributionPolicy.footerInsetDp(false, true)); + assertEquals(120, SplashAttributionPolicy.footerInsetDp(true, true)); + } + + @Test + public void bannerRequiresCustomSplashFinishedIntroAndFirstPage() { + assertTrue(SplashAttributionPolicy.shouldShow(true, true, true, true, false, false)); + assertFalse(SplashAttributionPolicy.shouldShow(true, true, false, true, false, false)); + assertFalse(SplashAttributionPolicy.shouldShow(true, false, true, true, false, false)); + assertFalse(SplashAttributionPolicy.shouldShow(true, true, true, false, false, false)); + assertFalse(SplashAttributionPolicy.shouldShow(true, true, true, true, true, false)); + assertFalse(SplashAttributionPolicy.shouldShow(true, true, true, true, false, true)); + } +} From cf39fdfd1623be4a37dc0bd471e4725e613f5502 Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:30:20 +0200 Subject: [PATCH 08/10] Use tested splash attribution policy --- .../launch/AttributionBannerController.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java b/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java index 210d400..7f75584 100644 --- a/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java +++ b/app/src/main/java/de/starlightunit/wrapper/launch/AttributionBannerController.java @@ -33,7 +33,7 @@ public AttributionBannerController( this.activity = activity; this.banner = banner; this.enabled = enabled; - this.durationMs = Math.max(2000L, Math.min(4000L, durationMs)); + this.durationMs = SplashAttributionPolicy.clampDurationMs(durationMs); applyBottomInset(bottomInsetDp); banner.setVisibility(View.GONE); banner.setAlpha(0f); @@ -61,12 +61,14 @@ public void cancel() { } private void maybeShow() { - if (cancelled - || shown - || !enabled - || !introFinished - || !customSplashUsed - || !firstPageReady) { + if (!SplashAttributionPolicy.shouldShow( + enabled, + introFinished, + customSplashUsed, + firstPageReady, + shown, + cancelled + )) { return; } From 4bc400b86633dc785a6151a8715c7fd12e1bfc65 Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:30:23 +0200 Subject: [PATCH 09/10] Use tested attribution footer inset policy --- .../java/de/starlightunit/wrapper/MainActivity.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/starlightunit/wrapper/MainActivity.java b/app/src/main/java/de/starlightunit/wrapper/MainActivity.java index b73acb3..41e716e 100644 --- a/app/src/main/java/de/starlightunit/wrapper/MainActivity.java +++ b/app/src/main/java/de/starlightunit/wrapper/MainActivity.java @@ -33,6 +33,7 @@ import de.starlightunit.wrapper.download.DownloadDestinationPolicy; import de.starlightunit.wrapper.launch.AttributionBannerController; import de.starlightunit.wrapper.launch.QuantumIntroController; +import de.starlightunit.wrapper.launch.SplashAttributionPolicy; import de.starlightunit.wrapper.media.QuantumNativeMediaPlayer; import de.starlightunit.wrapper.navigation.DeepLinkResolver; import de.starlightunit.wrapper.navigation.ExternalLinkLauncher; @@ -283,14 +284,10 @@ private void requestLegacyPublicDownloadPermissionIfNeeded() { } private int attributionBottomInsetDp() { - int inset = 16; - if (AppConfig.BOTTOM_TABS_ENABLED) { - inset += 58; - } - if (AppConfig.CONTEXTUAL_TOOLBAR_ENABLED) { - inset += 46; - } - return inset; + return SplashAttributionPolicy.footerInsetDp( + AppConfig.BOTTOM_TABS_ENABLED, + AppConfig.CONTEXTUAL_TOOLBAR_ENABLED + ); } private void configureWindow() { From a5b8570ba840e150e0bce8462d438099fb5b0b2f Mon Sep 17 00:00:00 2001 From: Starlight Unit Studios Date: Fri, 18 Sep 2026 12:37:33 +0200 Subject: [PATCH 10/10] Document custom splash attribution lifecycle --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bababa..d5948f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +Custom splash replacement and post-load STU attribution. + +- Generated apps may replace the visible STU production splash with one profile-provided custom splash; both splash images are never shown in sequence. +- The production splash stays packaged as a safe fallback when no valid custom resource exists. +- Custom-splash builds show the canonical STU attribution banner once after both intro completion and the first successful main-page load. +- Attribution display duration is bounded to 2 through 4 seconds and defaults to 3 seconds. +- The floating banner automatically clears enabled Bottom Tabs and Contextual Toolbar surfaces. +- Factory-splash builds and failed first-page loads never show the footer attribution banner. +- Splash and attribution lifecycles are controlled by separate components with a small tested coordination policy. +- Keeps Android 6 / API 23 compatibility and the existing Android 16 startup safeguards. + Native link handling runtime. - Adds first-match-wins link rules for scheme, host and path-prefix matching.