BIT-752: Add Environment/EnvironmentRepository/EnvironmentDiskSource (#151)

This commit is contained in:
Brian Yencho
2023-10-24 10:46:33 -05:00
committed by Álison Fernandes
parent f4dbe68527
commit e4ab70a106
15 changed files with 575 additions and 24 deletions

View File

@@ -1,16 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val BASE_KEY = "bwPreferencesStorage"
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
private const val STATE_KEY = "$BASE_KEY:state"
@@ -18,9 +17,10 @@ private const val STATE_KEY = "$BASE_KEY:state"
* Primary implementation of [AuthDiskSource].
*/
class AuthDiskSourceImpl(
private val sharedPreferences: SharedPreferences,
sharedPreferences: SharedPreferences,
private val json: Json,
) : AuthDiskSource {
) : BaseDiskSource(sharedPreferences = sharedPreferences),
AuthDiskSource {
override var rememberedEmailAddress: String?
get() = getString(key = REMEMBERED_EMAIL_ADDRESS_KEY)
set(value) {
@@ -48,25 +48,12 @@ class AuthDiskSourceImpl(
extraBufferCapacity = Int.MAX_VALUE,
)
private val onSharedPreferenceChangeListener =
OnSharedPreferenceChangeListener { _, key ->
when (key) {
STATE_KEY -> mutableUserStateFlow.tryEmit(userState)
}
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?,
) {
when (key) {
STATE_KEY -> mutableUserStateFlow.tryEmit(userState)
}
init {
sharedPreferences
.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener)
}
private fun getString(
key: String,
default: String? = null,
): String? = sharedPreferences.getString(key, default)
private fun putString(
key: String,
value: String?,
): Unit = sharedPreferences.edit { putString(key, value) }
}

View File

@@ -0,0 +1,33 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit
/**
* Base class for simplifying interactions with [SharedPreferences].
*/
abstract class BaseDiskSource(
private val sharedPreferences: SharedPreferences,
) : OnSharedPreferenceChangeListener {
init {
@Suppress("LeakingThis")
sharedPreferences
.registerOnSharedPreferenceChangeListener(this)
}
protected fun getString(
key: String,
default: String? = null,
): String? = sharedPreferences.getString(key, default)
protected fun putString(
key: String,
value: String?,
): Unit = sharedPreferences.edit { putString(key, value) }
companion object {
const val BASE_KEY: String = "bwPreferencesStorage"
}
}

View File

@@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import kotlinx.coroutines.flow.Flow
/**
* Primary access point for general environment-related disk information.
*/
interface EnvironmentDiskSource {
/**
* The currently persisted [EnvironmentUrlDataJson] (or `null` if not set).
*/
var preAuthEnvironmentUrlData: EnvironmentUrlDataJson?
/**
* Emits updates that track [preAuthEnvironmentUrlData]. This will replay the last known value,
* if any.
*/
val preAuthEnvironmentUrlDataFlow: Flow<EnvironmentUrlDataJson?>
}

View File

@@ -0,0 +1,48 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val PRE_AUTH_URLS_KEY = "$BASE_KEY:preAuthEnvironmentUrls"
/**
* Primary implementation of [EnvironmentDiskSource].
*/
class EnvironmentDiskSourceImpl(
sharedPreferences: SharedPreferences,
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
EnvironmentDiskSource {
override var preAuthEnvironmentUrlData: EnvironmentUrlDataJson?
get() = getString(key = PRE_AUTH_URLS_KEY)?.let { json.decodeFromString(it) }
set(value) {
putString(
key = PRE_AUTH_URLS_KEY,
value = value?.let { json.encodeToString(value) },
)
}
override val preAuthEnvironmentUrlDataFlow: Flow<EnvironmentUrlDataJson?>
get() = mutableEnvironmentUrlDataFlow
.onSubscription { emit(preAuthEnvironmentUrlData) }
private val mutableEnvironmentUrlDataFlow = MutableSharedFlow<EnvironmentUrlDataJson?>(
replay = 1,
extraBufferCapacity = Int.MAX_VALUE,
)
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?,
) {
when (key) {
PRE_AUTH_URLS_KEY -> mutableEnvironmentUrlDataFlow.tryEmit(preAuthEnvironmentUrlData)
}
}
}

View File

@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.di
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import javax.inject.Singleton
/**
* Provides persistence-related dependencies in the platform package.
*/
@Module
@InstallIn(SingletonComponent::class)
object DiskModule {
@Provides
@Singleton
fun provideEnvironmentDiskSource(
sharedPreferences: SharedPreferences,
json: Json,
): EnvironmentDiskSource =
EnvironmentDiskSourceImpl(
sharedPreferences = sharedPreferences,
json = json,
)
}

View File

@@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.platform.repository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import kotlinx.coroutines.flow.StateFlow
/**
* Provides an API for observing and modifying environment state.
*/
interface EnvironmentRepository {
/**
* The currently set environment.
*/
var environment: Environment
/**
* Emits updates that track [environment].
*/
val environmentStateFlow: StateFlow<Environment>
}

View File

@@ -0,0 +1,48 @@
package com.x8bit.bitwarden.data.platform.repository
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/**
* Primary implementation of [EnvironmentRepository].
*/
class EnvironmentRepositoryImpl(
private val environmentDiskSource: EnvironmentDiskSource,
private val dispatcher: CoroutineDispatcher,
) : EnvironmentRepository {
private val scope = CoroutineScope(dispatcher)
override var environment: Environment
get() = environmentDiskSource
.preAuthEnvironmentUrlData
.toEnvironmentUrlsOrDefault()
set(value) {
environmentDiskSource.preAuthEnvironmentUrlData = value.environmentUrlData
}
override val environmentStateFlow: StateFlow<Environment>
get() = environmentDiskSource
.preAuthEnvironmentUrlDataFlow
.map { it.toEnvironmentUrlsOrDefault() }
.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = Environment.Us,
)
}
/**
* Converts a nullable [EnvironmentUrlDataJson] to an [Environment], where `null` values default to
* the US environment.
*/
private fun EnvironmentUrlDataJson?.toEnvironmentUrlsOrDefault(): Environment =
this?.toEnvironmentUrls() ?: Environment.Us

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.onEach
class NetworkConfigRepositoryImpl(
private val authRepository: AuthRepository,
private val authTokenInterceptor: AuthTokenInterceptor,
private val environmentRepository: EnvironmentRepository,
dispatcher: CoroutineDispatcher,
) : NetworkConfigRepository {
@@ -30,5 +31,12 @@ class NetworkConfigRepositoryImpl(
}
}
.launchIn(scope)
environmentRepository
.environmentStateFlow
.onEach { environment ->
// TODO: Update base URL interceptors (BIT-725)
}
.launchIn(scope)
}
}

View File

@@ -1,7 +1,10 @@
package com.x8bit.bitwarden.data.platform.repository.di
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepository
import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepositoryImpl
import dagger.Module
@@ -18,15 +21,27 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
@Singleton
fun provideEnvironmentRepository(
environmentDiskSource: EnvironmentDiskSource,
): EnvironmentRepository =
EnvironmentRepositoryImpl(
environmentDiskSource = environmentDiskSource,
dispatcher = Dispatchers.IO,
)
@Provides
@Singleton
fun provideNetworkConfigRepository(
authRepository: AuthRepository,
authTokenInterceptor: AuthTokenInterceptor,
environmentRepository: EnvironmentRepository,
): NetworkConfigRepository =
NetworkConfigRepositoryImpl(
authRepository = authRepository,
authTokenInterceptor = authTokenInterceptor,
environmentRepository = environmentRepository,
dispatcher = Dispatchers.IO,
)
}

View File

@@ -0,0 +1,70 @@
package com.x8bit.bitwarden.data.platform.repository.model
import android.os.Parcelable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
/**
* A higher-level wrapper around [EnvironmentUrlDataJson] that provides type-safety, enumerability,
* and human-readable labels.
*/
sealed class Environment : Parcelable {
/**
* The [Type] of the environment.
*/
abstract val type: Type
/**
* The raw [environmentUrlData] that contains specific base URLs for each relevant domain.
*/
abstract val environmentUrlData: EnvironmentUrlDataJson
/**
* Helper for a returning a human-readable label from a [Type].
*/
val label: Text get() = type.label
/**
* The default US environment.
*/
@Parcelize
data object Us : Environment() {
override val type: Type get() = Type.US
override val environmentUrlData: EnvironmentUrlDataJson
get() = EnvironmentUrlDataJson.DEFAULT_US
}
/**
* The default EU environment.
*/
@Parcelize
data object Eu : Environment() {
override val type: Type get() = Type.EU
override val environmentUrlData: EnvironmentUrlDataJson
get() = EnvironmentUrlDataJson.DEFAULT_EU
}
/**
* A custom self-hosted environment with a fully configurable [environmentUrlData].
*/
@Parcelize
data class SelfHosted(
override val environmentUrlData: @RawValue EnvironmentUrlDataJson,
) : Environment() {
override val type: Type get() = Type.SELF_HOSTED
}
/**
* A summary of the various types that can be enumerated over and which contains a
* human-readable [label].
*/
enum class Type(val label: Text) {
US(label = "bitwarden.com".asText()),
EU(label = "bitwarden.eu".asText()),
SELF_HOSTED(label = R.string.self_hosted.asText()),
}
}

View File

@@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.platform.repository.util
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.repository.model.Environment
/**
* Converts a raw [EnvironmentUrlDataJson] to an externally-consumable [Environment].
*/
fun EnvironmentUrlDataJson.toEnvironmentUrls(): Environment =
when (this) {
Environment.Us.environmentUrlData -> Environment.Us
Environment.Eu.environmentUrlData -> Environment.Eu
else -> Environment.SelfHosted(environmentUrlData = this)
}