First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.cachecleaner.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(libs.androidx.startup)
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.cachecleaner.api
interface CacheCleaner {
/**
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
*
* Will fail silently in case of errors while deleting the files.
*/
fun clearCache()
}

View File

@@ -0,0 +1,28 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.cachecleaner.impl"
}
setupDependencyInjection()
dependencies {
api(projects.features.cachecleaner.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
testCommonDependencies(libs)
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.cachecleaner.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.cachecleaner.api.CacheCleaner
@ContributesTo(AppScope::class)
interface CacheCleanerBindings {
fun cacheCleaner(): CacheCleaner
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.cachecleaner.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.cachecleaner.api.CacheCleaner
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
/**
* Default implementation of [CacheCleaner].
*/
@ContributesBinding(AppScope::class)
class DefaultCacheCleaner(
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
@CacheDirectory private val cacheDir: File,
) : CacheCleaner {
companion object {
val SUBDIRS_TO_CLEANUP = listOf("temp/media", "temp/voice")
}
override fun clearCache() {
coroutineScope.launch(dispatchers.io) {
runCatchingExceptions {
SUBDIRS_TO_CLEANUP.forEach {
File(cacheDir.path, it).apply {
if (exists()) {
if (!deleteRecursively()) error("Failed to delete recursively cache directory $this")
}
if (!mkdirs()) error("Failed to create cache directory $this")
}
}
}.onFailure {
Timber.e(it, "Failed to clear cache")
}
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.cachecleaner.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
class DefaultCacheCleanerTest {
@get:Rule
val temporaryFolder = TemporaryFolder()
@Test
fun `calling clearCache actually removes file in the SUBDIRS_TO_CLEANUP list`() = runTest {
// Create temp subdirs and fill with 2 files each
DefaultCacheCleaner.SUBDIRS_TO_CLEANUP.forEach {
File(temporaryFolder.root, it).apply {
mkdirs()
File(this, "temp1").createNewFile()
File(this, "temp2").createNewFile()
}
}
// Clear cache
aCacheCleaner().clearCache()
// Check the files are gone but the sub dirs are not.
DefaultCacheCleaner.SUBDIRS_TO_CLEANUP.forEach {
File(temporaryFolder.root, it).apply {
assertThat(exists()).isTrue()
assertThat(isDirectory).isTrue()
assertThat(listFiles()).isEmpty()
}
}
}
@Test
fun `clear cache fails silently`() = runTest {
// Set cache dir as unreadable, unwritable and unexecutable so that the deletion fails.
check(temporaryFolder.root.setReadable(false))
check(temporaryFolder.root.setWritable(false))
check(temporaryFolder.root.setExecutable(false))
aCacheCleaner().clearCache()
}
private fun TestScope.aCacheCleaner() = DefaultCacheCleaner(
coroutineScope = this,
dispatchers = this.testCoroutineDispatchers(true),
cacheDir = temporaryFolder.root,
)
}