From 0853bf09a99b89ea0fab1da62eea9e742fd3660f Mon Sep 17 00:00:00 2001
From: net <96362337+netqo@users.noreply.github.com>
Date: Wed, 27 May 2026 19:13:04 -0300
Subject: [PATCH 1/2] chore(brand): replace launcher icon + splash glyph with
the Stack mark
The Android Studio template robot was still shipping as the app icon
and as the splash mark. Ports the layered-squares glyph the login
hero uses (LoginScreen.StackLogoGlyph) into the launcher + splash
slots so the app's branding is consistent from the launcher tap
through to the first Compose frame.
* ic_launcher_foreground: brand glyph sized for the adaptive-icon
safe zone (central 66dp of a 108dp viewport) so the corners
survive round / squircle launcher masks.
* ic_launcher_background: solid SurfaceBase (#0B0B12) plate that
matches the window background, so the splash-to-content
transition doesn't flash a different color.
* ic_launcher_monochrome: single-tint variant for Android 13+
themed icons (the previous adaptive-icon XML pointed monochrome
at the colored foreground, which the platform can't recolor
cleanly).
* ic_brand_splash: standalone splash glyph that fills the full
108dp viewport. The launcher foreground has to keep its content
inside the safe zone and would render visibly small in the
SplashScreen API icon area (no mask is applied there); this
drawable expands the same three squares so the splash reads as
the Stack mark instead of a tiny inset.
* themes.xml: splash theme now points at ic_brand_splash.
Legacy mipmap-{m,h,xh,xxh,xxxh}dpi PNGs are untouched. They only
ship to API 24-25 devices that can't load the adaptive-icon XML
(min SDK is 24, so the fallback is required); generating new PNGs
from the vector needs a build-time tool that isn't wired in yet.
---
app/src/main/res/drawable/ic_brand_splash.xml | 28 +++
.../res/drawable/ic_launcher_background.xml | 168 +-----------------
.../res/drawable/ic_launcher_foreground.xml | 45 +++--
.../res/drawable/ic_launcher_monochrome.xml | 26 +++
.../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +-
.../mipmap-anydpi-v26/ic_launcher_round.xml | 2 +-
app/src/main/res/values/themes.xml | 2 +-
7 files changed, 85 insertions(+), 188 deletions(-)
create mode 100644 app/src/main/res/drawable/ic_brand_splash.xml
create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml
diff --git a/app/src/main/res/drawable/ic_brand_splash.xml b/app/src/main/res/drawable/ic_brand_splash.xml
new file mode 100644
index 0000000..252bb87
--- /dev/null
+++ b/app/src/main/res/drawable/ic_brand_splash.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 07d5da9..44277e9 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -1,170 +1,16 @@
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 2b068d1..ac3bfdc 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,30 +1,27 @@
+
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ android:fillColor="#00000000"
+ android:strokeColor="#8B5CF6"
+ android:strokeWidth="3"
+ android:pathData="M32,32 h44 v44 h-44 z" />
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml
new file mode 100644
index 0000000..efc18b0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 6f3b755..b070c76 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,5 +2,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755..b070c76 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,5 +2,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 39518bb..a134db5 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -17,7 +17,7 @@
postSplashScreenTheme once the activity is ready. -->
From 5d925f32fcfc131750055b03a3782f578ef88cb2 Mon Sep 17 00:00:00 2001
From: net <96362337+netqo@users.noreply.github.com>
Date: Wed, 27 May 2026 19:27:51 -0300
Subject: [PATCH 2/2] fix(test): make StackNavHostTest survive routes that use
hiltViewModel
The smoke suite started failing the moment real screens replaced the
nav placeholders, because the default createComposeRule() launches a
plain ComponentActivity that Hilt cannot wire ("Given component
holder class androidx.activity.ComponentActivity does not implement
interface dagger.hilt.internal.GeneratedComponent...").
Adds a bare `@AndroidEntryPoint HiltTestActivity` in `src/debug/` so
the debug APK ships it (the instrumentation runner resolves
activities through the target APK's PackageManager, so the class
cannot live in `src/androidTest/` alone). The test now wires
HiltAndroidRule + createAndroidComposeRule() via a
RuleChain so Hilt injects before the activity launches; navigation
uses defaultPath to support Wallet's optional `?tab={tab}` query
arg without re-asserting the bare path.
All 26 instrumented tests pass on device after this change.
---
.../navigation/StackNavHostTest.kt | 38 +++++++++++++------
app/src/debug/AndroidManifest.xml | 17 +++++++++
.../stackcasino/HiltTestActivity.kt | 23 +++++++++++
3 files changed, 67 insertions(+), 11 deletions(-)
create mode 100644 app/src/debug/AndroidManifest.xml
create mode 100644 app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt
diff --git a/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt b/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt
index 9905da2..a44a0cd 100644
--- a/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt
+++ b/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt
@@ -1,25 +1,41 @@
package com.plainstudio.stackcasino.navigation
-import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
-import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.plainstudio.stackcasino.HiltTestActivity
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
/**
- * Smoke-validates that every [Route] declared in the sealed hierarchy is
- * actually registered in the nav graph and reachable from the start
- * destination. If a route is added to [Route] without a matching
+ * Smoke-validates that every [Route] declared in the sealed hierarchy
+ * is actually registered in the nav graph and reachable from the
+ * start destination. If a route is added to [Route] without a matching
* `composable(...)` block in [StackNavHost], the call to [navigate]
* here throws and the test fails.
+ *
+ * Runs against [HiltTestActivity] (not the default `ComponentActivity`
+ * the bare `createComposeRule()` would spin up) because several
+ * destinations call `hiltViewModel()` which only resolves on an
+ * `@AndroidEntryPoint` host.
*/
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class StackNavHostTest {
+ private val hiltRule = HiltAndroidRule(this)
+ private val composeRule = createAndroidComposeRule()
+
+ // Hilt has to inject the test before the Compose rule mounts the
+ // activity, otherwise hiltViewModel() inside the first composed
+ // screen has no graph to pull from.
@get:Rule
- val composeRule = createComposeRule()
+ val ruleChain: RuleChain = RuleChain.outerRule(hiltRule).around(composeRule)
private lateinit var navController: TestNavHostController
@@ -27,8 +43,8 @@ class StackNavHostTest {
fun every_static_route_is_reachable() {
composeRule.setContent {
navController =
- TestNavHostController(ApplicationProvider.getApplicationContext()).apply {
- navigatorProvider.addNavigator(androidx.navigation.compose.ComposeNavigator())
+ TestNavHostController(composeRule.activity).apply {
+ navigatorProvider.addNavigator(ComposeNavigator())
}
StackNavHost(navController = navController, startDestination = Route.Login.path)
}
@@ -52,7 +68,7 @@ class StackNavHostTest {
)
staticTargets.forEach { route ->
- composeRule.runOnUiThread { navController.navigate(route.path) }
+ composeRule.runOnUiThread { navController.navigate(route.defaultPath) }
composeRule.waitForIdle()
assertEquals(
"Navigation to ${route.path} did not land on the expected destination.",
@@ -66,8 +82,8 @@ class StackNavHostTest {
fun parametric_routes_resolve_with_arguments() {
composeRule.setContent {
navController =
- TestNavHostController(ApplicationProvider.getApplicationContext()).apply {
- navigatorProvider.addNavigator(androidx.navigation.compose.ComposeNavigator())
+ TestNavHostController(composeRule.activity).apply {
+ navigatorProvider.addNavigator(ComposeNavigator())
}
StackNavHost(navController = navController, startDestination = Route.Login.path)
}
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..60e50b3
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt b/app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt
new file mode 100644
index 0000000..13eb651
--- /dev/null
+++ b/app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt
@@ -0,0 +1,23 @@
+package com.plainstudio.stackcasino
+
+import androidx.activity.ComponentActivity
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * Bare-bones `@AndroidEntryPoint` shell used as the host activity for
+ * Compose tests that mount screens calling `hiltViewModel()`. The
+ * default `createComposeRule()` spins up a plain `ComponentActivity`
+ * which Hilt cannot wire (it throws "Given component holder class
+ * androidx.activity.ComponentActivity does not implement interface
+ * dagger.hilt.internal.GeneratedComponent..."), so any Compose test
+ * that needs the Hilt graph has to launch through this activity via
+ * `createAndroidComposeRule()`.
+ *
+ * Lives in `src/debug/` (not `src/androidTest/`) because Android's
+ * instrumentation runner resolves activities through the target APK's
+ * PackageManager: a class declared only in the test APK would fail
+ * with "Unable to resolve activity". Production release builds drop
+ * the activity entirely since the `debug/` sourceset is variant-scoped.
+ */
+@AndroidEntryPoint
+class HiltTestActivity : ComponentActivity()