Android: public host as the build property kvitt.publicHost (D-121)
The release server's host name comes from -Pkvitt.publicHost, else kvitt.publicHost in android/local.properties, else the placeholder kvitt.example.invalid. :app exposes it as BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost (the https invite-link intent filter, port 8443) and RELEASE_SERVER_URL, which :feature:onboarding receives through the @ReleaseServerUrl Hilt qualifier for S-1's prefill. Tests and goldens use fixed hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -34,14 +34,15 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="kvitt" android:host="invite" />
|
||||
</intent-filter>
|
||||
<!-- Unverified link: the user may get a chooser (AC-2.6). -->
|
||||
<!-- Unverified link: the user may get a chooser (AC-2.6). The host is the Gradle property kvitt.publicHost
|
||||
(BuildConfig.PUBLIC_HOST, D-121). -->
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="icehack.asuscomm.com"
|
||||
android:host="${publicHost}"
|
||||
android:port="8443"
|
||||
android:pathPrefix="/i/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -9,6 +9,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import se.hagstedt.kvitt.adapter.fcm.PushListener
|
||||
import se.hagstedt.kvitt.feature.onboarding.ReleaseServerUrl
|
||||
import se.hagstedt.kvitt.link.RELEASE_SERVER_URL
|
||||
import se.hagstedt.kvitt.push.PushHandler
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
@@ -23,6 +25,10 @@ annotation class AppScope
|
||||
object AppModule {
|
||||
@Provides @Singleton @AppScope
|
||||
fun scope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
/** S-1's prefilled release server, from `BuildConfig.PUBLIC_HOST` (AC-2.7, D-121). */
|
||||
@Provides @ReleaseServerUrl
|
||||
fun releaseServerUrl(): String = RELEASE_SERVER_URL
|
||||
}
|
||||
|
||||
@Module
|
||||
|
||||
@@ -26,8 +26,9 @@ sealed interface AppLink {
|
||||
|
||||
/**
|
||||
* Parses the intents `MainActivity` receives: `kvitt://invite?server=…&code=…[&from=…]` and
|
||||
* `https://icehack.asuscomm.com:8443/i/<CODE>[?from=…]` (AC-2.6, both through `:feature:onboarding`'s [InviteLink], the reader
|
||||
* S-1 uses for pasted links), and the notification taps this app builds ([openIntent]).
|
||||
* `https://<BuildConfig.PUBLIC_HOST>:8443/i/<CODE>[?from=…]` (AC-2.6; the manifest's intent filter takes its host from the
|
||||
* `publicHost` placeholder, D-121), both through `:feature:onboarding`'s [InviteLink], the reader S-1 uses for pasted
|
||||
* links, which takes the server from the link itself, and the notification taps this app builds ([openIntent]).
|
||||
*/
|
||||
object AppLinks {
|
||||
const val ACTION_OPEN = "se.hagstedt.kvitt.action.OPEN"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package se.hagstedt.kvitt.link
|
||||
|
||||
import se.hagstedt.kvitt.BuildConfig
|
||||
import se.hagstedt.kvitt.feature.onboarding.releaseServerUrl
|
||||
|
||||
/**
|
||||
* The release server, `https://<BuildConfig.PUBLIC_HOST>:8443` (AC-2.7, D-121): prefilled on S-1 and the host of the
|
||||
* `https` invite links the manifest declares (AC-2.6, the `publicHost` placeholder). `PUBLIC_HOST` is the Gradle
|
||||
* property `kvitt.publicHost`; without it the build uses the placeholder `kvitt.example.invalid`, which never resolves.
|
||||
*/
|
||||
val RELEASE_SERVER_URL: String = releaseServerUrl(BuildConfig.PUBLIC_HOST)
|
||||
@@ -11,6 +11,7 @@ import se.hagstedt.kvitt.fakes.FakeRepositories
|
||||
import se.hagstedt.kvitt.fakes.FakeScenario
|
||||
import se.hagstedt.kvitt.fakes.FakeScenarios
|
||||
import se.hagstedt.kvitt.link.AppLink
|
||||
import se.hagstedt.kvitt.link.RELEASE_SERVER_URL
|
||||
import se.hagstedt.kvitt.model.SessionState
|
||||
import se.hagstedt.kvitt.design.testing.MainDispatcherRule
|
||||
|
||||
@@ -22,7 +23,7 @@ class AppViewModelTest {
|
||||
|
||||
private fun vm(fakes: FakeRepositories) = AppViewModel(fakes.session, fakes.sync, fakes.accounts)
|
||||
|
||||
private val link = AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCD2345EFGH", "Jacob")
|
||||
private val link = AppLink.Invite(RELEASE_SERVER_URL, "ABCD2345EFGH", "Jacob")
|
||||
|
||||
@Test
|
||||
fun anInviteLinkWhilePaired_isDropped_andNeverPrefillsALaterPairing() {
|
||||
|
||||
@@ -4,17 +4,43 @@ import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import se.hagstedt.kvitt.BuildConfig
|
||||
import se.hagstedt.kvitt.feature.onboarding.releaseServerUrl
|
||||
|
||||
/** AC-2.6 invite links and AC-13.6 notification taps as `MainActivity` receives them. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [36], application = Application::class)
|
||||
class AppLinksTest {
|
||||
/** The host this build was configured with (`kvitt.publicHost`, D-121). */
|
||||
private val host = BuildConfig.PUBLIC_HOST
|
||||
|
||||
@Test
|
||||
fun theReleaseServerIsHttpsOnPort8443OfThePublicHost() {
|
||||
assertEquals("https://$host:8443", RELEASE_SERVER_URL)
|
||||
// Without kvitt.publicHost the build uses the placeholder, which never resolves (RFC 2606).
|
||||
assertEquals("https://kvitt.example.invalid:8443", releaseServerUrl("kvitt.example.invalid"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theManifestOpensLandingLinksOfThePublicHostOnly() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
fun opens(url: String) = context.packageManager.queryIntentActivities(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(url)).addCategory(Intent.CATEGORY_BROWSABLE).setPackage(context.packageName),
|
||||
0,
|
||||
).any { it.activityInfo.name == "se.hagstedt.kvitt.MainActivity" }
|
||||
assertTrue(opens("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN"))
|
||||
assertFalse(opens("https://other.example.org:8443/i/ABCDEFGHJKMN"))
|
||||
assertFalse(opens("https://$host/i/ABCDEFGHJKMN"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appLinkFillsServerAndCode() {
|
||||
val link = AppLinks.parseUri("kvitt://invite?server=http%3A%2F%2F192.168.1.210%3A8080&code=ABCD-EFGH-JKMN")
|
||||
@@ -24,33 +50,33 @@ class AppLinksTest {
|
||||
@Test
|
||||
fun appLinkNormalisesTheCode() {
|
||||
// §8.3: lowercase, O -> 0, I and L -> 1.
|
||||
assertEquals("ABCD0113EFGH", AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2Ficehack.asuscomm.com%3A8443&code=abcdo1l3efgh")?.code)
|
||||
assertEquals("ABCD0113EFGH", AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F$host%3A8443&code=abcdo1l3efgh")?.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun landingLinkFillsTheServerFromTheLink() {
|
||||
val link = AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/ABCDEFGHJKMN")
|
||||
assertEquals(AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN"), link)
|
||||
val link = AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")
|
||||
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), link)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun linksWithTheAdminNameCarryIt_andOldLinksStillParse() {
|
||||
assertEquals(
|
||||
AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN", "Åsa Berg"),
|
||||
AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/ABCDEFGHJKMN?from=%C3%85sa%20Berg"),
|
||||
AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN", "Åsa Berg"),
|
||||
AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN?from=%C3%85sa%20Berg"),
|
||||
)
|
||||
assertEquals(
|
||||
AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN", "Jacob"),
|
||||
AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2Ficehack%2Easuscomm%2Ecom%3A8443&code=ABCDEFGHJKMN&from=Jacob"),
|
||||
AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN", "Jacob"),
|
||||
AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F${host.replace(".", "%2E")}%3A8443&code=ABCDEFGHJKMN&from=Jacob"),
|
||||
)
|
||||
assertNull(AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/ABCDEFGHJKMN")?.adminName)
|
||||
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")?.adminName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherLinksAreIgnored() {
|
||||
assertNull(AppLinks.parseUri(null))
|
||||
assertNull(AppLinks.parseUri("https://icehack.asuscomm.com:8443/apk/latest"))
|
||||
assertNull(AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/SHORT"))
|
||||
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/apk/latest"))
|
||||
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/i/SHORT"))
|
||||
assertNull(AppLinks.parseUri("kvitt://invite?server=x"))
|
||||
assertNull(AppLinks.parseUri("kvitt://other?code=ABCDEFGHJKMN"))
|
||||
// A bare code is a paste on S-1, not a link.
|
||||
@@ -59,8 +85,8 @@ class AppLinksTest {
|
||||
|
||||
@Test
|
||||
fun viewIntentIsParsed() {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://icehack.asuscomm.com:8443/i/ABCD-EFGH-JKMN"))
|
||||
assertEquals(AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN"), AppLinks.parse(intent))
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$RELEASE_SERVER_URL/i/ABCD-EFGH-JKMN"))
|
||||
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), AppLinks.parse(intent))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user