194 lines
6.2 KiB
Kotlin
194 lines
6.2 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.sync.Mutex
|
|
import kotlinx.coroutines.sync.withLock
|
|
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.
|
|
*/
|
|
class AndroidPreferenceProvider private constructor(
|
|
private val sharedPreferences: SharedPreferences,
|
|
private val dispatcher: CoroutineDispatcher
|
|
) : PreferenceProvider {
|
|
private val mutex = Mutex()
|
|
/*
|
|
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
|
|
* confines them to a single background thread.
|
|
*/
|
|
|
|
override suspend fun hasKey(key: PreferenceKey) =
|
|
withContext(dispatcher) {
|
|
sharedPreferences.contains(key.key)
|
|
}
|
|
|
|
@SuppressLint("ApplySharedPref")
|
|
override suspend fun putString(
|
|
key: PreferenceKey,
|
|
value: String?
|
|
) = withContext(dispatcher) {
|
|
mutex.withLock {
|
|
val editor = sharedPreferences.edit()
|
|
|
|
editor.putString(key.key, value)
|
|
|
|
editor.commit()
|
|
|
|
Unit
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ApplySharedPref")
|
|
override suspend fun putStringSet(
|
|
key: PreferenceKey,
|
|
value: Set<String>?
|
|
) = withContext(dispatcher) {
|
|
mutex.withLock {
|
|
val editor = sharedPreferences.edit()
|
|
|
|
editor.putStringSet(key.key, value)
|
|
|
|
editor.commit()
|
|
|
|
Unit
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ApplySharedPref")
|
|
override suspend fun putLong(
|
|
key: PreferenceKey,
|
|
value: Long?
|
|
) = withContext(dispatcher) {
|
|
mutex.withLock {
|
|
val editor = sharedPreferences.edit()
|
|
|
|
if (value != null) {
|
|
editor.putLong(key.key, value)
|
|
} else {
|
|
editor.remove(key.key)
|
|
}
|
|
editor.commit()
|
|
Unit
|
|
}
|
|
}
|
|
|
|
override suspend fun getLong(key: PreferenceKey): Long? =
|
|
withContext(dispatcher) {
|
|
if (sharedPreferences.contains(key.key)) {
|
|
sharedPreferences.getLong(key.key, 0)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
override suspend fun getString(key: PreferenceKey) =
|
|
withContext(dispatcher) {
|
|
sharedPreferences.getString(key.key, null)
|
|
}
|
|
|
|
override suspend fun getStringSet(key: PreferenceKey): Set<String>? =
|
|
withContext(dispatcher) {
|
|
sharedPreferences.getStringSet(key.key, null)
|
|
}
|
|
|
|
@SuppressLint("ApplySharedPref")
|
|
override suspend fun clearPreferences() =
|
|
withContext(dispatcher) {
|
|
val editor = sharedPreferences.edit()
|
|
|
|
editor.clear()
|
|
|
|
return@withContext editor.commit()
|
|
}
|
|
|
|
override fun observe(key: PreferenceKey): Flow<String?> =
|
|
callbackFlow {
|
|
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 sharedPreferences =
|
|
withContext(singleThreadedDispatcher) {
|
|
val mainKey =
|
|
MasterKey
|
|
.Builder(context)
|
|
.apply {
|
|
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
}.build()
|
|
|
|
EncryptedSharedPreferences.create(
|
|
context,
|
|
filename,
|
|
mainKey,
|
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
)
|
|
}
|
|
|
|
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
|
|
}
|
|
}
|
|
}
|