Delete media caches on startup (#1807)
Clear media caches on app startup
This commit is contained in:
parent
abea28a5d2
commit
af8f5b805c
9 changed files with 282 additions and 1 deletions
|
|
@ -18,6 +18,7 @@ package io.element.android.x
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.startup.AppInitializer
|
import androidx.startup.AppInitializer
|
||||||
|
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
|
||||||
import io.element.android.libraries.di.DaggerComponentOwner
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
import io.element.android.x.di.AppComponent
|
import io.element.android.x.di.AppComponent
|
||||||
import io.element.android.x.di.DaggerAppComponent
|
import io.element.android.x.di.DaggerAppComponent
|
||||||
|
|
@ -34,6 +35,7 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
||||||
AppInitializer.getInstance(this).apply {
|
AppInitializer.getInstance(this).apply {
|
||||||
initializeComponent(CrashInitializer::class.java)
|
initializeComponent(CrashInitializer::class.java)
|
||||||
initializeComponent(TracingInitializer::class.java)
|
initializeComponent(TracingInitializer::class.java)
|
||||||
|
initializeComponent(CacheCleanerInitializer::class.java)
|
||||||
}
|
}
|
||||||
logApplicationInfo()
|
logApplicationInfo()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
features/cachecleaner/api/build.gradle.kts
Normal file
29
features/cachecleaner/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.cachecleaner.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(libs.androidx.startup)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.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()
|
||||||
|
}
|
||||||
|
|
@ -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.cachecleaner.api
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
|
@ContributesTo(AppScope::class)
|
||||||
|
interface CacheCleanerBindings {
|
||||||
|
fun cacheCleaner(): CacheCleaner
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.cachecleaner.api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.startup.Initializer
|
||||||
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
|
||||||
|
class CacheCleanerInitializer : Initializer<Unit> {
|
||||||
|
override fun create(context: Context) {
|
||||||
|
context.bindings<CacheCleanerBindings>().cacheCleaner().clearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
||||||
|
}
|
||||||
41
features/cachecleaner/impl/build.gradle.kts
Normal file
41
features/cachecleaner/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.cachecleaner.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.anvilannotations)
|
||||||
|
anvil(projects.anvilcodegen)
|
||||||
|
api(projects.features.cachecleaner.api)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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.cachecleaner.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.cachecleaner.api.CacheCleaner
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.CacheDirectory
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [CacheCleaner].
|
||||||
|
*/
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultCacheCleaner @Inject constructor(
|
||||||
|
private val scope: 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() {
|
||||||
|
scope.launch(dispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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.cachecleaner.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
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 {
|
||||||
|
Truth.assertThat(exists()).isTrue()
|
||||||
|
Truth.assertThat(isDirectory).isTrue()
|
||||||
|
Truth.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(
|
||||||
|
scope = this,
|
||||||
|
dispatchers = this.testCoroutineDispatchers(true),
|
||||||
|
cacheDir = temporaryFolder.root,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,6 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
source = mediaSource,
|
source = mediaSource,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
body = body,
|
body = body,
|
||||||
useCache = false,
|
|
||||||
).mapCatching {
|
).mapCatching {
|
||||||
it.use { mediaFile ->
|
it.use { mediaFile ->
|
||||||
val dest = cachedFile.apply { parentFile?.mkdirs() }
|
val dest = cachedFile.apply { parentFile?.mkdirs() }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue