Merge pull request #648 from vector-im/feature/bma/clearCache

Developer option to clear cache
This commit is contained in:
Benoit Marty 2023-06-26 12:06:26 +02:00 committed by GitHub
commit 52fd72d24b
53 changed files with 690 additions and 106 deletions

View file

@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="bmarty">
<words>
<w>homeserver</w>
</words>
</dictionary>
</component>

View file

@ -2,8 +2,10 @@
<dictionary name="shared"> <dictionary name="shared">
<words> <words>
<w>backstack</w> <w>backstack</w>
<w>homeserver</w>
<w>kover</w> <w>kover</w>
<w>onboarding</w> <w>onboarding</w>
<w>showkase</w>
<w>textfields</w> <w>textfields</w>
</words> </words>
</dictionary> </dictionary>

View file

@ -44,6 +44,7 @@ import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView import io.element.android.appnav.root.RootView
import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.login.api.oidc.OidcActionFlow
import io.element.android.features.preferences.api.CacheService
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@ -54,7 +55,9 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val cacheService: CacheService,
private val matrixClientsHolder: MatrixClientsHolder, private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter, private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint, private val bugReportEntryPoint: BugReportEntryPoint,
@ -88,11 +92,19 @@ class RootFlowNode @AssistedInject constructor(
private fun observeLoggedInState() { private fun observeLoggedInState() {
authenticationService.isLoggedIn() authenticationService.isLoggedIn()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { isLoggedIn -> .combine(
Timber.v("isLoggedIn=$isLoggedIn") cacheService.cacheIndex().onEach {
Timber.v("cacheIndex=$it")
matrixClientsHolder.removeAll()
}
) { isLoggedIn, cacheIdx -> isLoggedIn to cacheIdx }
.onEach { pair ->
val isLoggedIn = pair.first
val cacheIndex = pair.second
Timber.v("isLoggedIn=$isLoggedIn, cacheIndex=$cacheIndex")
if (isLoggedIn) { if (isLoggedIn) {
tryToRestoreLatestSession( tryToRestoreLatestSession(
onSuccess = { switchToLoggedInFlow(it) }, onSuccess = { switchToLoggedInFlow(it, cacheIndex) },
onFailure = { switchToNotLoggedInFlow() } onFailure = { switchToNotLoggedInFlow() }
) )
} else { } else {
@ -102,8 +114,8 @@ class RootFlowNode @AssistedInject constructor(
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
private fun switchToLoggedInFlow(sessionId: SessionId) { private fun switchToLoggedInFlow(sessionId: SessionId, cacheIndex: Int) {
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId)) backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
} }
private fun switchToNotLoggedInFlow() { private fun switchToNotLoggedInFlow() {
@ -163,7 +175,7 @@ class RootFlowNode @AssistedInject constructor(
object NotLoggedInFlow : NavTarget object NotLoggedInFlow : NavTarget
@Parcelize @Parcelize
data class LoggedInFlow(val sessionId: SessionId) : NavTarget data class LoggedInFlow(val sessionId: SessionId, val cacheIndex: Int) : NavTarget
@Parcelize @Parcelize
object BugReport : NavTarget object BugReport : NavTarget
@ -235,8 +247,9 @@ class RootFlowNode @AssistedInject constructor(
} }
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
val cacheIndex = cacheService.cacheIndex().first()
return attachChild { return attachChild {
backstack.newRoot(NavTarget.LoggedInFlow(sessionId)) backstack.newRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
} }
} }
} }

View file

@ -29,9 +29,9 @@ class FakeAnalyticsService(
didAskUserConsent: Boolean = false didAskUserConsent: Boolean = false
): AnalyticsService { ): AnalyticsService {
private var isEnabledFlow = MutableStateFlow(isEnabled) private val isEnabledFlow = MutableStateFlow(isEnabled)
private var didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
var capturedEvents = mutableListOf<VectorAnalyticsEvent>() val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
override fun getAvailableAnalyticsProviders(): List<AnalyticsProvider> = emptyList() override fun getAvailableAnalyticsProviders(): List<AnalyticsProvider> = emptyList()

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeSeenInvitesStore : SeenInvitesStore { class FakeSeenInvitesStore : SeenInvitesStore {
private var existing = MutableStateFlow(emptySet<RoomId>()) private val existing = MutableStateFlow(emptySet<RoomId>())
private var provided: Set<RoomId>? = null private var provided: Set<RoomId>? = null
fun publishRoomIds(invites: Set<RoomId>) { fun publishRoomIds(invites: Set<RoomId>) {

View file

@ -21,7 +21,7 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.androidutils.file.getFileSize
import io.element.android.libraries.androidutils.file.getMimeType import io.element.android.libraries.androidutils.file.getMimeType

View file

@ -25,8 +25,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.features.messages.impl.timeline.util.toHtmlDocument
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType

View file

@ -32,7 +32,7 @@ import io.element.android.features.messages.impl.timeline.factories.virtual.Time
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation
import io.element.android.features.messages.timeline.FakeFileSizeFormatter import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.api
import kotlinx.coroutines.flow.Flow
interface CacheService {
/**
* Returns a flow of the current cache index, can let the app to know when the
* cache has been cleared, for instance to restart the app.
* Will be a flow of Int, starting from 0, and incrementing each time the cache is cleared.
*/
fun cacheIndex(): Flow<Int>
}

View file

@ -32,6 +32,7 @@ anvil {
dependencies { dependencies {
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
@ -39,6 +40,7 @@ dependencies {
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.featureflag.ui) implementation(projects.libraries.featureflag.ui)
implementation(projects.libraries.elementresources) implementation(projects.libraries.elementresources)
implementation(projects.libraries.network)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.features.rageshake.api) implementation(projects.features.rageshake.api)
@ -47,6 +49,7 @@ dependencies {
implementation(projects.features.logout.api) implementation(projects.features.logout.api)
implementation(libs.datetime) implementation(libs.datetime)
implementation(libs.accompanist.placeholder) implementation(libs.accompanist.placeholder)
implementation(libs.coil.compose)
api(projects.features.preferences.api) api(projects.features.preferences.api)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
@ -62,6 +65,7 @@ dependencies {
testImplementation(projects.features.logout.impl) testImplementation(projects.features.logout.impl)
testImplementation(projects.features.analytics.test) testImplementation(projects.features.analytics.test)
testImplementation(projects.features.analytics.impl) testImplementation(projects.features.analytics.impl)
testImplementation(projects.tests.testutils)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
} }

View file

@ -14,25 +14,26 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.messages.impl.timeline.util package io.element.android.features.preferences.impl
import android.content.Context
import android.text.format.Formatter
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject import javax.inject.Inject
interface FileSizeFormatter { @SingleIn(AppScope::class)
/**
* Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
*/
fun format(fileSize: Long): String
}
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class AndroidFileSizeFormatter @Inject constructor(@ApplicationContext private val context: Context) : FileSizeFormatter { class DefaultCacheService @Inject constructor() : CacheService {
override fun format(fileSize: Long): String { private val cacheIndexState = MutableStateFlow(0)
return Formatter.formatShortFileSize(context, fileSize)
override fun cacheIndex(): Flow<Int> {
return cacheIndexState
}
fun incrementCacheIndex() {
cacheIndexState.value++
} }
} }

View file

@ -20,4 +20,5 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface DeveloperSettingsEvents { sealed interface DeveloperSettingsEvents {
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
object ClearCache: DeveloperSettingsEvents
} }

View file

@ -18,12 +18,18 @@ package io.element.android.features.preferences.impl.developer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -36,6 +42,8 @@ import javax.inject.Inject
class DeveloperSettingsPresenter @Inject constructor( class DeveloperSettingsPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase,
) : Presenter<DeveloperSettingsState> { ) : Presenter<DeveloperSettingsState> {
@Composable @Composable
@ -47,6 +55,12 @@ class DeveloperSettingsPresenter @Inject constructor(
val enabledFeatures = remember { val enabledFeatures = remember {
mutableStateMapOf<String, Boolean>() mutableStateMapOf<String, Boolean>()
} }
val cacheSize = remember {
mutableStateOf<Async<String>>(Async.Uninitialized)
}
val clearCacheAction = remember {
mutableStateOf<Async<Unit>>(Async.Uninitialized)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
FeatureFlags.values().forEach { feature -> FeatureFlags.values().forEach { feature ->
features[feature.key] = feature features[feature.key] = feature
@ -55,6 +69,10 @@ class DeveloperSettingsPresenter @Inject constructor(
} }
val featureUiModels = createUiModels(features, enabledFeatures) val featureUiModels = createUiModels(features, enabledFeatures)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
// Compute cache size each time the clear cache action value is changed
LaunchedEffect(clearCacheAction.value) {
computeCacheSize(cacheSize)
}
fun handleEvents(event: DeveloperSettingsEvents) { fun handleEvents(event: DeveloperSettingsEvents) {
when (event) { when (event) {
@ -64,11 +82,14 @@ class DeveloperSettingsPresenter @Inject constructor(
event.feature, event.feature,
event.isEnabled event.isEnabled
) )
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
} }
} }
return DeveloperSettingsState( return DeveloperSettingsState(
features = featureUiModels.toImmutableList(), features = featureUiModels.toImmutableList(),
cacheSize = cacheSize.value,
clearCacheAction = clearCacheAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
@ -103,6 +124,18 @@ class DeveloperSettingsPresenter @Inject constructor(
enabledFeatures[featureUiModel.key] = enabled enabledFeatures[featureUiModel.key] = enabled
} }
} }
private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<Async<String>>) = launch {
suspend {
computeCacheSizeUseCase()
}.execute(cacheSize)
}
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
suspend {
clearCacheUseCase()
}.execute(clearCacheAction)
}
} }

View file

@ -16,10 +16,13 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class DeveloperSettingsState( data class DeveloperSettingsState constructor(
val features: ImmutableList<FeatureUiModel>, val features: ImmutableList<FeatureUiModel>,
val cacheSize: Async<String>,
val clearCacheAction: Async<Unit>,
val eventSink: (DeveloperSettingsEvents) -> Unit val eventSink: (DeveloperSettingsEvents) -> Unit
) )

View file

@ -17,16 +17,20 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> { open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> {
override val values: Sequence<DeveloperSettingsState> override val values: Sequence<DeveloperSettingsState>
get() = sequenceOf( get() = sequenceOf(
aDeveloperSettingsState(), aDeveloperSettingsState(),
aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()),
) )
} }
fun aDeveloperSettingsState() = DeveloperSettingsState( fun aDeveloperSettingsState() = DeveloperSettingsState(
features = aFeatureUiModelList(), features = aFeatureUiModelList(),
cacheSize = Async.Success("1.2 MB"),
clearCacheAction = Async.Uninitialized,
eventSink = {} eventSink = {}
) )

View file

@ -16,11 +16,15 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView import io.element.android.libraries.designsystem.components.preferences.PreferenceView
@ -52,6 +56,20 @@ fun DeveloperSettingsView(
onClick = onOpenShowkase onClick = onOpenShowkase
) )
} }
val cache = state.cacheSize
PreferenceCategory(title = "Cache") {
PreferenceText(
title = "Clear cache",
icon = Icons.Default.Delete,
currentValue = cache.dataOrNull(),
loadingCurrentValue = state.cacheSize.isLoading() || state.clearCacheAction.isLoading(),
onClick = {
if (state.clearCacheAction.isLoading().not()) {
state.eventSink(DeveloperSettingsEvents.ClearCache)
}
}
)
}
} }
} }

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalCoilApi::class)
package io.element.android.features.preferences.impl.tasks
import android.content.Context
import coil.Coil
import coil.annotation.ExperimentalCoilApi
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Provider
interface ClearCacheUseCase {
suspend operator fun invoke()
}
@ContributesBinding(SessionScope::class)
class DefaultClearCacheUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers,
private val defaultCacheIndexProvider: DefaultCacheService,
private val okHttpClient: Provider<OkHttpClient>,
) : ClearCacheUseCase {
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
// Clear Matrix cache
matrixClient.clearCache()
// Clear Coil cache
Coil.imageLoader(context).let {
it.diskCache?.clear()
it.memoryCache?.clear()
}
// Clear OkHttp cache
okHttpClient.get().cache?.delete()
// Clear app cache
context.cacheDir.deleteRecursively()
// Ensure the app is restarted
defaultCacheIndexProvider.incrementCacheIndex()
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.tasks
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.file.getSizeOfFiles
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.withContext
import javax.inject.Inject
interface ComputeCacheSizeUseCase {
suspend operator fun invoke(): String
}
@ContributesBinding(SessionScope::class)
class DefaultComputeCacheSizeUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers,
private val fileSizeFormatter: FileSizeFormatter,
) : ComputeCacheSizeUseCase {
override suspend fun invoke(): String = withContext(coroutineDispatchers.io) {
var cumulativeSize = 0L
cumulativeSize += matrixClient.getCacheSize()
// - 4096 to not include the size fo the folder
cumulativeSize += (context.cacheDir.getSizeOfFiles() - 4096).coerceAtLeast(0)
fileSizeFormatter.format(cumulativeSize)
}
}

View file

@ -20,6 +20,9 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
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.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -29,13 +32,17 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures initial state is correct`() = runTest { fun `present - ensures initial state is correct`() = runTest {
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService() FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.features).isEmpty() assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -43,7 +50,9 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures feature list is loaded`() = runTest { fun `present - ensures feature list is loaded`() = runTest {
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService() FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -58,7 +67,9 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService() FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -74,4 +85,28 @@ class DeveloperSettingsPresenterTest {
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test
fun `present - clear cache`() = runTest {
val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
clearCacheUseCase,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
initialState.eventSink(DeveloperSettingsEvents.ClearCache)
val stateAfterEvent = awaitItem()
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(Async.Loading::class.java)
skipItems(1)
assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java)
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
} }

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.tasks
import io.element.android.tests.testutils.simulateLongTask
class FakeClearCacheUseCase : ClearCacheUseCase {
var executeHasBeenCalled = false
private set
override suspend fun invoke() = simulateLongTask {
executeHasBeenCalled = true
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.tasks
import io.element.android.tests.testutils.simulateLongTask
class FakeComputeCacheSizeUseCase : ComputeCacheSizeUseCase {
override suspend fun invoke() = simulateLongTask {
"O kB"
}
}

View file

@ -52,6 +52,7 @@ import java.io.OutputStreamWriter
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
/** /**
* BugReporter creates and sends the bug reports. * BugReporter creates and sends the bug reports.
@ -62,7 +63,7 @@ class DefaultBugReporter @Inject constructor(
private val screenshotHolder: ScreenshotHolder, private val screenshotHolder: ScreenshotHolder,
private val crashDataStore: CrashDataStore, private val crashDataStore: CrashDataStore,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val okHttpClient: OkHttpClient, private val okHttpClient: Provider<OkHttpClient>,
/* /*
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val versionProvider: VersionProvider, private val versionProvider: VersionProvider,
@ -339,7 +340,7 @@ class DefaultBugReporter @Inject constructor(
// trigger the request // trigger the request
try { try {
mBugReportCall = okHttpClient.newCall(request) mBugReportCall = okHttpClient.get().newCall(request)
response = mBugReportCall!!.execute() response = mBugReportCall!!.execute()
responseCode = response.code responseCode = response.code
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -17,9 +17,11 @@
package io.element.android.libraries.androidutils.file package io.element.android.libraries.androidutils.file
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.Locale
import java.util.UUID import java.util.UUID
fun File.safeDelete() { fun File.safeDelete() {
@ -52,3 +54,99 @@ fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null):
val suffix = extension?.let { ".$extension" } val suffix = extension?.let { ".$extension" }
return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() } return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }
} }
// Implementation should return true in case of success
typealias ActionOnFile = (file: File) -> Boolean
/* ==========================================================================================
* Log
* ========================================================================================== */
fun lsFiles(context: Context) {
Timber.v("Content of cache dir:")
recursiveActionOnFile(context.cacheDir, ::logAction)
Timber.v("Content of files dir:")
recursiveActionOnFile(context.filesDir, ::logAction)
}
private fun logAction(file: File): Boolean {
if (file.isDirectory) {
Timber.v(file.toString())
} else {
Timber.v("$file ${file.length()} bytes")
}
return true
}
/* ==========================================================================================
* Private
* ========================================================================================== */
/**
* Return true in case of success.
*/
private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
if (file.isDirectory) {
file.list()?.forEach {
val result = recursiveActionOnFile(File(file, it), action)
if (!result) {
// Break the loop
return false
}
}
}
return action.invoke(file)
}
/**
* Get the file extension of a fileUri or a filename.
*
* @param fileUri the fileUri (can be a simple filename)
* @return the file extension, in lower case, or null is extension is not available or empty
*/
fun getFileExtension(fileUri: String): String? {
var reducedStr = fileUri
if (reducedStr.isNotEmpty()) {
// Remove fragment
reducedStr = reducedStr.substringBeforeLast('#')
// Remove query
reducedStr = reducedStr.substringBeforeLast('?')
// Remove path
val filename = reducedStr.substringAfterLast('/')
// Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
// See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
if (filename.isNotEmpty()) {
val dotPos = filename.lastIndexOf('.')
if (0 <= dotPos) {
val ext = filename.substring(dotPos + 1)
if (ext.isNotBlank()) {
return ext.lowercase(Locale.ROOT)
}
}
}
}
return null
}
/* ==========================================================================================
* Size
* ========================================================================================== */
@WorkerThread
fun File.getSizeOfFiles(): Long {
return walkTopDown()
.onEnter {
Timber.v("Get size of ${it.absolutePath}")
true
}
.sumOf { it.length() }
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.androidutils.filesize
import android.content.Context
import android.os.Build
import android.text.format.Formatter
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class AndroidFileSizeFormatter @Inject constructor(
@ApplicationContext private val context: Context,
) : FileSizeFormatter {
override fun format(fileSize: Long, useShortFormat: Boolean): String {
// Since Android O, the system considers that 1kB = 1000 bytes instead of 1024 bytes.
// We want to avoid that.
val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
fileSize
} else {
// First convert the size
when {
fileSize < 1024 -> fileSize
fileSize < 1024 * 1024 -> fileSize * 1000 / 1024
fileSize < 1024 * 1024 * 1024 -> fileSize * 1000 / 1024 * 1000 / 1024
else -> fileSize * 1000 / 1024 * 1000 / 1024 * 1000 / 1024
}
}
return if (useShortFormat) {
Formatter.formatShortFileSize(context, normalizedSize)
} else {
Formatter.formatFileSize(context, normalizedSize)
}
}
}

View file

@ -14,12 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.messages.timeline package io.element.android.libraries.androidutils.filesize
import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
class FakeFileSizeFormatter : FileSizeFormatter { class FakeFileSizeFormatter : FileSizeFormatter {
override fun format(fileSize: Long): String { override fun format(fileSize: Long, useShortFormat: Boolean): String {
return "$fileSize Bytes" return "$fileSize Bytes"
} }
} }

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.androidutils.filesize
interface FileSizeFormatter {
/**
* Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
*/
fun format(fileSize: Long, useShortFormat: Boolean = true): String
}

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.designsystem.components.preferences package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.progressSemantics
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
@ -55,7 +55,7 @@ fun PreferenceText(
tintColor: Color? = null, tintColor: Color? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -69,9 +69,10 @@ fun PreferenceText(
.padding(vertical = preferencePaddingVertical) .padding(vertical = preferencePaddingVertical)
) { ) {
PreferenceIcon(icon = icon, tintColor = tintColor) PreferenceIcon(icon = icon, tintColor = tintColor)
Column(modifier = Modifier Column(
.weight(1f) modifier = Modifier
.align(Alignment.CenterVertically) .weight(1f)
.align(Alignment.CenterVertically)
) { ) {
if (title != null) { if (title != null) {
Text( Text(
@ -92,15 +93,24 @@ fun PreferenceText(
} }
} }
if (currentValue != null) { if (currentValue != null) {
Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary) Text(
Spacer(Modifier.width(16.dp)) modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 16.dp),
text = currentValue,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
)
} else if (loadingCurrentValue) { } else if (loadingCurrentValue) {
CircularProgressIndicator(modifier = Modifier CircularProgressIndicator(
.progressSemantics() modifier = Modifier
.size(20.dp), strokeWidth = 2.dp) .progressSemantics()
Spacer(Modifier.width(16.dp)) .padding(horizontal = 16.dp)
.size(20.dp)
.align(Alignment.CenterVertically),
strokeWidth = 2.dp
)
} }
} }
} }
} }
@ -111,9 +121,39 @@ internal fun PreferenceTextPreview() = ElementThemedPreview { ContentToPreview()
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
PreferenceText( Column(
title = "Title", verticalArrangement = Arrangement.spacedBy(2.dp)
subtitle = "Some content", ) {
icon = Icons.Default.BugReport, PreferenceText(
) title = "Title",
icon = Icons.Default.BugReport,
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = Icons.Default.BugReport,
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = Icons.Default.BugReport,
currentValue = "123",
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = Icons.Default.BugReport,
loadingCurrentValue = true,
)
PreferenceText(
title = "Title",
icon = Icons.Default.BugReport,
currentValue = "123",
)
PreferenceText(
title = "Title",
icon = Icons.Default.BugReport,
loadingCurrentValue = true,
)
}
} }

View file

@ -50,6 +50,12 @@ interface MatrixClient : Closeable {
fun sessionVerificationService(): SessionVerificationService fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService fun pushersService(): PushersService
fun notificationService(): NotificationService fun notificationService(): NotificationService
suspend fun getCacheSize(): Long
/**
* Will close the client and delete the cache data.
*/
suspend fun clearCache()
suspend fun logout() suspend fun logout()
suspend fun loadUserDisplayName(): Result<String> suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String?> suspend fun loadUserAvatarURLString(): Result<String?>

View file

@ -32,6 +32,7 @@ dependencies {
// api(projects.libraries.rustsdk) // api(projects.libraries.rustsdk)
implementation(libs.matrix.sdk) implementation(libs.matrix.sdk)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)
api(projects.libraries.matrix.api) api(projects.libraries.matrix.api)
implementation(libs.dagger) implementation(libs.dagger)

View file

@ -18,6 +18,8 @@
package io.element.android.libraries.matrix.impl package io.element.android.libraries.matrix.impl
import io.element.android.libraries.androidutils.file.getSizeOfFiles
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.ProgressCallback
@ -336,15 +338,25 @@ class RustMatrixClient constructor(
client.destroy() client.destroy()
} }
override suspend fun getCacheSize(): Long {
// Do not use client.userId since it can throw if client has been closed (during clear cache)
return baseDirectory.getCacheSize(userID = sessionId.value)
}
override suspend fun clearCache() {
close()
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
}
override suspend fun logout() = withContext(dispatchers.io) { override suspend fun logout() = withContext(dispatchers.io) {
try { try {
client.logout() client.logout()
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Fail to call logout on HS. Still delete local files.") Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
} }
baseDirectory.deleteSessionDirectory(userID = client.userId())
sessionStore.removeSession(client.userId())
close() close()
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
sessionStore.removeSession(sessionId.value)
} }
override suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) { override suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) {
@ -378,11 +390,48 @@ class RustMatrixClient constructor(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
private fun File.deleteSessionDirectory(userID: String): Boolean { private suspend fun File.getCacheSize(
userID: String,
includeCryptoDb: Boolean = false,
): Long = withContext(dispatchers.io) {
// Rust sanitises the user ID replacing invalid characters with an _ // Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_") val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this, sanitisedUserID) val sessionDirectory = File(this@getCacheSize, sanitisedUserID)
return sessionDirectory.deleteRecursively() if (includeCryptoDb) {
sessionDirectory.getSizeOfFiles()
} else {
listOf(
"matrix-sdk-state.sqlite3",
"matrix-sdk-state.sqlite3-shm",
"matrix-sdk-state.sqlite3-wal",
).map { fileName ->
File(sessionDirectory, fileName)
}.sumOf { file ->
file.length()
}
}
}
private suspend fun File.deleteSessionDirectory(
userID: String,
deleteCryptoDb: Boolean = false,
): Boolean = withContext(dispatchers.io) {
// Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this@deleteSessionDirectory, sanitisedUserID)
if (deleteCryptoDb) {
// Delete the folder and all its content
sessionDirectory.deleteRecursively()
} else {
// Delete only the state.db file
sessionDirectory.listFiles().orEmpty()
.filter { it.name.contains("matrix-sdk-state") }
.forEach { file ->
Timber.w("Deleting file ${file.name}...")
file.safeDelete()
}
true
}
} }
} }

View file

@ -102,6 +102,13 @@ class FakeMatrixClient(
override fun stopSync() = Unit override fun stopSync() = Unit
override suspend fun getCacheSize(): Long {
return 0
}
override suspend fun clearCache() {
}
override suspend fun logout() { override suspend fun logout() {
delay(100) delay(100)
logoutFailure?.let { throw it } logoutFailure?.let { throw it }

View file

@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url") val A_OIDC_DATA = OidcDetails(url = "a-url")
class FakeAuthenticationService : MatrixAuthenticationService { class FakeAuthenticationService : MatrixAuthenticationService {
private var homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null) private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var oidcError: Throwable? = null private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null private var oidcCancelError: Throwable? = null
private var loginError: Throwable? = null private var loginError: Throwable? = null

View file

@ -26,16 +26,17 @@ import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
class LoggedInImageLoaderFactory @Inject constructor( class LoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val okHttpClient: OkHttpClient, private val okHttpClient: Provider<OkHttpClient>,
) : ImageLoaderFactory { ) : ImageLoaderFactory {
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader return ImageLoader
.Builder(context) .Builder(context)
.okHttpClient(okHttpClient) .okHttpClient { okHttpClient.get() }
.components { .components {
// Add gif support // Add gif support
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@ -54,12 +55,12 @@ class LoggedInImageLoaderFactory @Inject constructor(
class NotLoggedInImageLoaderFactory @Inject constructor( class NotLoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val okHttpClient: OkHttpClient, private val okHttpClient: Provider<OkHttpClient>,
) : ImageLoaderFactory { ) : ImageLoaderFactory {
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader return ImageLoader
.Builder(context) .Builder(context)
.okHttpClient(okHttpClient) .okHttpClient { okHttpClient.get() }
.build() .build()
} }
} }

View file

@ -43,6 +43,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import timber.log.Timber
import java.util.Locale import java.util.Locale
class RoomListScreen( class RoomListScreen(
@ -106,8 +107,10 @@ class RoomListScreen(
) )
DisposableEffect(Unit) { DisposableEffect(Unit) {
Timber.w("Start sync!")
matrixClient.startSync() matrixClient.startSync()
onDispose { onDispose {
Timber.w("Stop sync!")
matrixClient.stopSync() matrixClient.stopSync()
} }
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5d806cbab0f26fb4f471adb5fbecfc600603a7651c5391501d42b13b23617a4c oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae
size 29301 size 35345

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae
size 35345

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d04eb5d2ebb8b740edcaac00912994e8c164e7b5083595d4085d350af0ca6c33 oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0
size 28482 size 34308

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0
size 34308

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:04e83ead8c23ec9160eb9cfd656aa5e9bf8c302dcf435d00b78aa2eba3243f0d oid sha256:05dc60cbfa8de0e27acecc5d44f7e1841d15f1cdcb749dcf23995aa49d40d924
size 64181 size 64174

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b59df47ab2ad44752eb3441ed117fff517e42c2c08f5e0c6402168223f35daef oid sha256:dc5b5b7a08b61201bde775880eadd40dc51773fe616209c85c143fc703091cef
size 53042 size 53024

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:002fa211918a6d41c993339b30261fb8b564df442756bafe9f8866d2ecf51c81 oid sha256:51f3ee04917b0725bc0a721ac770636b7c273c1ef6f800c5ff3087ea59906396
size 54256 size 54267

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ec598fdc25acf4300e9e44a7b4913e3b9dd881cd27222393047421cd8c361217 oid sha256:05e6a676f742d70c75d1e4ed35667aadbf22e0d796ef165b5265e8889c5bd062
size 54722 size 54715

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:df7c6f78b6a54d1444388aafdd76c8c00ba15be688e2ee10f0582339e74e2499 oid sha256:51fb6dadd1af1bc140877f205d57270ff67a46a3b4c4508ddcdf11e2402f60e4
size 67872 size 67859

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:872c9b5285190577292485c2c0439ceb2259b68740591f1cad7129f322b089b3 oid sha256:1471ebd62c5514c04bb943e83ae1f2bf51edad409da5fb9c8f6613fbae09d333
size 57677 size 57679

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d3bdab2714bd1b232b255fbe0799209da5f40a390384fb21f56bbc1788032129 oid sha256:c164ad24729e25e5b4a1031bb6bb452516e9172b65d867d584efa63e5a095355
size 64512 size 64505

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a9a1a5bb3c04fb601b84a2b1373544e8eba94f688d938a990308a0e788d96445 oid sha256:e651c80d07b153dde903f427b4e41e3475c51b524d877ffe55b265ffaac8c32a
size 61182 size 61197

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b7ee7551b233428723d79b099d7bac4634188a3ac120ff3389b2ed33625dcc94 oid sha256:cb6a8ad69606bac57df9e1c758756eaf09e887875989546c8aa628207926b79e
size 51252 size 51274

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:48e7307ffc1d8f7f7a885e67851314c3b135e74a4268ff5fede59758f91a3358 oid sha256:75eab912dfea0e0eac42742dc131d1630270ac91bf840986be56a755765ee18a
size 51926 size 51917

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:632f982253a6e6f0101fd60a0ef48601cc2c06268ab3d66db084571a5861a52c oid sha256:d0d5bee6b1b4b6069a55c0f297a864979b7c1f1f212bc5bccf8806bd82004abc
size 52417 size 52429

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c49a225d80fe15170e6444271b63324f67099bb733eb1c4b989508a4d0d678ef oid sha256:4c2ce12fd0e6edff9ab4a468ec9f3eab2a206b7c4c51b26fa9fa7bf5a5efdc63
size 64502 size 64472

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d8612af1823d149e503e5d47814ecb171a4b2a4be19a243751c89034a1fdb30d oid sha256:2e000d7940a2fae0a350d508df8b5c95a27f76f60a865cd7372b8a32d89318f6
size 55076 size 55082

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:40ecdf8314109687437223860b5e28847d0cb8ba7d546829de5f061640e58315 oid sha256:86cca8af9ab505cd9cd1ae4a941d5053f2d3155eb033ed1121e7d15f92a6cb43
size 61500 size 61509

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1fc465aa6658ace0327804f04e329cae12bf358daca27e48a8ae6bd516752a9c oid sha256:197b1b5fa33ba31f4e47f70b12e4b6eaf7fb3ea30368e96b7dec08f37bdeb62c
size 12843 size 28185