OXYGENHUB

Coverage / app/src/main/java/io/oxygenhub/android/auth/SessionManager.kt 100.0%

44 of 44 lines run, 13 of 13 functions, 10 of 14 branches · run #1628 on main · run missed

1
package io.oxygenhub.android.auth
2
3
import io.oxygenhub.android.api.HubApi
4
import io.oxygenhub.android.api.HubException
5
import kotlinx.coroutines.flow.MutableStateFlow
6
import kotlinx.coroutines.flow.StateFlow
7
import kotlinx.coroutines.flow.asStateFlow
8
9
/** What signing out did on the hub. */
10
enum class SignOutResult {
11
    /** The token is revoked on the hub. */
12
    Revoked,
13
14
    /** The hub no longer knew the token (revoked or expired already). */
15
    AlreadyGone,
16
17
    /** Signed out here, but the hub could not be told: revoke it under Settings, Tokens. */
18
    NotRevoked,
19
}
20
21
/**
22
 * The app's one session: who is signed in, the API client for them, and
23
 * signing in and out. [apiFactory] makes the client for a session; it is
24
 * given a callback to run when the hub refuses the token.
25
 */
26
class SessionManager(
27
    private val store: SessionStore,
28
    private val apiFactory: (Session, onUnauthorized: () -> Unit) -> HubApi,
29
) {
30
    private val _session = MutableStateFlow(store.load())
31
    val session: StateFlow<Session?> = _session.asStateFlow()
32
33
    /** Why the last session ended without the user signing out, for the sign-in screen to say. */
34
    private val _ended = MutableStateFlow<String?>(null)
35
    val ended: StateFlow<String?> = _ended.asStateFlow()
36
37
    private var api: Pair<Session, HubApi>? = null
38
39
    val lastHub: String? get() = store.lastHub
40
41
    /** The client for the signed-in session, or null when signed out. */
42
    fun api(): HubApi? {
43
        val s = _session.value ?: return null
44
        api?.let { (owner, client) -> if (owner == s) return client }
45
        return apiFactory(s) { expired(s) }.also { api = s to it }
46
    }
47
48
    fun signedIn(session: Session) {
49
        store.save(session)
50
        _ended.value = null
51
        _session.value = session
52
    }
53
54
    fun rememberHub(hub: String) {
55
        store.lastHub = hub
56
    }
57
58
    /** Revokes the token on the hub, then forgets it here whatever the hub said. */
59
    suspend fun signOut(): SignOutResult {
60
        val client = api()
61
        val result = when {
62
            client == null -> SignOutResult.AlreadyGone
63
            else -> try {
64
                client.revokeToken()
65
                SignOutResult.Revoked
66
            } catch (e: HubException.Http) {
67
                if (e.status == 401) SignOutResult.AlreadyGone else SignOutResult.NotRevoked
68
            } catch (_: HubException) {
69
                SignOutResult.NotRevoked
70
            }
71
        }
72
        forget()
73
        if (result == SignOutResult.NotRevoked) {
74
            _ended.value = "Signed out here, but the hub couldn't be reached to revoke the token. " +
75
                "Revoke it on the hub under Settings, Tokens."
76
        }
77
        return result
78
    }
79
80
    /** The hub refused the token: forget it and ask for a new sign-in. */
81
    private fun expired(session: Session) {
82
        if (_session.value != session) return
83
        forget()
84
        _ended.value = "Your sign-in to ${session.hub} has ended (the token was revoked or not used for 90 days). Sign in again."
85
    }
86
87
    private fun forget() {
88
        store.clear()
89
        api = null
90
        _session.value = null
91
    }
92
93
    fun dismissEnded() {
94
        _ended.value = null
95
    }
96
}