diff --git a/CHANGELOG.md b/CHANGELOG.md index 88db49a84..9be3b93f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2 ## [Unreleased] -### Added +### Fixed +- Shared preferences object cached in-memory and locked with semaphore in order to improve stability of security-crypto library - Shielded address is not rotated by navigating to Receive and Request screens from homepage ## [2.0.2 (962)] - 2025-05-14 diff --git a/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt b/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt index b4ed07698..e25748471 100644 --- a/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt +++ b/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt @@ -30,7 +30,7 @@ import java.util.concurrent.Executors * 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( +class AndroidPreferenceProvider( private val sharedPreferences: SharedPreferences, private val dispatcher: CoroutineDispatcher ) : PreferenceProvider { @@ -163,15 +163,26 @@ class AndroidPreferenceProvider private constructor( } } - 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. - */ + companion object Factory : AndroidPreferenceFactory by AndroidPreferenceFactoryImpl() +} + +interface AndroidPreferenceFactory { + suspend fun newStandard(context: Context, filename: String): PreferenceProvider + + suspend fun newEncrypted(context: Context, filename: String): PreferenceProvider +} + +private class AndroidPreferenceFactoryImpl : AndroidPreferenceFactory { + private val semaphore = Mutex() + private val standardCache = mutableMapOf() + private val encryptedCache = mutableMapOf() + + override suspend fun newStandard(context: Context, filename: String) = + getOrCreate(standardCache, filename) { + /* + * 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 = @@ -182,14 +193,12 @@ class AndroidPreferenceProvider private constructor( 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. - */ + override suspend fun newEncrypted(context: Context, filename: String) = + getOrCreate(encryptedCache, filename) { + /* + * 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 = @@ -212,5 +221,10 @@ class AndroidPreferenceProvider private constructor( return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher) } - } + + private suspend inline fun getOrCreate( + map: MutableMap, + filename: String, + block: () -> PreferenceProvider + ): PreferenceProvider = semaphore.withLock { map.getOrPut(filename, block) } }