Merge pull request #648 from vector-im/feature/bma/clearCache
Developer option to clear cache
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user