Merge pull request #648 from vector-im/feature/bma/clearCache
Developer option to clear cache
This commit is contained in:
commit
59061f9e37
53 changed files with 690 additions and 106 deletions
7
.idea/dictionaries/bmarty.xml
generated
7
.idea/dictionaries/bmarty.xml
generated
|
|
@ -1,7 +0,0 @@
|
|||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="bmarty">
|
||||
<words>
|
||||
<w>homeserver</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
2
.idea/dictionaries/shared.xml
generated
2
.idea/dictionaries/shared.xml
generated
|
|
@ -2,8 +2,10 @@
|
|||
<dictionary name="shared">
|
||||
<words>
|
||||
<w>backstack</w>
|
||||
<w>homeserver</w>
|
||||
<w>kover</w>
|
||||
<w>onboarding</w>
|
||||
<w>showkase</w>
|
||||
<w>textfields</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import io.element.android.appnav.root.RootPresenter
|
|||
import io.element.android.appnav.root.RootView
|
||||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
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.libraries.architecture.BackstackNode
|
||||
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.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val cacheService: CacheService,
|
||||
private val matrixClientsHolder: MatrixClientsHolder,
|
||||
private val presenter: RootPresenter,
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
|
|
@ -88,11 +92,19 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private fun observeLoggedInState() {
|
||||
authenticationService.isLoggedIn()
|
||||
.distinctUntilChanged()
|
||||
.onEach { isLoggedIn ->
|
||||
Timber.v("isLoggedIn=$isLoggedIn")
|
||||
.combine(
|
||||
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) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { switchToLoggedInFlow(it) },
|
||||
onSuccess = { switchToLoggedInFlow(it, cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow() }
|
||||
)
|
||||
} else {
|
||||
|
|
@ -102,8 +114,8 @@ class RootFlowNode @AssistedInject constructor(
|
|||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun switchToLoggedInFlow(sessionId: SessionId) {
|
||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId))
|
||||
private fun switchToLoggedInFlow(sessionId: SessionId, cacheIndex: Int) {
|
||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
|
||||
}
|
||||
|
||||
private fun switchToNotLoggedInFlow() {
|
||||
|
|
@ -163,7 +175,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
object NotLoggedInFlow : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoggedInFlow(val sessionId: SessionId) : NavTarget
|
||||
data class LoggedInFlow(val sessionId: SessionId, val cacheIndex: Int) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object BugReport : NavTarget
|
||||
|
|
@ -235,8 +247,9 @@ class RootFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||
val cacheIndex = cacheService.cacheIndex().first()
|
||||
return attachChild {
|
||||
backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
|
||||
backstack.newRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ class FakeAnalyticsService(
|
|||
didAskUserConsent: Boolean = false
|
||||
): AnalyticsService {
|
||||
|
||||
private var isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private var didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
var capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||
private val isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||
|
||||
override fun getAvailableAnalyticsProviders(): List<AnalyticsProvider> = emptyList()
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
|
||||
class FakeSeenInvitesStore : SeenInvitesStore {
|
||||
|
||||
private var existing = MutableStateFlow(emptySet<RoomId>())
|
||||
private val existing = MutableStateFlow(emptySet<RoomId>())
|
||||
private var provided: Set<RoomId>? = null
|
||||
|
||||
fun publishRoomIds(invites: Set<RoomId>) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import android.net.Uri
|
|||
import androidx.core.net.toUri
|
||||
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.FileSizeFormatter
|
||||
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
|
||||
import io.element.android.libraries.androidutils.file.getFileName
|
||||
import io.element.android.libraries.androidutils.file.getFileSize
|
||||
import io.element.android.libraries.androidutils.file.getMimeType
|
||||
|
|
|
|||
|
|
@ -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.TimelineItemVideoContent
|
||||
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.libraries.androidutils.filesize.FileSizeFormatter
|
||||
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.FileMessageType
|
||||
|
|
|
|||
|
|
@ -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.groups.TimelineItemGrouper
|
||||
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.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ anvil {
|
|||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
|
@ -39,6 +40,7 @@ dependencies {
|
|||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.featureflag.ui)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.rageshake.api)
|
||||
|
|
@ -47,6 +49,7 @@ dependencies {
|
|||
implementation(projects.features.logout.api)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
implementation(libs.coil.compose)
|
||||
api(projects.features.preferences.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
|
|
@ -62,6 +65,7 @@ dependencies {
|
|||
testImplementation(projects.features.logout.impl)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.features.analytics.impl)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,25 +14,26 @@
|
|||
* 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 io.element.android.features.preferences.api.CacheService
|
||||
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
|
||||
|
||||
interface FileSizeFormatter {
|
||||
/**
|
||||
* Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
|
||||
*/
|
||||
fun format(fileSize: Long): String
|
||||
}
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidFileSizeFormatter @Inject constructor(@ApplicationContext private val context: Context) : FileSizeFormatter {
|
||||
override fun format(fileSize: Long): String {
|
||||
return Formatter.formatShortFileSize(context, fileSize)
|
||||
class DefaultCacheService @Inject constructor() : CacheService {
|
||||
private val cacheIndexState = MutableStateFlow(0)
|
||||
|
||||
override fun cacheIndex(): Flow<Int> {
|
||||
return cacheIndexState
|
||||
}
|
||||
|
||||
fun incrementCacheIndex() {
|
||||
cacheIndexState.value++
|
||||
}
|
||||
}
|
||||
|
|
@ -20,4 +20,5 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
|||
|
||||
sealed interface DeveloperSettingsEvents {
|
||||
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
|
||||
object ClearCache: DeveloperSettingsEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,18 @@ package io.element.android.features.preferences.impl.developer
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.execute
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.featureflag.api.Feature
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
|
|
@ -36,6 +42,8 @@ import javax.inject.Inject
|
|||
|
||||
class DeveloperSettingsPresenter @Inject constructor(
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
||||
private val clearCacheUseCase: ClearCacheUseCase,
|
||||
) : Presenter<DeveloperSettingsState> {
|
||||
|
||||
@Composable
|
||||
|
|
@ -47,6 +55,12 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
val enabledFeatures = remember {
|
||||
mutableStateMapOf<String, Boolean>()
|
||||
}
|
||||
val cacheSize = remember {
|
||||
mutableStateOf<Async<String>>(Async.Uninitialized)
|
||||
}
|
||||
val clearCacheAction = remember {
|
||||
mutableStateOf<Async<Unit>>(Async.Uninitialized)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
FeatureFlags.values().forEach { feature ->
|
||||
features[feature.key] = feature
|
||||
|
|
@ -55,6 +69,10 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
}
|
||||
val featureUiModels = createUiModels(features, enabledFeatures)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
// Compute cache size each time the clear cache action value is changed
|
||||
LaunchedEffect(clearCacheAction.value) {
|
||||
computeCacheSize(cacheSize)
|
||||
}
|
||||
|
||||
fun handleEvents(event: DeveloperSettingsEvents) {
|
||||
when (event) {
|
||||
|
|
@ -64,11 +82,14 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
event.feature,
|
||||
event.isEnabled
|
||||
)
|
||||
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
||||
}
|
||||
}
|
||||
|
||||
return DeveloperSettingsState(
|
||||
features = featureUiModels.toImmutableList(),
|
||||
cacheSize = cacheSize.value,
|
||||
clearCacheAction = clearCacheAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
@ -103,6 +124,18 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@
|
|||
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class DeveloperSettingsState(
|
||||
data class DeveloperSettingsState constructor(
|
||||
val features: ImmutableList<FeatureUiModel>,
|
||||
val cacheSize: Async<String>,
|
||||
val clearCacheAction: Async<Unit>,
|
||||
val eventSink: (DeveloperSettingsEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,16 +17,20 @@
|
|||
package io.element.android.features.preferences.impl.developer
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
|
||||
|
||||
open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> {
|
||||
override val values: Sequence<DeveloperSettingsState>
|
||||
get() = sequenceOf(
|
||||
aDeveloperSettingsState(),
|
||||
aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()),
|
||||
)
|
||||
}
|
||||
|
||||
fun aDeveloperSettingsState() = DeveloperSettingsState(
|
||||
features = aFeatureUiModelList(),
|
||||
cacheSize = Async.Success("1.2 MB"),
|
||||
clearCacheAction = Async.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@
|
|||
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.PreferenceText
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
|
||||
|
|
@ -52,6 +56,20 @@ fun DeveloperSettingsView(
|
|||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,9 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
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.test.FakeFeatureFlagService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -29,13 +32,17 @@ class DeveloperSettingsPresenterTest {
|
|||
@Test
|
||||
fun `present - ensures initial state is correct`() = runTest {
|
||||
val presenter = DeveloperSettingsPresenter(
|
||||
FakeFeatureFlagService()
|
||||
FakeFeatureFlagService(),
|
||||
FakeComputeCacheSizeUseCase(),
|
||||
FakeClearCacheUseCase(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.features).isEmpty()
|
||||
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +50,9 @@ class DeveloperSettingsPresenterTest {
|
|||
@Test
|
||||
fun `present - ensures feature list is loaded`() = runTest {
|
||||
val presenter = DeveloperSettingsPresenter(
|
||||
FakeFeatureFlagService()
|
||||
FakeFeatureFlagService(),
|
||||
FakeComputeCacheSizeUseCase(),
|
||||
FakeClearCacheUseCase(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -58,7 +67,9 @@ class DeveloperSettingsPresenterTest {
|
|||
@Test
|
||||
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
|
||||
val presenter = DeveloperSettingsPresenter(
|
||||
FakeFeatureFlagService()
|
||||
FakeFeatureFlagService(),
|
||||
FakeComputeCacheSizeUseCase(),
|
||||
FakeClearCacheUseCase(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -74,4 +85,28 @@ class DeveloperSettingsPresenterTest {
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ import java.io.OutputStreamWriter
|
|||
import java.net.HttpURLConnection
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* BugReporter creates and sends the bug reports.
|
||||
|
|
@ -62,7 +63,7 @@ class DefaultBugReporter @Inject constructor(
|
|||
private val screenshotHolder: ScreenshotHolder,
|
||||
private val crashDataStore: CrashDataStore,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
/*
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val versionProvider: VersionProvider,
|
||||
|
|
@ -339,7 +340,7 @@ class DefaultBugReporter @Inject constructor(
|
|||
|
||||
// trigger the request
|
||||
try {
|
||||
mBugReportCall = okHttpClient.newCall(request)
|
||||
mBugReportCall = okHttpClient.get().newCall(request)
|
||||
response = mBugReportCall!!.execute()
|
||||
responseCode = response.code
|
||||
} catch (e: Exception) {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@
|
|||
package io.element.android.libraries.androidutils.file
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
fun File.safeDelete() {
|
||||
|
|
@ -52,3 +54,99 @@ fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null):
|
|||
val suffix = extension?.let { ".$extension" }
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,12 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
|
||||
package io.element.android.libraries.androidutils.filesize
|
||||
|
||||
class FakeFileSizeFormatter : FileSizeFormatter {
|
||||
override fun format(fileSize: Long): String {
|
||||
override fun format(fileSize: Long, useShortFormat: Boolean): String {
|
||||
return "$fileSize Bytes"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.designsystem.components.preferences
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
|
|
@ -55,7 +55,7 @@ fun PreferenceText(
|
|||
tintColor: Color? = null,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
|
||||
val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -69,9 +69,10 @@ fun PreferenceText(
|
|||
.padding(vertical = preferencePaddingVertical)
|
||||
) {
|
||||
PreferenceIcon(icon = icon, tintColor = tintColor)
|
||||
Column(modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
|
|
@ -92,15 +93,24 @@ fun PreferenceText(
|
|||
}
|
||||
}
|
||||
if (currentValue != null) {
|
||||
Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 16.dp),
|
||||
text = currentValue,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
} else if (loadingCurrentValue) {
|
||||
CircularProgressIndicator(modifier = Modifier
|
||||
.progressSemantics()
|
||||
.size(20.dp), strokeWidth = 2.dp)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.progressSemantics()
|
||||
.padding(horizontal = 16.dp)
|
||||
.size(20.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,9 +121,39 @@ internal fun PreferenceTextPreview() = ElementThemedPreview { ContentToPreview()
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
PreferenceText(
|
||||
title = "Title",
|
||||
subtitle = "Some content",
|
||||
icon = Icons.Default.BugReport,
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ interface MatrixClient : Closeable {
|
|||
fun sessionVerificationService(): SessionVerificationService
|
||||
fun pushersService(): PushersService
|
||||
fun notificationService(): NotificationService
|
||||
suspend fun getCacheSize(): Long
|
||||
|
||||
/**
|
||||
* Will close the client and delete the cache data.
|
||||
*/
|
||||
suspend fun clearCache()
|
||||
suspend fun logout()
|
||||
suspend fun loadUserDisplayName(): Result<String>
|
||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
// api(projects.libraries.rustsdk)
|
||||
implementation(libs.matrix.sdk)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.matrix.api)
|
||||
implementation(libs.dagger)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
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.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
|
|
@ -336,15 +338,25 @@ class RustMatrixClient constructor(
|
|||
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) {
|
||||
try {
|
||||
client.logout()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||
}
|
||||
baseDirectory.deleteSessionDirectory(userID = client.userId())
|
||||
sessionStore.removeSession(client.userId())
|
||||
close()
|
||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
}
|
||||
|
||||
override suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) {
|
||||
|
|
@ -378,11 +390,48 @@ class RustMatrixClient constructor(
|
|||
|
||||
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 _
|
||||
val sanitisedUserID = userID.replace(":", "_")
|
||||
val sessionDirectory = File(this, sanitisedUserID)
|
||||
return sessionDirectory.deleteRecursively()
|
||||
val sessionDirectory = File(this@getCacheSize, sanitisedUserID)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,13 @@ class FakeMatrixClient(
|
|||
|
||||
override fun stopSync() = Unit
|
||||
|
||||
override suspend fun getCacheSize(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override suspend fun clearCache() {
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
delay(100)
|
||||
logoutFailure?.let { throw it }
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
val A_OIDC_DATA = OidcDetails(url = "a-url")
|
||||
|
||||
class FakeAuthenticationService : MatrixAuthenticationService {
|
||||
private var homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
private var oidcError: Throwable? = null
|
||||
private var oidcCancelError: Throwable? = null
|
||||
private var loginError: Throwable? = null
|
||||
|
|
|
|||
|
|
@ -26,16 +26,17 @@ import io.element.android.libraries.di.ApplicationContext
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class LoggedInImageLoaderFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
) : ImageLoaderFactory {
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader
|
||||
.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.okHttpClient { okHttpClient.get() }
|
||||
.components {
|
||||
// Add gif support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
|
@ -54,12 +55,12 @@ class LoggedInImageLoaderFactory @Inject constructor(
|
|||
|
||||
class NotLoggedInImageLoaderFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
) : ImageLoaderFactory {
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader
|
||||
.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.okHttpClient { okHttpClient.get() }
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
class RoomListScreen(
|
||||
|
|
@ -106,8 +107,10 @@ class RoomListScreen(
|
|||
)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
Timber.w("Start sync!")
|
||||
matrixClient.startSync()
|
||||
onDispose {
|
||||
Timber.w("Stop sync!")
|
||||
matrixClient.stopSync()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d806cbab0f26fb4f471adb5fbecfc600603a7651c5391501d42b13b23617a4c
|
||||
size 29301
|
||||
oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae
|
||||
size 35345
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae
|
||||
size 35345
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d04eb5d2ebb8b740edcaac00912994e8c164e7b5083595d4085d350af0ca6c33
|
||||
size 28482
|
||||
oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0
|
||||
size 34308
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0
|
||||
size 34308
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04e83ead8c23ec9160eb9cfd656aa5e9bf8c302dcf435d00b78aa2eba3243f0d
|
||||
size 64181
|
||||
oid sha256:05dc60cbfa8de0e27acecc5d44f7e1841d15f1cdcb749dcf23995aa49d40d924
|
||||
size 64174
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b59df47ab2ad44752eb3441ed117fff517e42c2c08f5e0c6402168223f35daef
|
||||
size 53042
|
||||
oid sha256:dc5b5b7a08b61201bde775880eadd40dc51773fe616209c85c143fc703091cef
|
||||
size 53024
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:002fa211918a6d41c993339b30261fb8b564df442756bafe9f8866d2ecf51c81
|
||||
size 54256
|
||||
oid sha256:51f3ee04917b0725bc0a721ac770636b7c273c1ef6f800c5ff3087ea59906396
|
||||
size 54267
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ec598fdc25acf4300e9e44a7b4913e3b9dd881cd27222393047421cd8c361217
|
||||
size 54722
|
||||
oid sha256:05e6a676f742d70c75d1e4ed35667aadbf22e0d796ef165b5265e8889c5bd062
|
||||
size 54715
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df7c6f78b6a54d1444388aafdd76c8c00ba15be688e2ee10f0582339e74e2499
|
||||
size 67872
|
||||
oid sha256:51fb6dadd1af1bc140877f205d57270ff67a46a3b4c4508ddcdf11e2402f60e4
|
||||
size 67859
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:872c9b5285190577292485c2c0439ceb2259b68740591f1cad7129f322b089b3
|
||||
size 57677
|
||||
oid sha256:1471ebd62c5514c04bb943e83ae1f2bf51edad409da5fb9c8f6613fbae09d333
|
||||
size 57679
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3bdab2714bd1b232b255fbe0799209da5f40a390384fb21f56bbc1788032129
|
||||
size 64512
|
||||
oid sha256:c164ad24729e25e5b4a1031bb6bb452516e9172b65d867d584efa63e5a095355
|
||||
size 64505
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9a1a5bb3c04fb601b84a2b1373544e8eba94f688d938a990308a0e788d96445
|
||||
size 61182
|
||||
oid sha256:e651c80d07b153dde903f427b4e41e3475c51b524d877ffe55b265ffaac8c32a
|
||||
size 61197
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7ee7551b233428723d79b099d7bac4634188a3ac120ff3389b2ed33625dcc94
|
||||
size 51252
|
||||
oid sha256:cb6a8ad69606bac57df9e1c758756eaf09e887875989546c8aa628207926b79e
|
||||
size 51274
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:48e7307ffc1d8f7f7a885e67851314c3b135e74a4268ff5fede59758f91a3358
|
||||
size 51926
|
||||
oid sha256:75eab912dfea0e0eac42742dc131d1630270ac91bf840986be56a755765ee18a
|
||||
size 51917
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:632f982253a6e6f0101fd60a0ef48601cc2c06268ab3d66db084571a5861a52c
|
||||
size 52417
|
||||
oid sha256:d0d5bee6b1b4b6069a55c0f297a864979b7c1f1f212bc5bccf8806bd82004abc
|
||||
size 52429
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c49a225d80fe15170e6444271b63324f67099bb733eb1c4b989508a4d0d678ef
|
||||
size 64502
|
||||
oid sha256:4c2ce12fd0e6edff9ab4a468ec9f3eab2a206b7c4c51b26fa9fa7bf5a5efdc63
|
||||
size 64472
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8612af1823d149e503e5d47814ecb171a4b2a4be19a243751c89034a1fdb30d
|
||||
size 55076
|
||||
oid sha256:2e000d7940a2fae0a350d508df8b5c95a27f76f60a865cd7372b8a32d89318f6
|
||||
size 55082
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:40ecdf8314109687437223860b5e28847d0cb8ba7d546829de5f061640e58315
|
||||
size 61500
|
||||
oid sha256:86cca8af9ab505cd9cd1ae4a941d5053f2d3155eb033ed1121e7d15f92a6cb43
|
||||
size 61509
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1fc465aa6658ace0327804f04e329cae12bf358daca27e48a8ae6bd516752a9c
|
||||
size 12843
|
||||
oid sha256:197b1b5fa33ba31f4e47f70b12e4b6eaf7fb3ea30368e96b7dec08f37bdeb62c
|
||||
size 28185
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue