Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions app/src/main/java/com/github/se/pooltrack/AppGate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.github.se.pooltrack

import com.github.se.pooltrack.ui.poolstay.PoolStayState

/** The screen that takes over the app, or [App] when none does. */
enum class AppGate {
/** Waiting for the stored entries, so nothing is shown yet (a blank frame, not a flash). */
Loading,

/** The user has to log how far they swam, see `PoolStayScreen`. */
PoolStay,

/** A newer release has to be installed, see `UpdateRequiredScreen`. */
Update,

/** Nobody is signed in with a real account. */
SignIn,

/** The normal app. */
App,
}

/**
* Which screen takes over the app, in order of priority.
*
* A swim waiting for its distance comes before the mandatory update: the update replaces the whole
* app and can take a while on the weak signal of a pool, and the user must not be kept from logging
* a swim they just finished. The distance is stored with the entry, so the update is simply offered
* right after it is logged.
*
* @param updateAvailable Whether a newer release has to be installed.
* @param signedIn Whether a real (non-anonymous) account is signed in; entries, and so swims, only
* exist for it.
* @param poolStay Where the user's current stay in the pool stands.
*/
fun appGate(updateAvailable: Boolean, signedIn: Boolean, poolStay: PoolStayState): AppGate =
when {
signedIn && poolStay.needsAttention() -> AppGate.PoolStay
// Not known yet whether a swim is waiting: show nothing rather than the update for a moment.
signedIn && poolStay == PoolStayState.Loading -> AppGate.Loading
updateAvailable -> AppGate.Update
!signedIn -> AppGate.SignIn
else -> AppGate.App
}

private fun PoolStayState.needsAttention() =
this is PoolStayState.CancelWindow || this is PoolStayState.LogRequired
41 changes: 22 additions & 19 deletions app/src/main/java/com/github/se/pooltrack/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import com.github.se.pooltrack.ui.navigation.ENTRY_TIMESTAMP_ARG
import com.github.se.pooltrack.ui.navigation.NavigationActions
import com.github.se.pooltrack.ui.navigation.Screen
import com.github.se.pooltrack.ui.poolstay.PoolStayScreen
import com.github.se.pooltrack.ui.poolstay.PoolStayState
import com.github.se.pooltrack.ui.poolstay.PoolStayViewModel
import com.github.se.pooltrack.ui.subscription.SubscriptionQuickViewScreen
import com.github.se.pooltrack.ui.subscription.SubscriptionScreen
Expand Down Expand Up @@ -108,10 +107,11 @@ class MainActivity : ComponentActivity() {
* `PoolTrackApp` is the main composable function that sets up the whole app UI. Gated behind Google
* sign-in - [SignInScreen] is shown instead until a real (non-anonymous) account is signed in -
* since subscriptions and entries are only meaningful once backed up to one. A newer release takes
* precedence over both: [UpdateRequiredScreen] replaces the whole app until it is installed. If the
* precedence over that: [UpdateRequiredScreen] replaces the whole app until it is installed. If the
* check can't reach GitHub (e.g. offline at the pool), the app stays usable so the pass can still
* be shown. Once signed in, a stay in the pool replaces the app too ([PoolStayScreen]) until the
* user logged how far they swam.
* user logged how far they swam, and that comes even before the update, so a swim that just ended
* can be logged first. See [appGate] for the exact order.
*/
@Composable
fun PoolTrackApp(
Expand All @@ -120,31 +120,34 @@ fun PoolTrackApp(
) {
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { updateViewModel.onResume() }
val updateState by updateViewModel.state.collectAsState()
if (updateState !is UpdateUiState.None) {
UpdateRequiredScreen(updateViewModel)
return
}

val currentUser by accountViewModel.currentUser.collectAsState()
val user = currentUser

if (user == null || user.isAnonymous) {
SignInScreen(viewModel = accountViewModel)
return
}

// After a confirmed scan the user sees only the pool stay screen, until they logged a distance.
// It is derived from the stored entry, so reopening the app later lands here again.
val poolStayViewModel: PoolStayViewModel = viewModel()
val poolStay by poolStayViewModel.state.collectAsState()
when (poolStay) {
PoolStayState.Loading -> return
PoolStayState.None -> Unit
is PoolStayState.CancelWindow,
is PoolStayState.LogRequired -> {

when (
appGate(
updateAvailable = updateState !is UpdateUiState.None,
signedIn = user != null && !user.isAnonymous,
poolStay = poolStay,
)
) {
AppGate.Loading -> return
AppGate.PoolStay -> {
PoolStayScreen(poolStayViewModel)
return
}
AppGate.Update -> {
UpdateRequiredScreen(updateViewModel)
return
}
AppGate.SignIn -> {
SignInScreen(viewModel = accountViewModel)
return
}
AppGate.App -> Unit
}

val navController: NavHostController = rememberNavController()
Expand Down
80 changes: 80 additions & 0 deletions app/src/test/java/com/github/se/pooltrack/AppGateTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.github.se.pooltrack

import com.github.se.pooltrack.model.entry.Entry
import com.github.se.pooltrack.ui.poolstay.PoolStayState
import java.time.Duration
import org.junit.Assert.assertEquals
import org.junit.Test

class AppGateTest {

private val entry = Entry(timestampEpochMilli = 0L, awaitingDistance = true)
private val cancelWindow =
PoolStayState.CancelWindow(entry, Duration.ofMinutes(2), Duration.ofMinutes(8))
private val logRequired = PoolStayState.LogRequired(entry)

private fun gate(update: Boolean, signedIn: Boolean, poolStay: PoolStayState) =
appGate(updateAvailable = update, signedIn = signedIn, poolStay = poolStay)

@Test
fun aSwimWaitingForItsDistance_comesBeforeTheUpdate() {
assertEquals(AppGate.PoolStay, gate(update = true, signedIn = true, poolStay = logRequired))
assertEquals(AppGate.PoolStay, gate(update = true, signedIn = true, poolStay = cancelWindow))
}

@Test
fun aSwimWaitingForItsDistance_isShown_withoutAnUpdate() {
assertEquals(AppGate.PoolStay, gate(update = false, signedIn = true, poolStay = logRequired))
}

@Test
fun theUpdate_isShown_onceNoSwimIsWaiting() {
assertEquals(
AppGate.Update,
gate(update = true, signedIn = true, poolStay = PoolStayState.None),
)
}

@Test
fun nothingIsShown_whileItIsUnknownWhetherASwimIsWaiting() {
// So the update never flashes up for a moment before the swim screen takes over.
assertEquals(
AppGate.Loading,
gate(update = true, signedIn = true, poolStay = PoolStayState.Loading),
)
assertEquals(
AppGate.Loading,
gate(update = false, signedIn = true, poolStay = PoolStayState.Loading),
)
}

@Test
fun theUpdate_isShown_toSomeoneNotSignedIn() {
assertEquals(
AppGate.Update,
gate(update = true, signedIn = false, poolStay = PoolStayState.None),
)
assertEquals(
AppGate.Update,
gate(update = true, signedIn = false, poolStay = PoolStayState.Loading),
)
}

@Test
fun signIn_isShown_whenNobodyIsSignedIn_andNoUpdateIsNeeded() {
assertEquals(
AppGate.SignIn,
gate(update = false, signedIn = false, poolStay = PoolStayState.None),
)
}

@Test
fun aSwimOfAnotherAccount_isIgnored_whenNobodyIsSignedIn() {
assertEquals(AppGate.SignIn, gate(update = false, signedIn = false, poolStay = logRequired))
}

@Test
fun theNormalApp_isShown_whenNothingTakesOver() {
assertEquals(AppGate.App, gate(update = false, signedIn = true, poolStay = PoolStayState.None))
}
}
Loading