secant-android-wallet/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt

117 lines
4.3 KiB
Kotlin

package co.electriccoin.zcash.preference
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
/**
* Provides an Android implementation of shared preferences.
*
* This class is thread-safe.
*
* For a given preference file, it is expected that only a single instance is constructed and that
* this instance lives for the lifetime of the application. Constructing multiple instances will
* potentially corrupt preference data and will leak resources.
*/
/*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
* confines them to a single background thread.
*/
class AndroidPreferenceProvider(
private val sharedPreferences: SharedPreferences,
private val dispatcher: CoroutineDispatcher
) : PreferenceProvider {
override suspend fun hasKey(key: PreferenceKey) = withContext(dispatcher) {
sharedPreferences.contains(key.key)
}
@SuppressLint("ApplySharedPref")
override suspend fun putString(key: PreferenceKey, value: String?) = withContext(dispatcher) {
val editor = sharedPreferences.edit()
editor.putString(key.key, value)
editor.commit()
Unit
}
override suspend fun getString(key: PreferenceKey) = withContext(dispatcher) {
sharedPreferences.getString(key.key, null)
}
override fun observe(key: PreferenceKey): Flow<String?> = callbackFlow<Unit> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
// Callback on main thread
trySend(Unit)
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
// Kickstart the emissions
trySend(Unit)
awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}.flowOn(dispatcher)
.map { getString(key) }
companion object {
suspend fun newStandard(context: Context, filename: String): PreferenceProvider {
/*
* Because of this line, we don't want multiple instances of this object created
* because we don't clean up the thread afterwards.
*/
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val sharedPreferences = withContext(singleThreadedDispatcher) {
context.getSharedPreferences(filename, Context.MODE_PRIVATE)
}
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
}
suspend fun newEncrypted(context: Context, filename: String): PreferenceProvider {
/*
* Because of this line, we don't want multiple instances of this object created
* because we don't clean up the thread afterwards.
*/
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val mainKey = withContext(singleThreadedDispatcher) {
@Suppress("BlockingMethodInNonBlockingContext")
MasterKey.Builder(context).apply {
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
}.build()
}
val sharedPreferences = withContext(singleThreadedDispatcher) {
@Suppress("BlockingMethodInNonBlockingContext")
EncryptedSharedPreferences.create(
context,
filename,
mainKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
}
}
}