summaryrefslogtreecommitdiff
path: root/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
blob: f83c7ba2719110ec3993c194c7cf4ed93db4cf38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/*
 * This file is part of GNU Taler
 * (C) 2020 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.cashier.config

import android.annotation.SuppressLint
import android.app.Application
import android.util.Log
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV
import androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
import androidx.security.crypto.MasterKeys
import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.http.HttpHeaders.Authorization
import io.ktor.http.HttpStatusCode.Companion.Unauthorized
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.taler.cashier.Response
import net.taler.cashier.Response.Companion.response
import net.taler.common.getIncompatibleStringOrNull
import net.taler.lib.common.Version

private val VERSION_BANK = Version(0, 0, 0)
private const val PREF_NAME = "net.taler.cashier.prefs"
private const val PREF_KEY_BANK_URL = "bankUrl"
private const val PREF_KEY_USERNAME = "username"
private const val PREF_KEY_PASSWORD = "password"
private const val PREF_KEY_CURRENCY = "currency"

private val TAG = ConfigManager::class.java.simpleName

class ConfigManager(
    private val app: Application,
    private val scope: CoroutineScope,
    private val httpClient: HttpClient
) {

    val configDestination = ConfigFragmentDirections.actionGlobalConfigFragment()

    private val masterKeyAlias = MasterKeys.getOrCreate(AES256_GCM_SPEC)
    private val prefs = EncryptedSharedPreferences.create(
        PREF_NAME, masterKeyAlias, app, AES256_SIV, AES256_GCM
    )

    internal var config = Config(
        bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!,
        username = prefs.getString(PREF_KEY_USERNAME, "")!!,
        password = prefs.getString(PREF_KEY_PASSWORD, "")!!
    )

    private val mCurrency = MutableLiveData<String>(
        prefs.getString(PREF_KEY_CURRENCY, null)
    )
    internal val currency: LiveData<String> = mCurrency

    private val mConfigResult = MutableLiveData<ConfigResult>()
    val configResult: LiveData<ConfigResult> = mConfigResult

    fun hasConfig() = config.bankUrl.isNotEmpty()
            && config.username.isNotEmpty()
            && config.password.isNotEmpty()

    /**
     * Start observing [configResult] after calling this to get the result async.
     * Warning: Ignore null results that are used to reset old results.
     */
    @UiThread
    fun checkAndSaveConfig(config: Config) = scope.launch {
        mConfigResult.value = null
        checkConfig(config).onError { failure ->
            val result = if (failure.isOffline(app)) {
                ConfigResult.Offline
            } else {
                ConfigResult.Error(failure.statusCode == Unauthorized, failure.msg)
            }
            mConfigResult.postValue(result)
        }.onSuccess { response ->
            val versionIncompatible =
                VERSION_BANK.getIncompatibleStringOrNull(app, response.version)
            val result = if (versionIncompatible != null) {
                ConfigResult.Error(false, versionIncompatible)
            } else {
                mCurrency.postValue(response.currency)
                prefs.edit().putString(PREF_KEY_CURRENCY, response.currency).apply()
                // save config
                saveConfig(config)
                ConfigResult.Success
            }
            mConfigResult.postValue(result)
        }
    }

    private suspend fun checkConfig(config: Config) = withContext(Dispatchers.IO) {
        val url = "${config.bankUrl}/config"
        Log.d(TAG, "Checking config: $url")
        val configResponse = response {
            httpClient.get(url) as ConfigResponse
        }
        if (configResponse.isFailure) {
            configResponse
        } else {
            // we need to check an endpoint that requires authentication as well
            // to see if the credentials are valid
            val balanceResponse = response {
                val authUrl = "${config.bankUrl}/accounts/${config.username}/balance"
                Log.d(TAG, "Checking auth: $authUrl")
                httpClient.get<Unit>(authUrl) {
                    header(Authorization, config.basicAuth)
                }
            }
            @Suppress("UNCHECKED_CAST")  // The type doesn't matter for failures
            if (balanceResponse.isFailure) balanceResponse as Response<ConfigResponse>
            else configResponse
        }
    }

    @WorkerThread
    @SuppressLint("ApplySharedPref")
    internal fun saveConfig(config: Config) {
        this.config = config
        prefs.edit()
            .putString(PREF_KEY_BANK_URL, config.bankUrl)
            .putString(PREF_KEY_USERNAME, config.username)
            .putString(PREF_KEY_PASSWORD, config.password)
            .commit()
    }

    fun lock() {
        saveConfig(config.copy(password = ""))
    }

}