Display cache size in the developer settings (#643)
This commit is contained in:
parent
bf80c251aa
commit
5d8adb25ae
12 changed files with 283 additions and 12 deletions
|
|
@ -32,6 +32,7 @@ anvil {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||||
|
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.execute
|
||||||
|
|
@ -41,6 +42,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class DeveloperSettingsPresenter @Inject constructor(
|
class DeveloperSettingsPresenter @Inject constructor(
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
||||||
private val clearCacheUseCase: ClearCacheUseCase,
|
private val clearCacheUseCase: ClearCacheUseCase,
|
||||||
) : Presenter<DeveloperSettingsState> {
|
) : Presenter<DeveloperSettingsState> {
|
||||||
|
|
||||||
|
|
@ -53,6 +55,9 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
val enabledFeatures = remember {
|
val enabledFeatures = remember {
|
||||||
mutableStateMapOf<String, Boolean>()
|
mutableStateMapOf<String, Boolean>()
|
||||||
}
|
}
|
||||||
|
val cacheSize = remember {
|
||||||
|
mutableStateOf<Async<Long>>(Async.Uninitialized)
|
||||||
|
}
|
||||||
val clearCacheAction = remember {
|
val clearCacheAction = remember {
|
||||||
mutableStateOf<Async<Unit>>(Async.Uninitialized)
|
mutableStateOf<Async<Unit>>(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +69,10 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
val featureUiModels = createUiModels(features, enabledFeatures)
|
val featureUiModels = createUiModels(features, enabledFeatures)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
// Compute cache size each time the clear cache action value is changed
|
||||||
|
LaunchedEffect(clearCacheAction.value) {
|
||||||
|
computeCacheSize(cacheSize)
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: DeveloperSettingsEvents) {
|
fun handleEvents(event: DeveloperSettingsEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -79,6 +88,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
|
|
||||||
return DeveloperSettingsState(
|
return DeveloperSettingsState(
|
||||||
features = featureUiModels.toImmutableList(),
|
features = featureUiModels.toImmutableList(),
|
||||||
|
cacheSizeInBytes = cacheSize.value,
|
||||||
clearCacheAction = clearCacheAction.value,
|
clearCacheAction = clearCacheAction.value,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
@ -115,6 +125,12 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<Async<Long>>) = launch {
|
||||||
|
suspend {
|
||||||
|
computeCacheSizeUseCase.execute()
|
||||||
|
}.execute(cacheSize)
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
|
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
clearCacheUseCase.execute()
|
clearCacheUseCase.execute()
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class DeveloperSettingsState constructor(
|
data class DeveloperSettingsState constructor(
|
||||||
val features: ImmutableList<FeatureUiModel>,
|
val features: ImmutableList<FeatureUiModel>,
|
||||||
|
val cacheSizeInBytes: Async<Long>,
|
||||||
val clearCacheAction: Async<Unit>,
|
val clearCacheAction: Async<Unit>,
|
||||||
val eventSink: (DeveloperSettingsEvents) -> Unit
|
val eventSink: (DeveloperSettingsEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
||||||
|
|
||||||
fun aDeveloperSettingsState() = DeveloperSettingsState(
|
fun aDeveloperSettingsState() = DeveloperSettingsState(
|
||||||
features = aFeatureUiModelList(),
|
features = aFeatureUiModelList(),
|
||||||
|
cacheSizeInBytes = Async.Success(0L),
|
||||||
clearCacheAction = Async.Uninitialized,
|
clearCacheAction = Async.Uninitialized,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.isLoading
|
import io.element.android.libraries.architecture.isLoading
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
|
|
@ -55,11 +56,15 @@ fun DeveloperSettingsView(
|
||||||
onClick = onOpenShowkase
|
onClick = onOpenShowkase
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val cache = state.cacheSizeInBytes
|
||||||
PreferenceCategory(title = "Cache") {
|
PreferenceCategory(title = "Cache") {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = "Clear cache",
|
title = "Clear cache",
|
||||||
icon = Icons.Default.Delete,
|
icon = Icons.Default.Delete,
|
||||||
loadingCurrentValue = state.clearCacheAction.isLoading(),
|
currentValue = if (cache is Async.Success) {
|
||||||
|
"${cache.state} bytes"
|
||||||
|
} else null,
|
||||||
|
loadingCurrentValue = state.cacheSizeInBytes.isLoading() || state.clearCacheAction.isLoading(),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (state.clearCacheAction.isLoading().not()) {
|
if (state.clearCacheAction.isLoading().not()) {
|
||||||
state.eventSink(DeveloperSettingsEvents.ClearCache)
|
state.eventSink(DeveloperSettingsEvents.ClearCache)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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.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 fun execute(): Long
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultComputeCacheSizeUseCase @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : ComputeCacheSizeUseCase {
|
||||||
|
override suspend fun execute(): Long = withContext(coroutineDispatchers.io) {
|
||||||
|
var cumulativeSize = 0L
|
||||||
|
cumulativeSize += matrixClient.getCacheSize()
|
||||||
|
cumulativeSize += context.cacheDir.getSizeOfFiles()
|
||||||
|
cumulativeSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||||
|
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
|
@ -32,7 +33,8 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - ensures initial state is correct`() = runTest {
|
fun `present - ensures initial state is correct`() = runTest {
|
||||||
val presenter = DeveloperSettingsPresenter(
|
val presenter = DeveloperSettingsPresenter(
|
||||||
FakeFeatureFlagService(),
|
FakeFeatureFlagService(),
|
||||||
FakeClearCacheUseCase()
|
FakeComputeCacheSizeUseCase(),
|
||||||
|
FakeClearCacheUseCase(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -40,6 +42,7 @@ class DeveloperSettingsPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.features).isEmpty()
|
assertThat(initialState.features).isEmpty()
|
||||||
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(initialState.cacheSizeInBytes).isEqualTo(Async.Uninitialized)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +51,8 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - ensures feature list is loaded`() = runTest {
|
fun `present - ensures feature list is loaded`() = runTest {
|
||||||
val presenter = DeveloperSettingsPresenter(
|
val presenter = DeveloperSettingsPresenter(
|
||||||
FakeFeatureFlagService(),
|
FakeFeatureFlagService(),
|
||||||
FakeClearCacheUseCase()
|
FakeComputeCacheSizeUseCase(),
|
||||||
|
FakeClearCacheUseCase(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -64,7 +68,8 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
|
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
|
||||||
val presenter = DeveloperSettingsPresenter(
|
val presenter = DeveloperSettingsPresenter(
|
||||||
FakeFeatureFlagService(),
|
FakeFeatureFlagService(),
|
||||||
FakeClearCacheUseCase()
|
FakeComputeCacheSizeUseCase(),
|
||||||
|
FakeClearCacheUseCase(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -86,7 +91,8 @@ class DeveloperSettingsPresenterTest {
|
||||||
val clearCacheUseCase = FakeClearCacheUseCase()
|
val clearCacheUseCase = FakeClearCacheUseCase()
|
||||||
val presenter = DeveloperSettingsPresenter(
|
val presenter = DeveloperSettingsPresenter(
|
||||||
FakeFeatureFlagService(),
|
FakeFeatureFlagService(),
|
||||||
clearCacheUseCase
|
FakeComputeCacheSizeUseCase(),
|
||||||
|
clearCacheUseCase,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -97,8 +103,10 @@ class DeveloperSettingsPresenterTest {
|
||||||
initialState.eventSink(DeveloperSettingsEvents.ClearCache)
|
initialState.eventSink(DeveloperSettingsEvents.ClearCache)
|
||||||
val stateAfterEvent = awaitItem()
|
val stateAfterEvent = awaitItem()
|
||||||
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(Async.Loading::class.java)
|
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(Async.Loading::class.java)
|
||||||
|
skipItems(1)
|
||||||
assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java)
|
assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java)
|
||||||
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
|
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 execute() = simulateLongTask {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* 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.file
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
// Implementation should return true in case of success
|
||||||
|
typealias ActionOnFile = (file: File) -> Boolean
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Delete
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
fun deleteAllFiles(root: File) {
|
||||||
|
Timber.v("Delete ${root.absolutePath}")
|
||||||
|
recursiveActionOnFile(root, ::deleteAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteAction(file: File): Boolean {
|
||||||
|
if (file.exists()) {
|
||||||
|
Timber.v("deleteFile: $file")
|
||||||
|
return file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* 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() }
|
||||||
|
}
|
||||||
|
|
@ -50,6 +50,7 @@ interface MatrixClient : Closeable {
|
||||||
fun sessionVerificationService(): SessionVerificationService
|
fun sessionVerificationService(): SessionVerificationService
|
||||||
fun pushersService(): PushersService
|
fun pushersService(): PushersService
|
||||||
fun notificationService(): NotificationService
|
fun notificationService(): NotificationService
|
||||||
|
suspend fun getCacheSize(): Long
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun logout()
|
suspend fun logout()
|
||||||
suspend fun loadUserDisplayName(): Result<String>
|
suspend fun loadUserDisplayName(): Result<String>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl
|
package io.element.android.libraries.matrix.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.androidutils.file.getSizeOfFiles
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
import io.element.android.libraries.androidutils.file.safeDelete
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -337,8 +338,12 @@ class RustMatrixClient constructor(
|
||||||
client.destroy()
|
client.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getCacheSize(): Long {
|
||||||
|
return baseDirectory.getCacheSize(userID = client.userId())
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clearCache() {
|
override suspend fun clearCache() {
|
||||||
baseDirectory.deleteSessionDirectory(userID = client.userId(), deleteCryptoDb = false)
|
baseDirectory.deleteSessionDirectory(userID = client.userId())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() = withContext(dispatchers.io) {
|
override suspend fun logout() = withContext(dispatchers.io) {
|
||||||
|
|
@ -347,7 +352,7 @@ class RustMatrixClient constructor(
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||||
}
|
}
|
||||||
baseDirectory.deleteSessionDirectory(userID = client.userId())
|
baseDirectory.deleteSessionDirectory(userID = client.userId(), deleteCryptoDb = true)
|
||||||
sessionStore.removeSession(client.userId())
|
sessionStore.removeSession(client.userId())
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
@ -383,14 +388,36 @@ class RustMatrixClient constructor(
|
||||||
|
|
||||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||||
|
|
||||||
private fun File.deleteSessionDirectory(
|
private suspend fun File.getCacheSize(
|
||||||
userID: String,
|
userID: String,
|
||||||
deleteCryptoDb: Boolean = false,
|
includeCryptoDb: Boolean = false,
|
||||||
): Boolean {
|
): Long = withContext(dispatchers.io) {
|
||||||
// Rust sanitises the user ID replacing invalid characters with an _
|
// Rust sanitises the user ID replacing invalid characters with an _
|
||||||
val sanitisedUserID = userID.replace(":", "_")
|
val sanitisedUserID = userID.replace(":", "_")
|
||||||
val sessionDirectory = File(this, sanitisedUserID)
|
val sessionDirectory = File(this@getCacheSize, sanitisedUserID)
|
||||||
return if (deleteCryptoDb) {
|
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
|
// Delete the folder and all its content
|
||||||
sessionDirectory.deleteRecursively()
|
sessionDirectory.deleteRecursively()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,10 @@ class FakeMatrixClient(
|
||||||
|
|
||||||
override fun stopSync() = Unit
|
override fun stopSync() = Unit
|
||||||
|
|
||||||
|
override suspend fun getCacheSize(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clearCache() {
|
override suspend fun clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue