Loading data/src/main/java/foundation/e/apps/data/login/repository/UserCapabilitiesProviderImpl.kt +13 −43 Original line number Diff line number Diff line Loading @@ -18,21 +18,17 @@ package foundation.e.apps.data.login.repository import android.content.SharedPreferences import foundation.e.apps.data.di.qualifiers.IoCoroutineScope import foundation.e.apps.data.preference.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.preference.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.preference.PREFERENCE_SHOW_PWA import foundation.e.apps.domain.auth.AuthSession import foundation.e.apps.domain.auth.AuthSessionRepository import foundation.e.apps.domain.auth.PlayStoreLoginMode import foundation.e.apps.domain.auth.UserCapabilities import foundation.e.apps.domain.auth.UserCapabilitiesProvider import foundation.e.apps.domain.source.SourceSelection import foundation.e.apps.domain.source.SourceSelectionRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import javax.inject.Inject Loading @@ -41,22 +37,18 @@ import javax.inject.Singleton @Singleton class UserCapabilitiesProviderImpl @Inject constructor( private val authSessionRepository: AuthSessionRepository, private val sharedPreferences: SharedPreferences, private val sourceSelectionRepository: SourceSelectionRepository, @IoCoroutineScope private val coroutineScope: CoroutineScope, ) : UserCapabilitiesProvider { override val capabilities: StateFlow<UserCapabilities> = combine( authSessionRepository.currentSession, booleanPreferenceFlow(PREFERENCE_SHOW_FOSS), booleanPreferenceFlow(PREFERENCE_SHOW_PWA), booleanPreferenceFlow(PREFERENCE_SHOW_GPLAY), ) { session, openSourceSelected, pwaSelected, playStoreSelected -> sourceSelectionRepository.sourceSelection, ) { session, sourceSelection -> buildCapabilities( session = session, openSourceSelected = openSourceSelected, pwaSelected = pwaSelected, playStoreSelected = playStoreSelected, sourceSelection = sourceSelection, ) }.stateIn( coroutineScope, Loading @@ -69,39 +61,16 @@ class UserCapabilitiesProviderImpl @Inject constructor( ) ) private fun booleanPreferenceFlow(key: String) = callbackFlow { trySend(sharedPreferences.getBoolean(key, true)) val listener = SharedPreferences.OnSharedPreferenceChangeListener { preferences, changedKey -> if (changedKey == key) { trySend(preferences.getBoolean(key, true)) } } sharedPreferences.registerOnSharedPreferenceChangeListener(listener) awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } }.stateIn( coroutineScope, SharingStarted.Eagerly, sharedPreferences.getBoolean(key, true) ) override suspend fun resolveCapabilities(): UserCapabilities { return buildCapabilities( session = authSessionRepository.resolveCurrentSession(), openSourceSelected = sharedPreferences.getBoolean(PREFERENCE_SHOW_FOSS, true), pwaSelected = sharedPreferences.getBoolean(PREFERENCE_SHOW_PWA, true), playStoreSelected = sharedPreferences.getBoolean(PREFERENCE_SHOW_GPLAY, true), sourceSelection = sourceSelectionRepository.currentSourceSelection(), ) } private fun buildCapabilities( session: AuthSession, openSourceSelected: Boolean, pwaSelected: Boolean, playStoreSelected: Boolean, sourceSelection: SourceSelection, ): UserCapabilities { if (session == AuthSession.Unauthenticated) { return UserCapabilities( Loading @@ -113,10 +82,11 @@ class UserCapabilitiesProviderImpl @Inject constructor( } return UserCapabilities( canAccessPlayStore = playStoreSelected && session is AuthSession.PlayStoreSession, canAccessOpenSource = openSourceSelected, canAccessPwa = pwaSelected, canPurchaseApps = playStoreSelected && canAccessPlayStore = sourceSelection.isPlayStoreSelected && session is AuthSession.PlayStoreSession, canAccessOpenSource = sourceSelection.isOpenSourceSelected, canAccessPwa = sourceSelection.isPwaSelected, canPurchaseApps = sourceSelection.isPlayStoreSelected && session is AuthSession.PlayStoreSession && session.loginMode != PlayStoreLoginMode.ANONYMOUS, ) Loading data/src/test/java/foundation/e/apps/data/login/repository/UserCapabilitiesProviderImplTest.kt +49 −51 Original line number Diff line number Diff line Loading @@ -18,15 +18,12 @@ package foundation.e.apps.data.login.repository import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import foundation.e.apps.data.preference.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.preference.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.preference.PREFERENCE_SHOW_PWA import foundation.e.apps.domain.auth.AuthSession import foundation.e.apps.domain.auth.AuthSessionRepository import foundation.e.apps.domain.auth.PlayStoreLoginMode import foundation.e.apps.domain.source.SourceSelection import foundation.e.apps.domain.source.SourceSelectionRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.StandardTestDispatcher Loading @@ -42,19 +39,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities track anonymous play store restrictions`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.anonymous", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.ANONYMOUS), ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -69,19 +58,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities stay disabled when unauthenticated`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.unauthenticated", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.Unauthenticated, ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -95,19 +76,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities keep open source access enabled for open source sessions`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.openSource", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.OpenSourceSession, ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -121,19 +94,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities keep play store access enabled during google session refresh`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.google", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.GOOGLE), ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -148,19 +113,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `resolveCapabilities reflects persisted google session state immediately`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.awaitGoogle", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.GOOGLE), ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -171,7 +128,34 @@ class UserCapabilitiesProviderImplTest { assertThat(capabilities.canPurchaseApps).isTrue() } private fun context(): Context = ApplicationProvider.getApplicationContext() @Test fun `capabilities react to source selection changes from repository`() = runTest { val sourceSelectionRepository = FakeSourceSelectionRepository() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.GOOGLE), ), sourceSelectionRepository = sourceSelectionRepository, coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) advanceUntilIdle() sourceSelectionRepository.saveSourceSelection( SourceSelection( isOpenSourceSelected = true, isPlayStoreSelected = false, isPwaSelected = false, ) ) advanceUntilIdle() val capabilities = repository.capabilities.value assertThat(capabilities.canAccessPlayStore).isFalse() assertThat(capabilities.canAccessOpenSource).isTrue() assertThat(capabilities.canAccessPwa).isFalse() assertThat(capabilities.canPurchaseApps).isFalse() } private class FakeAuthSessionRepository( session: AuthSession, Loading @@ -190,4 +174,18 @@ class UserCapabilitiesProviderImplTest { sessionState.value = AuthSession.Unauthenticated } } private class FakeSourceSelectionRepository( sourceSelection: SourceSelection = SourceSelection.DEFAULT, ) : SourceSelectionRepository { private val sourceSelectionState = MutableStateFlow(sourceSelection) override val sourceSelection: StateFlow<SourceSelection> = sourceSelectionState override fun currentSourceSelection(): SourceSelection = sourceSelectionState.value override fun saveSourceSelection(sourceSelection: SourceSelection) { sourceSelectionState.value = sourceSelection } } } Loading
data/src/main/java/foundation/e/apps/data/login/repository/UserCapabilitiesProviderImpl.kt +13 −43 Original line number Diff line number Diff line Loading @@ -18,21 +18,17 @@ package foundation.e.apps.data.login.repository import android.content.SharedPreferences import foundation.e.apps.data.di.qualifiers.IoCoroutineScope import foundation.e.apps.data.preference.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.preference.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.preference.PREFERENCE_SHOW_PWA import foundation.e.apps.domain.auth.AuthSession import foundation.e.apps.domain.auth.AuthSessionRepository import foundation.e.apps.domain.auth.PlayStoreLoginMode import foundation.e.apps.domain.auth.UserCapabilities import foundation.e.apps.domain.auth.UserCapabilitiesProvider import foundation.e.apps.domain.source.SourceSelection import foundation.e.apps.domain.source.SourceSelectionRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import javax.inject.Inject Loading @@ -41,22 +37,18 @@ import javax.inject.Singleton @Singleton class UserCapabilitiesProviderImpl @Inject constructor( private val authSessionRepository: AuthSessionRepository, private val sharedPreferences: SharedPreferences, private val sourceSelectionRepository: SourceSelectionRepository, @IoCoroutineScope private val coroutineScope: CoroutineScope, ) : UserCapabilitiesProvider { override val capabilities: StateFlow<UserCapabilities> = combine( authSessionRepository.currentSession, booleanPreferenceFlow(PREFERENCE_SHOW_FOSS), booleanPreferenceFlow(PREFERENCE_SHOW_PWA), booleanPreferenceFlow(PREFERENCE_SHOW_GPLAY), ) { session, openSourceSelected, pwaSelected, playStoreSelected -> sourceSelectionRepository.sourceSelection, ) { session, sourceSelection -> buildCapabilities( session = session, openSourceSelected = openSourceSelected, pwaSelected = pwaSelected, playStoreSelected = playStoreSelected, sourceSelection = sourceSelection, ) }.stateIn( coroutineScope, Loading @@ -69,39 +61,16 @@ class UserCapabilitiesProviderImpl @Inject constructor( ) ) private fun booleanPreferenceFlow(key: String) = callbackFlow { trySend(sharedPreferences.getBoolean(key, true)) val listener = SharedPreferences.OnSharedPreferenceChangeListener { preferences, changedKey -> if (changedKey == key) { trySend(preferences.getBoolean(key, true)) } } sharedPreferences.registerOnSharedPreferenceChangeListener(listener) awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } }.stateIn( coroutineScope, SharingStarted.Eagerly, sharedPreferences.getBoolean(key, true) ) override suspend fun resolveCapabilities(): UserCapabilities { return buildCapabilities( session = authSessionRepository.resolveCurrentSession(), openSourceSelected = sharedPreferences.getBoolean(PREFERENCE_SHOW_FOSS, true), pwaSelected = sharedPreferences.getBoolean(PREFERENCE_SHOW_PWA, true), playStoreSelected = sharedPreferences.getBoolean(PREFERENCE_SHOW_GPLAY, true), sourceSelection = sourceSelectionRepository.currentSourceSelection(), ) } private fun buildCapabilities( session: AuthSession, openSourceSelected: Boolean, pwaSelected: Boolean, playStoreSelected: Boolean, sourceSelection: SourceSelection, ): UserCapabilities { if (session == AuthSession.Unauthenticated) { return UserCapabilities( Loading @@ -113,10 +82,11 @@ class UserCapabilitiesProviderImpl @Inject constructor( } return UserCapabilities( canAccessPlayStore = playStoreSelected && session is AuthSession.PlayStoreSession, canAccessOpenSource = openSourceSelected, canAccessPwa = pwaSelected, canPurchaseApps = playStoreSelected && canAccessPlayStore = sourceSelection.isPlayStoreSelected && session is AuthSession.PlayStoreSession, canAccessOpenSource = sourceSelection.isOpenSourceSelected, canAccessPwa = sourceSelection.isPwaSelected, canPurchaseApps = sourceSelection.isPlayStoreSelected && session is AuthSession.PlayStoreSession && session.loginMode != PlayStoreLoginMode.ANONYMOUS, ) Loading
data/src/test/java/foundation/e/apps/data/login/repository/UserCapabilitiesProviderImplTest.kt +49 −51 Original line number Diff line number Diff line Loading @@ -18,15 +18,12 @@ package foundation.e.apps.data.login.repository import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import foundation.e.apps.data.preference.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.preference.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.preference.PREFERENCE_SHOW_PWA import foundation.e.apps.domain.auth.AuthSession import foundation.e.apps.domain.auth.AuthSessionRepository import foundation.e.apps.domain.auth.PlayStoreLoginMode import foundation.e.apps.domain.source.SourceSelection import foundation.e.apps.domain.source.SourceSelectionRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.StandardTestDispatcher Loading @@ -42,19 +39,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities track anonymous play store restrictions`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.anonymous", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.ANONYMOUS), ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -69,19 +58,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities stay disabled when unauthenticated`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.unauthenticated", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.Unauthenticated, ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -95,19 +76,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities keep open source access enabled for open source sessions`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.openSource", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.OpenSourceSession, ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -121,19 +94,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `capabilities keep play store access enabled during google session refresh`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.google", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.GOOGLE), ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -148,19 +113,11 @@ class UserCapabilitiesProviderImplTest { @Test fun `resolveCapabilities reflects persisted google session state immediately`() = runTest { val sharedPreferences = context() .getSharedPreferences("UserCapabilitiesProviderImplTest.awaitGoogle", Context.MODE_PRIVATE) sharedPreferences.edit() .putBoolean(PREFERENCE_SHOW_FOSS, true) .putBoolean(PREFERENCE_SHOW_PWA, true) .putBoolean(PREFERENCE_SHOW_GPLAY, true) .commit() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.GOOGLE), ), sharedPreferences = sharedPreferences, sourceSelectionRepository = FakeSourceSelectionRepository(), coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) Loading @@ -171,7 +128,34 @@ class UserCapabilitiesProviderImplTest { assertThat(capabilities.canPurchaseApps).isTrue() } private fun context(): Context = ApplicationProvider.getApplicationContext() @Test fun `capabilities react to source selection changes from repository`() = runTest { val sourceSelectionRepository = FakeSourceSelectionRepository() val repository = UserCapabilitiesProviderImpl( authSessionRepository = FakeAuthSessionRepository( session = AuthSession.PlayStoreSession(PlayStoreLoginMode.GOOGLE), ), sourceSelectionRepository = sourceSelectionRepository, coroutineScope = TestScope(StandardTestDispatcher(testScheduler)), ) advanceUntilIdle() sourceSelectionRepository.saveSourceSelection( SourceSelection( isOpenSourceSelected = true, isPlayStoreSelected = false, isPwaSelected = false, ) ) advanceUntilIdle() val capabilities = repository.capabilities.value assertThat(capabilities.canAccessPlayStore).isFalse() assertThat(capabilities.canAccessOpenSource).isTrue() assertThat(capabilities.canAccessPwa).isFalse() assertThat(capabilities.canPurchaseApps).isFalse() } private class FakeAuthSessionRepository( session: AuthSession, Loading @@ -190,4 +174,18 @@ class UserCapabilitiesProviderImplTest { sessionState.value = AuthSession.Unauthenticated } } private class FakeSourceSelectionRepository( sourceSelection: SourceSelection = SourceSelection.DEFAULT, ) : SourceSelectionRepository { private val sourceSelectionState = MutableStateFlow(sourceSelection) override val sourceSelection: StateFlow<SourceSelection> = sourceSelectionState override fun currentSourceSelection(): SourceSelection = sourceSelectionState.value override fun saveSourceSelection(sourceSelection: SourceSelection) { sourceSelectionState.value = sourceSelection } } }