Skip to content

Commit a640628

Browse files
authored
Fix random avatar/sticker/video/photo swapping caused by stale cache keys (#190)
fix #123
1 parent 94b5f46 commit a640628

19 files changed

Lines changed: 320 additions & 56 deletions

File tree

data/src/main/java/org/monogram/data/chats/ChatCache.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.monogram.data.chats
33
import org.drinkless.tdlib.TdApi
44
import org.monogram.data.datasource.cache.ChatsCacheDataSource
55
import org.monogram.data.datasource.cache.UserCacheDataSource
6+
import java.io.File
67
import java.util.concurrent.ConcurrentHashMap
78

89
class ChatCache : ChatsCacheDataSource, UserCacheDataSource {
@@ -424,11 +425,14 @@ class ChatCache : ChatsCacheDataSource, UserCacheDataSource {
424425
)
425426
clientData = "mc:${entity.memberCount};oc:${entity.onlineCount}"
426427
}
427-
if (entity.photoId != 0) {
428-
fileCache[entity.photoId] = TdApi.File().apply {
429-
id = entity.photoId
430-
local = TdApi.LocalFile().apply {
431-
this.path = entity.avatarPath.orEmpty()
428+
if (entity.photoId != 0 && !entity.avatarPath.isNullOrEmpty()) {
429+
val avatarFile = File(entity.avatarPath)
430+
if (avatarFile.exists()) {
431+
fileCache[entity.photoId] = TdApi.File().apply {
432+
id = entity.photoId
433+
local = TdApi.LocalFile().apply {
434+
this.path = entity.avatarPath
435+
}
432436
}
433437
}
434438
}

data/src/main/java/org/monogram/data/chats/ChatFileManager.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package org.monogram.data.chats
22

3-
import org.monogram.data.core.coRunCatching
43
import kotlinx.coroutines.launch
54
import org.drinkless.tdlib.TdApi
65
import org.monogram.core.DispatcherProvider
76
import org.monogram.core.ScopeProvider
7+
import org.monogram.data.core.coRunCatching
88
import org.monogram.data.gateway.TelegramGateway
99
import org.monogram.data.infra.FileDownloadQueue
10+
import org.monogram.data.infra.FileUpdateHandler
1011
import java.util.*
1112
import java.util.concurrent.ConcurrentHashMap
1213

@@ -15,6 +16,7 @@ class ChatFileManager(
1516
private val gateway: TelegramGateway,
1617
private val dispatchers: DispatcherProvider,
1718
private val fileQueue: FileDownloadQueue,
19+
private val fileUpdateHandler: FileUpdateHandler,
1820
scopeProvider: ScopeProvider,
1921
private val onUpdate: () -> Unit
2022
) {
@@ -23,13 +25,11 @@ class ChatFileManager(
2325
private val downloadingFiles: MutableSet<Int> = Collections.newSetFromMap(ConcurrentHashMap())
2426
private val loadingEmojis: MutableSet<Long> = Collections.newSetFromMap(ConcurrentHashMap())
2527
private val filePaths = ConcurrentHashMap<Int, String>()
26-
private val emojiPathsCache = ConcurrentHashMap<Long, String>()
27-
private val fileIdToEmojiId = ConcurrentHashMap<Int, Long>()
2828
private val chatPhotoIds = ConcurrentHashMap<Int, Long>()
2929
private val trackedFileIds = Collections.newSetFromMap(ConcurrentHashMap<Int, Boolean>())
3030

3131
fun getFilePath(fileId: Int): String? = filePaths[fileId]
32-
fun getEmojiPath(emojiId: Long): String? = emojiPathsCache[emojiId]
32+
fun getEmojiPath(emojiId: Long): String? = fileUpdateHandler.customEmojiPaths[emojiId]
3333
fun getChatIdByPhotoId(fileId: Int): Long? = chatPhotoIds[fileId]
3434

3535
fun registerChatPhoto(fileId: Int, chatId: Long) {
@@ -51,8 +51,8 @@ class ChatFileManager(
5151
private fun handleFileUpdated(fileId: Int, path: String): Boolean {
5252
if (path.isEmpty()) return false
5353
var updated = false
54-
fileIdToEmojiId[fileId]?.let { emojiId ->
55-
emojiPathsCache[emojiId] = path
54+
fileUpdateHandler.fileIdToCustomEmojiId[fileId]?.let { emojiId ->
55+
fileUpdateHandler.customEmojiPaths[emojiId] = path
5656
updated = true
5757
}
5858
if (chatPhotoIds.containsKey(fileId)) updated = true
@@ -74,17 +74,17 @@ class ChatFileManager(
7474
}
7575

7676
fun loadEmoji(emojiId: Long) {
77-
if (emojiId == 0L || emojiPathsCache.containsKey(emojiId)) return
77+
if (emojiId == 0L || fileUpdateHandler.customEmojiPaths.containsKey(emojiId)) return
7878
if (loadingEmojis.add(emojiId)) {
7979
scope.launch(dispatchers.io) {
8080
coRunCatching {
8181
val result = gateway.execute(TdApi.GetCustomEmojiStickers(longArrayOf(emojiId)))
8282
val sticker = result.stickers.firstOrNull() ?: return@launch
8383
val file = sticker.sticker
8484
val path = file.local.path.ifEmpty { filePaths[file.id] ?: "" }
85-
fileIdToEmojiId[file.id] = emojiId
85+
fileUpdateHandler.fileIdToCustomEmojiId[file.id] = emojiId
8686
if (path.isNotEmpty()) {
87-
emojiPathsCache[emojiId] = path
87+
fileUpdateHandler.customEmojiPaths[emojiId] = path
8888
onUpdate()
8989
} else {
9090
downloadFile(file.id, 32)

data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.monogram.data.chats.ChatCache
1212
import org.monogram.data.gateway.TdLibException
1313
import org.monogram.data.gateway.TelegramGateway
1414
import org.monogram.data.infra.FileDownloadQueue
15+
import org.monogram.data.infra.FileUpdateHandler
1516
import org.monogram.data.mapper.MessageMapper
1617
import org.monogram.data.mapper.toApi
1718
import org.monogram.domain.models.*
@@ -28,6 +29,7 @@ class TdMessageRemoteDataSource(
2829
private val cache: ChatCache,
2930
private val pollRepository: PollRepository,
3031
private val fileDownloadQueue: FileDownloadQueue,
32+
private val fileUpdateHandler: FileUpdateHandler,
3133
private val dispatcherProvider: DispatcherProvider,
3234
scopeProvider: ScopeProvider
3335
) : MessageRemoteDataSource {
@@ -69,8 +71,6 @@ class TdMessageRemoteDataSource(
6971
enum class DownloadType { VIDEO, GIF, STICKER, VIDEO_NOTE, DEFAULT }
7072

7173
private val fileIdToMessageMap = fileDownloadQueue.registry.fileIdToMessageMap
72-
val customEmojiPaths = ConcurrentHashMap<Long, String>()
73-
val fileIdToCustomEmojiId = ConcurrentHashMap<Int, Long>()
7474
private val messageUpdateJobs = ConcurrentHashMap<Pair<Long, Long>, Job>()
7575
private val lastProgressMap = ConcurrentHashMap<Int, Int>()
7676
private val lastDownloadActiveMap = ConcurrentHashMap<Int, Boolean>()
@@ -1357,8 +1357,8 @@ class TdMessageRemoteDataSource(
13571357
if (isDC) {
13581358
fileDownloadQueue.notifyDownloadComplete(file.id)
13591359
lastProgressMap.remove(file.id)
1360-
fileIdToCustomEmojiId[file.id]?.let { customEmojiId ->
1361-
customEmojiPaths[customEmojiId] = file.local?.path ?: ""
1360+
fileUpdateHandler.fileIdToCustomEmojiId[file.id]?.let { customEmojiId ->
1361+
fileUpdateHandler.customEmojiPaths[customEmojiId] = file.local?.path ?: ""
13621362
}
13631363

13641364
val entries = fileIdToMessageMap[file.id]

data/src/main/java/org/monogram/data/di/dataModule.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,7 @@ val dataModule = module {
288288
gateway = get(),
289289
userRepository = get(),
290290
chatInfoRepository = get(),
291-
customEmojiPaths = get<FileUpdateHandler>().customEmojiPaths,
292-
fileIdToCustomEmojiId = get<FileUpdateHandler>().fileIdToCustomEmojiId,
291+
fileUpdateHandler = get(),
293292
fileApi = get(),
294293
appPreferences = get(),
295294
cache = get(),
@@ -329,6 +328,7 @@ val dataModule = module {
329328
chatFolderDao = get(),
330329
userFullInfoDao = get(),
331330
fileQueue = get(),
331+
fileUpdateHandler = get(),
332332
stringProvider = get()
333333
)
334334
}
@@ -421,6 +421,7 @@ val dataModule = module {
421421
cache = get(),
422422
pollRepository = get(),
423423
fileDownloadQueue = get(),
424+
fileUpdateHandler = get(),
424425
dispatcherProvider = get(),
425426
scopeProvider = get()
426427
)

data/src/main/java/org/monogram/data/mapper/MessageMapper.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.monogram.data.chats.ChatCache
1010
import org.monogram.data.datasource.remote.MessageFileApi
1111
import org.monogram.data.datasource.remote.TdMessageRemoteDataSource
1212
import org.monogram.data.gateway.TelegramGateway
13+
import org.monogram.data.infra.FileUpdateHandler
1314
import org.monogram.domain.models.*
1415
import org.monogram.domain.repository.AppPreferencesProvider
1516
import org.monogram.domain.repository.ChatInfoRepository
@@ -22,14 +23,15 @@ class MessageMapper(
2223
private val gateway: TelegramGateway,
2324
private val userRepository: UserRepository,
2425
private val chatInfoRepository: ChatInfoRepository,
25-
private val customEmojiPaths: ConcurrentHashMap<Long, String>,
26-
private val fileIdToCustomEmojiId: ConcurrentHashMap<Int, Long>,
26+
private val fileUpdateHandler: FileUpdateHandler,
2727
private val fileApi: MessageFileApi,
2828
private val appPreferences: AppPreferencesProvider,
2929
private val cache: ChatCache,
3030
scopeProvider: ScopeProvider
3131
) {
3232
val scope = scopeProvider.appScope
33+
private val customEmojiPaths = fileUpdateHandler.customEmojiPaths
34+
private val fileIdToCustomEmojiId = fileUpdateHandler.fileIdToCustomEmojiId
3335

3436
private data class SenderUserSnapshot(
3537
val name: String,
@@ -125,12 +127,14 @@ class MessageMapper(
125127
}
126128

127129
private fun resolveCachedPath(fileId: Int, storedPath: String?): String? {
128-
val fromCache = fileId.takeIf { it != 0 }
129-
?.let { cache.fileCache[it]?.local?.path }
130+
val fromStored = storedPath
131+
?.takeIf { it.isNotBlank() }
130132
?.takeIf { isValidPath(it) }
131-
if (fromCache != null) return fromCache
133+
if (fromStored != null) return fromStored
132134

133-
return storedPath?.takeIf { it.isNotBlank() }?.takeIf { isValidPath(it) }
135+
return fileId.takeIf { it != 0 }
136+
?.let { cache.fileCache[it]?.local?.path }
137+
?.takeIf { isValidPath(it) }
134138
}
135139

136140
private fun registerCachedFile(fileId: Int, chatId: Long, messageId: Long) {

data/src/main/java/org/monogram/data/repository/ChatsListRepositoryImpl.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.monogram.data.gateway.TelegramGateway
2323
import org.monogram.data.gateway.UpdateDispatcher
2424
import org.monogram.data.infra.ConnectionManager
2525
import org.monogram.data.infra.FileDownloadQueue
26+
import org.monogram.data.infra.FileUpdateHandler
2627
import org.monogram.data.mapper.ChatMapper
2728
import org.monogram.data.mapper.MessageMapper
2829
import org.monogram.domain.models.ChatModel
@@ -53,6 +54,7 @@ class ChatsListRepositoryImpl(
5354
private val chatFolderDao: ChatFolderDao,
5455
private val userFullInfoDao: UserFullInfoDao,
5556
private val fileQueue: FileDownloadQueue,
57+
private val fileUpdateHandler: FileUpdateHandler,
5658
private val stringProvider: StringProvider
5759
) : ChatListRepository,
5860
ChatFolderRepository,
@@ -96,6 +98,7 @@ class ChatsListRepositoryImpl(
9698
dispatchers = dispatchers,
9799
scopeProvider = scopeProvider,
98100
fileQueue = fileQueue,
101+
fileUpdateHandler = fileUpdateHandler,
99102
onUpdate = {
100103
triggerUpdate()
101104
refreshActiveForumTopics()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.monogram.presentation.core.util
2+
3+
import java.io.File
4+
5+
fun fileCacheKey(path: String?): String? {
6+
if (path.isNullOrBlank()) return null
7+
val file = File(path)
8+
if (!file.exists()) return null
9+
return "${file.absolutePath}:${file.lastModified()}:${file.length()}"
10+
}
11+
12+
fun miniThumbnailCacheKey(data: ByteArray): String {
13+
return "mini:${data.contentHashCode()}:${data.size}"
14+
}
15+
16+
fun mediaCacheKey(data: Any?): String? {
17+
return when (data) {
18+
null -> null
19+
is ByteArray -> miniThumbnailCacheKey(data)
20+
is File -> "${data.absolutePath}:${data.lastModified()}:${data.length()}"
21+
is String -> {
22+
when {
23+
data.startsWith("http://", ignoreCase = true) ||
24+
data.startsWith("https://", ignoreCase = true) ||
25+
data.startsWith("content:", ignoreCase = true) ||
26+
data.startsWith("file:", ignoreCase = true) -> data
27+
28+
else -> fileCacheKey(data) ?: data
29+
}
30+
}
31+
32+
else -> data.toString()
33+
}
34+
}
35+
36+
fun namespacedCacheKey(namespace: String, data: Any?): String? {
37+
val key = mediaCacheKey(data) ?: return null
38+
return "$namespace:$key"
39+
}

presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlphaVideoPlayer.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import kotlinx.coroutines.isActive
6161
import org.monogram.domain.repository.PlayerDataSourceFactory
6262
import org.monogram.presentation.core.util.LocalVideoPlayerPool
6363
import org.monogram.presentation.core.util.getMimeType
64+
import org.monogram.presentation.core.util.namespacedCacheKey
6465
import java.io.File
6566
import java.io.FileNotFoundException
6667
import java.util.concurrent.ArrayBlockingQueue
@@ -252,15 +253,20 @@ fun VideoStickerPlayer(
252253
exit = fadeOut(tween(250)),
253254
modifier = Modifier.fillMaxSize()
254255
) {
256+
val thumbnailCacheKey = remember(currentPath, thumbnailData, fileId) {
257+
namespacedCacheKey("video_sticker_thumb:$fileId", thumbnailData ?: currentPath)
258+
}
255259
AsyncImage(
256260
model = ImageRequest.Builder(context)
257261
.data(thumbnailData ?: currentPath)
258262
.apply {
263+
thumbnailCacheKey?.let {
264+
memoryCacheKey(it)
265+
diskCacheKey(it)
266+
}
259267
if (thumbnailData == null) {
260268
decoderFactory(VideoFrameDecoder.Factory())
261269
videoFrameMillis(0)
262-
memoryCacheKey(currentPath)
263-
diskCacheKey(currentPath)
264270
}
265271
}
266272
.crossfade(false)

0 commit comments

Comments
 (0)