From 89d687a0ade5261544d69e7f41db0a9a670d4e6f Mon Sep 17 00:00:00 2001 From: papi Date: Wed, 20 May 2026 16:23:32 -0400 Subject: [PATCH] Refresh first-run welcome flow --- app/src/main/java/com/papi/nova/PcView.kt | 10 + .../com/papi/nova/ui/NovaWelcomeActivity.kt | 47 ++- .../res/layout-land/activity_nova_welcome.xml | 295 +++++++---------- .../main/res/layout/activity_nova_welcome.xml | 309 +++++++----------- .../papi/nova/ui/NovaWelcomeRefreshTest.kt | 61 ++++ 5 files changed, 355 insertions(+), 367 deletions(-) create mode 100644 app/src/test/java/com/papi/nova/ui/NovaWelcomeRefreshTest.kt diff --git a/app/src/main/java/com/papi/nova/PcView.kt b/app/src/main/java/com/papi/nova/PcView.kt index 380c2a05..e4cb0207 100644 --- a/app/src/main/java/com/papi/nova/PcView.kt +++ b/app/src/main/java/com/papi/nova/PcView.kt @@ -1098,6 +1098,16 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { } initializeViews(prefs) + handleWelcomeAction(intent.getStringExtra(NovaWelcomeActivity.EXTRA_WELCOME_ACTION)) + } + + private fun handleWelcomeAction(action: String?) { + if (action != NovaWelcomeActivity.ACTION_SCAN_QR) { + return + } + + intent.removeExtra(NovaWelcomeActivity.EXTRA_WELCOME_ACTION) + window.decorView.post { launchQrScanner() } } private fun startComputerUpdates() { diff --git a/app/src/main/java/com/papi/nova/ui/NovaWelcomeActivity.kt b/app/src/main/java/com/papi/nova/ui/NovaWelcomeActivity.kt index 31a8f3dd..bf85de94 100644 --- a/app/src/main/java/com/papi/nova/ui/NovaWelcomeActivity.kt +++ b/app/src/main/java/com/papi/nova/ui/NovaWelcomeActivity.kt @@ -1,9 +1,13 @@ package com.papi.nova.ui +import android.content.Context +import android.content.Intent import android.os.Bundle +import android.view.View import androidx.appcompat.app.AppCompatActivity -import com.papi.nova.R import com.papi.nova.PcView +import com.papi.nova.R +import com.papi.nova.preferences.AddComputerManually /** * First-launch welcome screen. Shows once, then never again. @@ -15,22 +19,39 @@ class NovaWelcomeActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_nova_welcome) - findViewById(R.id.welcome_start_btn).setOnClickListener { - // Mark as seen - getSharedPreferences("nova_prefs", MODE_PRIVATE).edit() - .putBoolean("welcome_seen", true) - .commit() - val next = android.content.Intent(this, PcView::class.java) - intent.extras?.let { next.putExtras(it) } - startActivity(next) - finish() + findViewById(R.id.welcome_discover_btn).setOnClickListener { + finishWelcome(Intent(this, PcView::class.java)) + } + findViewById(R.id.welcome_add_manual_btn).setOnClickListener { + finishWelcome(Intent(this, AddComputerManually::class.java)) + } + findViewById(R.id.welcome_scan_qr_btn).setOnClickListener { + finishWelcome( + Intent(this, PcView::class.java) + .putExtra(EXTRA_WELCOME_ACTION, ACTION_SCAN_QR), + ) } } + private fun finishWelcome(next: Intent) { + getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() + .putBoolean(KEY_WELCOME_SEEN, true) + .commit() + intent.extras?.let { next.putExtras(it) } + next.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(next) + finish() + } + companion object { - fun shouldShow(context: android.content.Context): Boolean { - return !context.getSharedPreferences("nova_prefs", android.content.Context.MODE_PRIVATE) - .getBoolean("welcome_seen", false) + const val EXTRA_WELCOME_ACTION = "com.papi.nova.extra.WELCOME_ACTION" + const val ACTION_SCAN_QR = "scan_qr" + private const val PREFS_NAME = "nova_prefs" + private const val KEY_WELCOME_SEEN = "welcome_seen" + + fun shouldShow(context: Context): Boolean { + return !context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean(KEY_WELCOME_SEEN, false) } } } diff --git a/app/src/main/res/layout-land/activity_nova_welcome.xml b/app/src/main/res/layout-land/activity_nova_welcome.xml index 13d358bf..3e61b33d 100644 --- a/app/src/main/res/layout-land/activity_nova_welcome.xml +++ b/app/src/main/res/layout-land/activity_nova_welcome.xml @@ -5,238 +5,197 @@ android:layout_height="match_parent" android:background="?android:colorBackground"> - - + android:orientation="horizontal" + android:paddingHorizontal="36dp" + android:paddingVertical="24dp"> - + android:paddingEnd="28dp"> + android:src="@drawable/ic_nova_star_foreground" /> + android:text="Welcome to Nova" + android:textColor="@color/nova_ice" + android:textSize="28sp" /> + android:gravity="center" + android:lineSpacingExtra="2dp" + android:text="A polished streaming home for Polaris, handhelds, TV, and Moonlight-compatible hosts." + android:textColor="@color/nova_text_secondary" + android:textSize="14sp" /> - + android:paddingStart="28dp"> - + android:layout_marginBottom="10dp" + android:background="@drawable/nova_card_bg_focusable" + android:orientation="vertical" + android:padding="14dp"> - - - - - - - + android:letterSpacing="0.08" + android:text="01 Find your host" + android:textAllCaps="true" + android:textColor="@color/nova_accent" + android:textSize="10sp" /> + + - + android:layout_marginBottom="10dp" + android:background="@drawable/nova_card_bg_focusable" + android:orientation="vertical" + android:padding="14dp"> - - - - - - - + android:letterSpacing="0.08" + android:text="02 Pair with confidence" + android:textAllCaps="true" + android:textColor="@color/nova_accent" + android:textSize="10sp" /> + + - + android:layout_marginBottom="16dp" + android:background="@drawable/nova_card_bg_focusable" + android:orientation="vertical" + android:padding="14dp"> - - - - - - - + android:letterSpacing="0.08" + android:text="03 Built for handhelds and TV" + android:textAllCaps="true" + android:textColor="@color/nova_accent" + android:textSize="10sp" /> + + - + android:baselineAligned="false" + android:orientation="horizontal"> - - - + + + + - - - - - + android:focusable="true" + android:nextFocusLeft="@id/welcome_add_manual_btn" + android:text="Scan QR" + android:textAllCaps="false" + android:textColor="@color/nova_text_primary" + app:cornerRadius="25dp" + app:strokeColor="@color/nova_focus_stroke_selector" + app:strokeWidth="2dp" /> - - - - diff --git a/app/src/main/res/layout/activity_nova_welcome.xml b/app/src/main/res/layout/activity_nova_welcome.xml index 4e04701e..8b0f0586 100644 --- a/app/src/main/res/layout/activity_nova_welcome.xml +++ b/app/src/main/res/layout/activity_nova_welcome.xml @@ -5,7 +5,6 @@ android:layout_height="match_parent" android:background="?android:colorBackground"> - @@ -19,227 +18,165 @@ + android:orientation="vertical" + android:paddingHorizontal="28dp" + android:paddingTop="40dp" + android:paddingBottom="28dp"> - + android:src="@drawable/ic_nova_star_foreground" /> + android:text="Welcome to Nova" + android:textColor="@color/nova_ice" + android:textSize="28sp" /> + android:gravity="center" + android:lineSpacingExtra="2dp" + android:text="A polished streaming home for Polaris, handhelds, TV, and Moonlight-compatible hosts." + android:textColor="@color/nova_text_secondary" + android:textSize="14sp" /> - + android:padding="18dp"> - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - + + - - - - - - - - - - - + android:layout_marginTop="8dp" + android:text="Use standard Moonlight pairing, or scan a Polaris pairing QR when your host shows one." + android:textColor="@color/nova_text_primary" + android:textSize="16sp" /> - - + + + + + + - + app:cornerRadius="26dp" + app:strokeColor="@color/nova_focus_stroke_selector" + app:strokeWidth="2dp" /> + + + diff --git a/app/src/test/java/com/papi/nova/ui/NovaWelcomeRefreshTest.kt b/app/src/test/java/com/papi/nova/ui/NovaWelcomeRefreshTest.kt new file mode 100644 index 00000000..eee297ae --- /dev/null +++ b/app/src/test/java/com/papi/nova/ui/NovaWelcomeRefreshTest.kt @@ -0,0 +1,61 @@ +package com.papi.nova.ui + +import java.io.File +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class NovaWelcomeRefreshTest { + @Test + fun welcomeLayoutsExposeThreeControllerActions() { + val layouts = arrayOf( + "src/main/res/layout/activity_nova_welcome.xml", + "src/main/res/layout-land/activity_nova_welcome.xml", + ) + + for (layout in layouts) { + val xml = readFile(layout) + assertTrue("$layout should expose Discover hosts", xml.contains("@+id/welcome_discover_btn")) + assertTrue("$layout should expose Add manually", xml.contains("@+id/welcome_add_manual_btn")) + assertTrue("$layout should expose Scan QR", xml.contains("@+id/welcome_scan_qr_btn")) + assertTrue("$layout primary action should be D-pad focusable", buttonBlock(xml, "welcome_discover_btn").contains("android:focusable=\"true\"")) + assertTrue("$layout manual action should be D-pad focusable", buttonBlock(xml, "welcome_add_manual_btn").contains("android:focusable=\"true\"")) + assertTrue("$layout QR action should be D-pad focusable", buttonBlock(xml, "welcome_scan_qr_btn").contains("android:focusable=\"true\"")) + } + } + + @Test + fun welcomeCopyStaysScopedToVerifiedFlows() { + val portrait = readFile("src/main/res/layout/activity_nova_welcome.xml") + val landscape = readFile("src/main/res/layout-land/activity_nova_welcome.xml") + val copy = portrait + landscape + + assertTrue("welcome should mention Polaris", copy.contains("Polaris")) + assertTrue("welcome should mention Moonlight compatibility", copy.contains("Moonlight-compatible") || copy.contains("Moonlight pairing")) + assertTrue("welcome should frame QR as Polaris pairing only", copy.contains("Polaris pairing QR")) + assertFalse("welcome should not overclaim automatic QR or TOFU pairing", copy.contains("TOFU auto-pair")) + assertFalse("welcome should not overclaim AI tuning", copy.contains("AI-optimized")) + } + + @Test + fun welcomeActivityKeepsSeenFlagAndRoutesActions() { + val welcomeSource = readFile("src/main/java/com/papi/nova/ui/NovaWelcomeActivity.kt") + val pcViewSource = readFile("src/main/java/com/papi/nova/PcView.kt") + + assertTrue("welcome_seen should still be persisted", welcomeSource.contains("KEY_WELCOME_SEEN") && welcomeSource.contains("putBoolean(KEY_WELCOME_SEEN, true)")) + assertTrue("welcome completion should still commit synchronously before leaving", welcomeSource.contains(".commit()")) + assertTrue("manual add action should use the existing manual add screen", welcomeSource.contains("AddComputerManually::class.java")) + assertTrue("QR action should be explicit", welcomeSource.contains("EXTRA_WELCOME_ACTION") && welcomeSource.contains("ACTION_SCAN_QR")) + assertTrue("PcView should handle the welcome QR action through the wired scanner", pcViewSource.contains("handleWelcomeAction") && pcViewSource.contains("launchQrScanner()")) + } + + private fun buttonBlock(xml: String, id: String): String { + val idIndex = xml.indexOf("@+id/$id") + assertTrue("missing $id", idIndex >= 0) + val start = xml.lastIndexOf('<', idIndex).coerceAtLeast(0) + val end = xml.indexOf("/>", idIndex).let { if (it >= 0) it + 2 else xml.length } + return xml.substring(start, end) + } + + private fun readFile(path: String): String = File(path).readText() +}