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? ) = 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? = 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 = 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) } } }