Change in clear cache behavior:

- Do not reset the analytics store, so that we do not ask the user consent again => Parity with iOS.
- Do not reset the permission store, because it contains information that's related to the system permission, which cannot be retrieved otherwise => Should help with #3195.
This commit is contained in:
Benoit Marty
2025-09-22 10:30:04 +02:00
committed by Benoit Marty
parent bb5ef7a62c
commit f683728f35
15 changed files with 1 additions and 114 deletions

View File

@@ -15,9 +15,6 @@ import kotlinx.coroutines.flow.StateFlow
interface FtueService { interface FtueService {
/** The current state of the FTUE. */ /** The current state of the FTUE. */
val state: StateFlow<FtueState> val state: StateFlow<FtueState>
/** Reset the FTUE state. */
suspend fun reset()
} }
/** The state of the FTUE. */ /** The state of the FTUE. */

View File

@@ -58,13 +58,6 @@ class DefaultFtueService(
} }
} }
override suspend fun reset() {
analyticsService.reset()
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
}
}
init { init {
combine( combine(
sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus -> sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus ->

View File

@@ -27,8 +27,6 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@@ -191,47 +189,6 @@ class DefaultFtueServiceTest {
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete) assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
} }
} }
@Test
fun `reset do the expected actions S`() = runTest {
val resetAnalyticsLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService(
resetLambda = resetAnalyticsLambda
)
val resetPermissionLambda = lambdaRecorder<String, Unit> { }
val permissionStateProvider = FakePermissionStateProvider(
resetPermissionLambda = resetPermissionLambda
)
val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.S,
permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService,
)
service.reset()
resetAnalyticsLambda.assertions().isCalledOnce()
resetPermissionLambda.assertions().isNeverCalled()
}
@Test
fun `reset do the expected actions TIRAMISU`() = runTest {
val resetLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService(
resetLambda = resetLambda
)
val resetPermissionLambda = lambdaRecorder<String, Unit> { }
val permissionStateProvider = FakePermissionStateProvider(
resetPermissionLambda = resetPermissionLambda
)
val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.TIRAMISU,
permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService,
)
service.reset()
resetLambda.assertions().isCalledOnce()
resetPermissionLambda.assertions().isCalledOnce()
.with(value("android.permission.POST_NOTIFICATIONS"))
}
} }
internal fun TestScope.createDefaultFtueService( internal fun TestScope.createDefaultFtueService(

View File

@@ -9,18 +9,11 @@ package io.element.android.features.ftue.test
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class FakeFtueService( class FakeFtueService : FtueService {
private val resetLambda: () -> Unit = { lambdaError() },
) : FtueService {
override val state: MutableStateFlow<FtueState> = MutableStateFlow(FtueState.Unknown) override val state: MutableStateFlow<FtueState> = MutableStateFlow(FtueState.Unknown)
override suspend fun reset() {
resetLambda()
}
suspend fun emitState(newState: FtueState) { suspend fun emitState(newState: FtueState) {
state.emit(newState) state.emit(newState)
} }

View File

@@ -72,7 +72,6 @@ dependencies {
implementation(projects.features.rageshake.api) implementation(projects.features.rageshake.api)
implementation(projects.features.lockscreen.api) implementation(projects.features.lockscreen.api)
implementation(projects.features.analytics.api) implementation(projects.features.analytics.api)
implementation(projects.features.ftue.api)
implementation(projects.features.licenses.api) implementation(projects.features.licenses.api)
implementation(projects.features.logout.api) implementation(projects.features.logout.api)
implementation(projects.features.deactivation.api) implementation(projects.features.deactivation.api)
@@ -101,7 +100,6 @@ dependencies {
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.features.ftue.test)
testImplementation(projects.features.invite.test) testImplementation(projects.features.invite.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.logout.test) testImplementation(projects.features.logout.test)

View File

@@ -12,7 +12,6 @@ import coil3.SingletonImageLoader
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.Provider import dev.zacsweers.metro.Provider
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -36,7 +35,6 @@ class DefaultClearCacheUseCase(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val defaultCacheService: DefaultCacheService, private val defaultCacheService: DefaultCacheService,
private val okHttpClient: Provider<OkHttpClient>, private val okHttpClient: Provider<OkHttpClient>,
private val ftueService: FtueService,
private val pushService: PushService, private val pushService: PushService,
private val seenInvitesStore: SeenInvitesStore, private val seenInvitesStore: SeenInvitesStore,
private val activeRoomsHolder: ActiveRoomsHolder, private val activeRoomsHolder: ActiveRoomsHolder,
@@ -56,7 +54,6 @@ class DefaultClearCacheUseCase(
// Clear app cache // Clear app cache
context.cacheDir.deleteRecursively() context.cacheDir.deleteRecursively()
// Clear some settings // Clear some settings
ftueService.reset()
seenInvitesStore.clear() seenInvitesStore.clear()
// Ensure any error will be displayed again // Ensure any error will be displayed again
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false) pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)

View File

@@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.tasks
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.test.FakeFtueService
import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@@ -41,10 +40,6 @@ class DefaultClearCacheUseCaseTest {
clearCacheLambda = clearCacheLambda, clearCacheLambda = clearCacheLambda,
) )
val defaultCacheService = DefaultCacheService() val defaultCacheService = DefaultCacheService()
val resetFtueLambda = lambdaRecorder<Unit> { }
val ftueService = FakeFtueService(
resetLambda = resetFtueLambda,
)
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> } val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
val resetBatteryOptimizationStateResult = lambdaRecorder<Unit> { } val resetBatteryOptimizationStateResult = lambdaRecorder<Unit> { }
val pushService = FakePushService( val pushService = FakePushService(
@@ -59,7 +54,6 @@ class DefaultClearCacheUseCaseTest {
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
defaultCacheService = defaultCacheService, defaultCacheService = defaultCacheService,
okHttpClient = { OkHttpClient.Builder().build() }, okHttpClient = { OkHttpClient.Builder().build() },
ftueService = ftueService,
pushService = pushService, pushService = pushService,
seenInvitesStore = seenInvitesStore, seenInvitesStore = seenInvitesStore,
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,
@@ -67,7 +61,6 @@ class DefaultClearCacheUseCaseTest {
defaultCacheService.clearedCacheEventFlow.test { defaultCacheService.clearedCacheEventFlow.test {
sut.invoke() sut.invoke()
clearCacheLambda.assertions().isCalledOnce() clearCacheLambda.assertions().isCalledOnce()
resetFtueLambda.assertions().isCalledOnce()
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
.with(value(matrixClient.sessionId), value(false)) .with(value(matrixClient.sessionId), value(false))
resetBatteryOptimizationStateResult.assertions().isCalledOnce() resetBatteryOptimizationStateResult.assertions().isCalledOnce()

View File

@@ -16,6 +16,4 @@ interface PermissionStateProvider {
suspend fun setPermissionAsked(permission: String, value: Boolean) suspend fun setPermissionAsked(permission: String, value: Boolean)
fun isPermissionAsked(permission: String): Flow<Boolean> fun isPermissionAsked(permission: String): Flow<Boolean>
suspend fun resetPermission(permission: String)
} }

View File

@@ -40,6 +40,4 @@ class DefaultPermissionStateProvider(
override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value) override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value)
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionsStore.isPermissionAsked(permission) override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionsStore.isPermissionAsked(permission)
override suspend fun resetPermission(permission: String) = permissionsStore.resetPermission(permission)
} }

View File

@@ -15,7 +15,6 @@ class FakePermissionStateProvider(
private var permissionGranted: Boolean = true, private var permissionGranted: Boolean = true,
permissionDenied: Boolean = false, permissionDenied: Boolean = false,
permissionAsked: Boolean = false, permissionAsked: Boolean = false,
private val resetPermissionLambda: (String) -> Unit = {},
) : PermissionStateProvider { ) : PermissionStateProvider {
private val permissionDeniedFlow = MutableStateFlow(permissionDenied) private val permissionDeniedFlow = MutableStateFlow(permissionDenied)
private val permissionAskedFlow = MutableStateFlow(permissionAsked) private val permissionAskedFlow = MutableStateFlow(permissionAsked)
@@ -37,10 +36,4 @@ class FakePermissionStateProvider(
} }
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionAskedFlow override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionAskedFlow
override suspend fun resetPermission(permission: String) {
setPermissionAsked(permission, false)
setPermissionDenied(permission, false)
resetPermissionLambda(permission)
}
} }

View File

@@ -47,9 +47,4 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
* Update analyticsId from the AccountData. * Update analyticsId from the AccountData.
*/ */
suspend fun setAnalyticsId(analyticsId: String) suspend fun setAnalyticsId(analyticsId: String)
/**
* Reset the analytics service (will ask for user consent again).
*/
suspend fun reset()
} }

View File

@@ -70,10 +70,6 @@ class DefaultAnalyticsService(
analyticsStore.setDidAskUserConsent() analyticsStore.setDidAskUserConsent()
} }
override suspend fun reset() {
analyticsStore.setDidAskUserConsent(false)
}
override suspend fun setAnalyticsId(analyticsId: String) { override suspend fun setAnalyticsId(analyticsId: String) {
Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)") Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)")
analyticsStore.setAnalyticsId(analyticsId) analyticsStore.setAnalyticsId(analyticsId)

View File

@@ -180,20 +180,6 @@ class DefaultAnalyticsServiceTest {
resetLambda.assertions().isCalledOnce() resetLambda.assertions().isCalledOnce()
} }
@Test
fun `when reset is invoked, the user consent is reset`() = runTest {
val store = FakeAnalyticsStore(
defaultDidAskUserConsent = true,
)
val sut = createDefaultAnalyticsService(
coroutineScope = backgroundScope,
analyticsStore = store,
)
assertThat(store.didAskUserConsentFlow.first()).isTrue()
sut.reset()
assertThat(store.didAskUserConsentFlow.first()).isFalse()
}
@Test @Test
fun `when a session is added, nothing happen`() = runTest { fun `when a session is added, nothing happen`() = runTest {
val sut = createDefaultAnalyticsService( val sut = createDefaultAnalyticsService(

View File

@@ -31,7 +31,6 @@ class NoopAnalyticsService : AnalyticsService {
override suspend fun setDidAskUserConsent() = Unit override suspend fun setDidAskUserConsent() = Unit
override val analyticsIdFlow: Flow<String> = flowOf("") override val analyticsIdFlow: Flow<String> = flowOf("")
override suspend fun setAnalyticsId(analyticsId: String) = Unit override suspend fun setAnalyticsId(analyticsId: String) = Unit
override suspend fun reset() = Unit
override fun capture(event: VectorAnalyticsEvent) = Unit override fun capture(event: VectorAnalyticsEvent) = Unit
override fun screen(screen: VectorAnalyticsScreen) = Unit override fun screen(screen: VectorAnalyticsScreen) = Unit
override fun updateUserProperties(userProperties: UserProperties) = Unit override fun updateUserProperties(userProperties: UserProperties) = Unit

View File

@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.asStateFlow
class FakeAnalyticsService( class FakeAnalyticsService(
isEnabled: Boolean = false, isEnabled: Boolean = false,
didAskUserConsent: Boolean = false, didAskUserConsent: Boolean = false,
private val resetLambda: () -> Unit = {},
) : AnalyticsService { ) : AnalyticsService {
private val isEnabledFlow = MutableStateFlow(isEnabled) private val isEnabledFlow = MutableStateFlow(isEnabled)
override val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) override val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
@@ -65,9 +64,4 @@ class FakeAnalyticsService(
override fun updateSuperProperties(updatedProperties: SuperProperties) { override fun updateSuperProperties(updatedProperties: SuperProperties) {
// No op // No op
} }
override suspend fun reset() {
didAskUserConsentFlow.value = false
resetLambda()
}
} }