Skip to content

Commit 363b8db

Browse files
committed
feat: handle cell urls expiration (WPB-21328)
1 parent 3cbcc91 commit 363b8db

File tree

6 files changed

+57
-15
lines changed

6 files changed

+57
-15
lines changed

app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ class SharedCallingViewModel @AssistedInject constructor(
338338
private fun recentInCallReactionMap(): MutableMap<UserId, String> =
339339
ExpiringMap<UserId, String>(
340340
scope = viewModelScope,
341-
expiration = InCallReactions.recentReactionShowDurationMs,
341+
expirationMs = InCallReactions.recentReactionShowDurationMs,
342342
delegate = mutableStateMapOf<UserId, String>()
343343
)
344344

app/src/main/kotlin/com/wire/android/ui/common/multipart/MultipartAttachmentUi.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ data class MultipartAttachmentUi(
3131
val localPath: String?,
3232
val contentHash: String? = null,
3333
val contentUrl: String? = null,
34+
val contentUrlExpiresAt: Long? = null,
3435
val previewUrl: String? = null,
3536
val mimeType: String,
3637
val assetType: AttachmentFileType,
@@ -55,6 +56,7 @@ fun CellAssetContent.toUiModel(progress: Float?) = MultipartAttachmentUi(
5556
fileName = this.assetPath?.substringAfterLast("/"),
5657
localPath = this.localPath,
5758
contentUrl = this.contentUrl,
59+
contentUrlExpiresAt = this.contentUrlExpiresAt,
5860
previewUrl = this.previewUrl,
5961
mimeType = this.mimeType,
6062
assetType = AttachmentFileType.fromMimeType(mimeType),

app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ import androidx.compose.runtime.mutableStateMapOf
2121
import androidx.lifecycle.ViewModel
2222
import androidx.lifecycle.viewModelScope
2323
import com.wire.android.appLogger
24+
import com.wire.android.feature.cells.domain.model.AttachmentFileType
2425
import com.wire.android.feature.cells.domain.model.AttachmentFileType.IMAGE
2526
import com.wire.android.feature.cells.domain.model.AttachmentFileType.PDF
2627
import com.wire.android.feature.cells.domain.model.AttachmentFileType.VIDEO
27-
import com.wire.android.feature.cells.domain.model.AttachmentFileType
2828
import com.wire.android.ui.common.multipart.AssetSource
2929
import com.wire.android.ui.common.multipart.MultipartAttachmentUi
3030
import com.wire.android.ui.common.multipart.toUiModel
31+
import com.wire.android.util.ExpiringMap
3132
import com.wire.android.util.FileManager
3233
import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase
3334
import com.wire.kalium.cells.domain.usecase.RefreshCellAssetStateUseCase
@@ -41,6 +42,7 @@ import kotlinx.collections.immutable.toImmutableList
4142
import kotlinx.coroutines.launch
4243
import okio.Path.Companion.toPath
4344
import javax.inject.Inject
45+
import kotlin.time.Duration.Companion.hours
4446

4547
@HiltViewModel
4648
class MultipartAttachmentsViewModel @Inject constructor(
@@ -50,7 +52,18 @@ class MultipartAttachmentsViewModel @Inject constructor(
5052
private val kaliumFileSystem: KaliumFileSystem,
5153
) : ViewModel() {
5254

53-
private val refreshed = mutableListOf<String>()
55+
private companion object {
56+
val DEFAULT_CONTENT_URL_EXPIRY_MS = 1.hours.inWholeMilliseconds
57+
}
58+
59+
private val refreshed = ExpiringMap<String, Unit>(
60+
scope = viewModelScope,
61+
expirationMs = DEFAULT_CONTENT_URL_EXPIRY_MS,
62+
delegate = mutableMapOf(),
63+
onEntryExpired = { key, _ ->
64+
viewModelScope.launch { refreshAsset(key) }
65+
}
66+
)
5467

5568
private val uploadProgress = mutableStateMapOf<String, Float>()
5669

@@ -105,12 +118,17 @@ class MultipartAttachmentsViewModel @Inject constructor(
105118
}
106119

107120
fun refreshAssetState(attachment: MultipartAttachmentUi) {
108-
if (refreshed.contains(attachment.uuid).not()) {
109-
refreshed.add(attachment.uuid)
110-
if (attachment.source == AssetSource.CELL) {
111-
viewModelScope.launch { refreshAsset(attachment.uuid) }
112-
}
121+
122+
if (attachment.source != AssetSource.CELL) return
123+
if (refreshed.contains(attachment.uuid)) return
124+
125+
if (attachment.contentUrlExpiresAt != null) {
126+
refreshed.putWithExpireAt(attachment.uuid, Unit, attachment.contentUrlExpiresAt)
127+
} else {
128+
refreshed.put(attachment.uuid, Unit)
113129
}
130+
131+
viewModelScope.launch { refreshAsset(attachment.uuid) }
114132
}
115133

116134
private fun openLocalFile(attachment: MultipartAttachmentUi) {

app/src/main/kotlin/com/wire/android/util/ExpiringMap.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,25 @@ import java.util.concurrent.ConcurrentHashMap
2828
*/
2929
class ExpiringMap<K, V>(
3030
private val scope: CoroutineScope,
31-
private val expiration: Long,
31+
private val expirationMs: Long,
3232
private val delegate: MutableMap<K, V>,
33+
private val onEntryExpired: ((key: K, value: V?) -> Unit)? = null,
3334
private val currentTime: () -> Long = { System.currentTimeMillis() },
3435
) : MutableMap<K, V> by delegate {
3536

3637
private val timestamps: MutableMap<K, Long> = ConcurrentHashMap<K, Long>()
3738
private var cleanupJob: Job? = null
3839

40+
fun putWithExpireAt(key: K, value: V, expireAt: Long): V? {
41+
return delegate.put(key, value).also {
42+
timestamps.put(key, expireAt)
43+
scheduleCleanup()
44+
}
45+
}
46+
3947
override fun put(key: K, value: V): V? {
4048
return delegate.put(key, value).also {
41-
timestamps.put(key, currentTime() + expiration)
49+
timestamps.put(key, currentTime() + expirationMs)
4250
scheduleCleanup()
4351
}
4452
}
@@ -65,7 +73,8 @@ class ExpiringMap<K, V>(
6573
val now = currentTime()
6674
timestamps.entries.onEach { (key, expiration) ->
6775
if (expiration <= now) {
68-
delegate.remove(key)
76+
val value = delegate.remove(key)
77+
onEntryExpired?.invoke(key, value)
6978
}
7079
}
7180
timestamps.entries.removeAll { it.value <= now }

app/src/test/kotlin/com/wire/android/util/ExpiringMapTest.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ExpiringMapTest {
5252
}
5353

5454
@Test
55-
fun `check item can not be obtained before expiration`() = runTest {
55+
fun `check item can be obtained before expiration`() = runTest {
5656
// given
5757
val map = withTestExpiringMap()
5858

@@ -107,9 +107,22 @@ class ExpiringMapTest {
107107
assertEquals(null, map["testKey"])
108108
}
109109

110-
private fun TestScope.withTestExpiringMap(): MutableMap<String, String> = ExpiringMap<String, String>(
110+
@Test
111+
fun `check item can be put with custom expiration`() = runTest {
112+
// given
113+
val map = withTestExpiringMap()
114+
115+
// when
116+
map.putWithExpireAt("testKey", "testValue", currentTime + 500)
117+
advanceTimeBy(301) // advance past default expiration
118+
119+
// then
120+
assertEquals("testValue", map["testKey"])
121+
}
122+
123+
private fun TestScope.withTestExpiringMap(): ExpiringMap<String, String> = ExpiringMap<String, String>(
111124
scope = this.backgroundScope,
112-
expiration = 300,
125+
expirationMs = 300,
113126
delegate = mutableMapOf(),
114127
currentTime = { currentTime }
115128
)

0 commit comments

Comments
 (0)