android: harden avatar cache reads and refresh current download sources
This commit is contained in:
1 parent
15dbb6fe51
commit
b3c0a2fa7b
3 files changed
+56
-12
No files matched your search
@@ -64,7 +64,7 @@ class AvatarRepository(context: Context) {
|
||||
scope.launch {
|
||||
try {
|
||||
val bitmap = withContext(Dispatchers.IO) {
|
||||
decode(File(directory(uid), file).readBytes(), file.removeSuffix(".png"), false)
|
||||
decode(readImage(File(directory(uid), file)), file.removeSuffix(".png"), false)
|
||||
}
|
||||
alive(epoch); cache.put(file, bitmap); changed()
|
||||
} catch (e: CancellationException) { throw e }
|
||||
@@ -148,7 +148,7 @@ class AvatarRepository(context: Context) {
|
||||
_members.value = _members.value.filterKeys { it.group != group }
|
||||
_groups.value -= group; dirtyGroups.remove(group); changed(); return
|
||||
}
|
||||
if (_groups.value.containsKey(group)) ensureGroup(group, true)
|
||||
if (type == 4 && _groups.value.containsKey(group)) ensureGroup(group, true)
|
||||
requestMembers(group)
|
||||
}
|
||||
}
|
||||
@@ -222,7 +222,7 @@ class AvatarRepository(context: Context) {
|
||||
val record = AvatarProtocol.group(request(53, group) { id, gid -> NativeLib.requestGroupAvatar(id, gid) })
|
||||
require(!record.failed) { "core rejected stored group avatar" }
|
||||
val state = GroupAvatarState(record.version, record.hash, record.canEdit, hasImage = record.image.isNotEmpty())
|
||||
val bitmap = if (record.image.isEmpty()) null else withContext(Dispatchers.IO) {
|
||||
val bitmap = if (record.image.isEmpty()) null else cache.get(groupKey(group, state)) ?: withContext(Dispatchers.IO) {
|
||||
decode(record.image, record.hash, true)
|
||||
}
|
||||
alive(epoch)
|
||||
@@ -259,28 +259,47 @@ class AvatarRepository(context: Context) {
|
||||
while (true) {
|
||||
alive(epoch)
|
||||
foreground.first { it }
|
||||
if (member(group, owner)?.avatarFile != key) break
|
||||
// Повтор берёт актуальную ссылку; тот же хеш может быть доступен через другую группу/сервер.
|
||||
val candidates = _members.value.entries.filter { it.value.avatarFile == key && it.value.avatarServer != 0L }
|
||||
.sortedBy { if (it.key.group == sourceGroup && it.key.owner == owner) 0 else 1 }
|
||||
if (candidates.isEmpty()) break
|
||||
val source = candidates[retry % candidates.size]
|
||||
try {
|
||||
val bitmap = transfers.withPermit {
|
||||
val root = directory(uid); val file = File(root, key)
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!root.isDirectory && !root.mkdirs()) throw IOException("cannot create cache directory")
|
||||
if (file.isFile) {
|
||||
try { decode(file.readBytes(), key.removeSuffix(".png"), false).also { file.setLastModified(System.currentTimeMillis()) } }
|
||||
catch (e: Exception) { log("WARN", "Invalid cached avatar file=$key: ${e.message}"); file.delete(); null }
|
||||
try {
|
||||
decode(readImage(file), key.removeSuffix(".png"), false).also {
|
||||
if (!file.setLastModified(System.currentTimeMillis())) log("WARN", "Cannot touch cache file=$file")
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
log("WARN", "Invalid cached avatar file=$key: ${e.message}")
|
||||
if (!file.delete()) log("WARN", "Cannot delete corrupt cache file=$file")
|
||||
null
|
||||
}
|
||||
} else null
|
||||
} ?: download(sourceGroup, m, root)
|
||||
} ?: download(source.key.group, source.value, root)
|
||||
}
|
||||
alive(epoch); cache.put(key, bitmap); changed()
|
||||
log("INFO", "User image ready group=$sourceGroup owner=${AvatarProtocol.hex(owner)} file=$key retry=$retry")
|
||||
break
|
||||
} catch (e: CancellationException) { throw e }
|
||||
catch (e: Exception) {
|
||||
log("WARN", "User image failed group=$sourceGroup owner=${AvatarProtocol.hex(owner)} server=${AvatarProtocol.hex(m.avatarServer)} file=$key retry=$retry: ${e.message}")
|
||||
log("WARN", "User image failed group=${source.key.group} owner=${AvatarProtocol.hex(source.value.nodeId)} server=${AvatarProtocol.hex(source.value.avatarServer)} file=$key retry=$retry: ${e.message}")
|
||||
if (member(group, owner)?.avatarFile != key) break
|
||||
delay((5000L shl retry.coerceAtMost(5))); retry++
|
||||
}
|
||||
}
|
||||
} finally { if (generation == epoch) loads.remove(key) }
|
||||
} finally {
|
||||
if (generation == epoch) {
|
||||
loads.remove(key)
|
||||
if (cache.get(key) == null) changed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +312,7 @@ class AvatarRepository(context: Context) {
|
||||
}) ?: throw IOException("missing final transfer result")
|
||||
require(reply.status == 0 && reply.size in 1..AvatarProtocol.MAX_USER) { "download status=${reply.status} size=${reply.size}" }
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = decode(temp.readBytes(), m.avatarFile.removeSuffix(".png"), false)
|
||||
val bitmap = decode(readImage(temp), m.avatarFile.removeSuffix(".png"), false)
|
||||
val target = File(root, m.avatarFile)
|
||||
if (!temp.renameTo(target)) throw IOException("cannot publish validated cache file")
|
||||
prune(root)
|
||||
@@ -302,6 +321,21 @@ class AvatarRepository(context: Context) {
|
||||
} finally { withContext(NonCancellable + Dispatchers.IO) { if (temp.exists() && !temp.delete()) log("WARN", "Cannot delete temp=$temp") } }
|
||||
}
|
||||
|
||||
private fun readImage(file: File): ByteArray {
|
||||
val size = file.length()
|
||||
require(size in 1..AvatarProtocol.MAX_USER) { "invalid cached file size=$size path=$file" }
|
||||
return file.inputStream().use { input ->
|
||||
val bytes = ByteArray(size.toInt()); var offset = 0
|
||||
while (offset < bytes.size) {
|
||||
val count = input.read(bytes, offset, bytes.size - offset)
|
||||
require(count > 0) { "cached file truncated while reading" }
|
||||
offset += count
|
||||
}
|
||||
require(input.read() == -1) { "cached file grew while reading" }
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
/** Проверяем размер, хеш и геометрию до выделения декодированного изображения. */
|
||||
private fun decode(bytes: ByteArray, hash: String, group: Boolean): Bitmap {
|
||||
require(bytes.size in 1..if (group) AvatarProtocol.MAX_GROUP else AvatarProtocol.MAX_USER) { "image byte limit" }
|
||||
@@ -317,7 +351,9 @@ class AvatarRepository(context: Context) {
|
||||
}
|
||||
private fun prune(root: File) {
|
||||
val pinned = defaultAvatar?.substringAfter('/')
|
||||
val files = root.listFiles()?.filter { AvatarProtocol.validFile(it.name) }?.sortedBy { it.lastModified() } ?: return
|
||||
val entries = root.listFiles()
|
||||
if (entries == null) { log("WARN", "Cannot enumerate avatar cache directory=$root"); return }
|
||||
val files = entries.filter { AvatarProtocol.validFile(it.name) }.sortedBy { it.lastModified() }
|
||||
var size = files.sumOf { it.length() }
|
||||
for (file in files) if (size > 100L * 1024 * 1024 && file.name != pinned) {
|
||||
val bytes = file.length()
|
||||
@@ -401,7 +437,7 @@ class AvatarRepository(context: Context) {
|
||||
transfers.withPermit {
|
||||
if (server != 0L) {
|
||||
val local = File(directory(uid), file)
|
||||
withContext(Dispatchers.IO) { decode(local.readBytes(), file.removeSuffix(".png"), false) }
|
||||
withContext(Dispatchers.IO) { decode(readImage(local), file.removeSuffix(".png"), false) }
|
||||
val mkdir = AvatarProtocol.response(request(49, group, server, uid, 8) { id, gid -> NativeLib.avatarMkdir(id, gid, server, uid) })
|
||||
require(mkdir.status == 0 || mkdir.status == 5) { "mkdir status=${mkdir.status}" }
|
||||
val stat = AvatarProtocol.response(request(49, group, server, uid, 2) { id, gid -> NativeLib.avatarStat(id, gid, server, uid, "avatars/$file") })
|
||||
|
||||
@@ -152,7 +152,10 @@ fun ChatScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
if (properties) GroupPropertiesDialog(channel, onDismiss = { properties = false })
|
||||
if (properties) GroupPropertiesDialog(channel, onDismiss = {
|
||||
properties = false
|
||||
if (soundOn == com.utun.chat.data.NotificationSound.isMuted(channel.id)) viewModel.toggleSound()
|
||||
})
|
||||
if (radioActive) {
|
||||
RadioSourceBar(viewModel)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user