Benoit Marty
10 months ago
committed by
GitHub
40 changed files with 646 additions and 93 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Suppress usage of removeTimeline method. |
@ -0,0 +1,29 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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() |
||||
} |
@ -0,0 +1,41 @@
@@ -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 @@
@@ -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 @@
@@ -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, |
||||
) |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* 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.messages.impl.voicemessages.timeline |
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers |
||||
import io.element.android.libraries.di.RoomScope |
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem |
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent |
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer |
||||
import kotlinx.coroutines.withContext |
||||
import javax.inject.Inject |
||||
|
||||
interface RedactedVoiceMessageManager { |
||||
suspend fun onEachMatrixTimelineItem(timelineItems: List<MatrixTimelineItem>) |
||||
} |
||||
|
||||
@ContributesBinding(RoomScope::class) |
||||
class DefaultRedactedVoiceMessageManager @Inject constructor( |
||||
private val dispatchers: CoroutineDispatchers, |
||||
private val mediaPlayer: MediaPlayer, |
||||
) : RedactedVoiceMessageManager { |
||||
override suspend fun onEachMatrixTimelineItem(timelineItems: List<MatrixTimelineItem>) { |
||||
withContext(dispatchers.computation) { |
||||
mediaPlayer.state.value.let { playerState -> |
||||
if (playerState.isPlaying && playerState.mediaId != null) { |
||||
val needsToPausePlayer = timelineItems.any { |
||||
it is MatrixTimelineItem.Event && |
||||
playerState.mediaId == it.eventId?.value && |
||||
it.event.content is RedactedContent |
||||
} |
||||
if (needsToPausePlayer) { |
||||
withContext(dispatchers.main) { mediaPlayer.pause() } |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
/* |
||||
* 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.messages.voicemessages.timeline |
||||
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager |
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem |
||||
|
||||
class FakeRedactedVoiceMessageManager : RedactedVoiceMessageManager { |
||||
|
||||
private val _invocations: MutableList<List<MatrixTimelineItem>> = mutableListOf() |
||||
val invocations: List<List<MatrixTimelineItem>> |
||||
get() = _invocations |
||||
|
||||
override suspend fun onEachMatrixTimelineItem(timelineItems: List<MatrixTimelineItem>) { |
||||
_invocations.add(timelineItems) |
||||
} |
||||
} |
@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
|
||||
/* |
||||
* 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.messages.voicemessages.timeline |
||||
|
||||
import com.google.common.truth.Truth |
||||
import io.element.android.features.messages.impl.voicemessages.timeline.DefaultRedactedVoiceMessageManager |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem |
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo |
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem |
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails |
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 |
||||
import io.element.android.libraries.matrix.test.A_USER_ID |
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer |
||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer |
||||
import io.element.android.tests.testutils.testCoroutineDispatchers |
||||
import kotlinx.coroutines.test.TestScope |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class RedactedVoiceMessageManagerTest { |
||||
@Test |
||||
fun `redacted event - no playing related media`() = runTest { |
||||
val mediaPlayer = FakeMediaPlayer().apply { |
||||
setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = "audio/ogg") |
||||
play() |
||||
} |
||||
val manager = aDefaultRedactedVoiceMessageManager(mediaPlayer = mediaPlayer) |
||||
|
||||
Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) |
||||
Truth.assertThat(mediaPlayer.state.value.isPlaying).isTrue() |
||||
|
||||
manager.onEachMatrixTimelineItem(aRedactedMatrixTimeline(AN_EVENT_ID_2)) |
||||
|
||||
Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) |
||||
Truth.assertThat(mediaPlayer.state.value.isPlaying).isTrue() |
||||
} |
||||
|
||||
@Test |
||||
fun `redacted event - playing related media is paused`() = runTest { |
||||
val mediaPlayer = FakeMediaPlayer().apply { |
||||
setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = "audio/ogg") |
||||
play() |
||||
} |
||||
val manager = aDefaultRedactedVoiceMessageManager(mediaPlayer = mediaPlayer) |
||||
|
||||
Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) |
||||
Truth.assertThat(mediaPlayer.state.value.isPlaying).isTrue() |
||||
|
||||
manager.onEachMatrixTimelineItem(aRedactedMatrixTimeline(AN_EVENT_ID)) |
||||
|
||||
Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) |
||||
Truth.assertThat(mediaPlayer.state.value.isPlaying).isFalse() |
||||
} |
||||
} |
||||
|
||||
fun TestScope.aDefaultRedactedVoiceMessageManager( |
||||
mediaPlayer: MediaPlayer = FakeMediaPlayer(), |
||||
) = DefaultRedactedVoiceMessageManager( |
||||
dispatchers = this.testCoroutineDispatchers(true), |
||||
mediaPlayer = mediaPlayer, |
||||
) |
||||
|
||||
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>( |
||||
MatrixTimelineItem.Event( |
||||
uniqueId = 0, |
||||
event = EventTimelineItem( |
||||
eventId = eventId, |
||||
transactionId = null, |
||||
isEditable = false, |
||||
isLocal = false, |
||||
isOwn = false, |
||||
isRemote = false, |
||||
localSendState = null, |
||||
reactions = listOf(), |
||||
sender = A_USER_ID, |
||||
senderProfile = ProfileTimelineDetails.Unavailable, |
||||
timestamp = 9442, |
||||
content = RedactedContent, |
||||
debugInfo = TimelineItemDebugInfo( |
||||
model = "enim", |
||||
originalJson = null, |
||||
latestEditedJson = null |
||||
), |
||||
origin = null |
||||
), |
||||
) |
||||
) |
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.
Loading…
Reference in new issue