android: add asynchronous avatar JNI operations and wire models

This commit is contained in:
evgeny committed 2026-10-06 16:15:00 +02:00
1 parent 714370a1a2
commit 8efe075679
4 files changed
+189 -7

No files matched your search

@@ -0,0 +1,62 @@
package com.utun.chat.data
import java.nio.ByteBuffer
import java.nio.ByteOrder
/** Форматы событий общего ядра; все идентификаторы сохраняют 64 бита, включая старший бит. */
object AvatarProtocol {
const val MAX_USER = 1024 * 1024
const val MAX_GROUP = 16 * 1024
private val filePattern = Regex("[0-9a-f]{64}\\.png")
fun validFile(file: String): Boolean = filePattern.matches(file)
fun node(id: String): Long = id.toULongOrNull(16)?.toLong() ?: 0
fun hex(id: Long): String = id.toULong().toString(16).padStart(16, '0')
fun hash(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it.toInt() and 255) }
private fun buffer(data: ByteArray) = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
data class Group(val id: Long, val group: Long, val version: Long, val hash: String,
val canEdit: Boolean, val failed: Boolean, val image: ByteArray)
fun group(data: ByteArray): Group {
require(data.size in 60..60 + MAX_GROUP) { "group snapshot length=${data.size}" }
val b = buffer(data)
val id = b.long; val group = b.long; val version = b.long
val hash = ByteArray(32); b.get(hash)
val canEdit = b.get().toInt(); val failed = b.get().toInt()
val size = b.short.toInt() and 65535
require(canEdit in 0..1 && failed in 0..1 && size == b.remaining()) { "invalid group snapshot header" }
return Group(id, group, version, hash(hash), canEdit != 0, failed != 0, ByteArray(size).also { b.get(it) })
}
data class Reply(val id: Long, val group: Long, val server: Long = 0, val owner: Long = 0,
val command: Int = 0, val status: Int = 0, val size: Long = 0, val hash: String = "")
fun response(data: ByteArray): Reply {
require(data.size >= 141) { "file response length=${data.size}" }
val b = buffer(data)
val group = b.long; val server = b.long
require(b.get().toInt() == 4 && b.get().toInt() == 2) { "invalid file response protocol/kind" }
val command = b.get().toInt() and 255; val status = b.short.toInt() and 65535
val id = b.long; val owner = b.long
b.long; val size = b.long
b.position(16 + 77); val hash = ByteArray(32); b.get(hash)
b.position(16 + 119)
val path = b.short.toInt() and 65535; val target = b.short.toInt() and 65535; val payload = b.short.toInt() and 65535
require(path <= 1024 && target <= 1024 && payload <= 8192 && b.remaining() == path + target + payload) {
"invalid file response body lengths"
}
return Reply(id, group, server, owner, command, status, size, hash(hash))
}
fun transfer(data: ByteArray): Reply? {
require(data.size == 51) { "transfer length=${data.size}" }
val b = buffer(data)
val id = b.long; val group = b.long; val server = b.long; val owner = b.long
val done = b.long; val total = b.long; val status = b.short.toInt() and 65535
val state = b.get().toInt()
require(state in 1..2 && done >= 0 && total >= 0 && done <= total) { "invalid transfer progress" }
return if (state == 2) Reply(id, group, server, owner, status = status, size = total) else null
}
fun published(data: ByteArray): Reply {
require(data.size == 17 && data[16].toInt() in 0..1) { "invalid publication result" }
val b = buffer(data)
return Reply(b.long, b.long, status = b.get().toInt())
}
}
@@ -67,7 +67,7 @@ data class Message(val id: Long, val author: String, val text: String, val ts: L
val imageWidth: Int = 0, val imageHeight: Int = 0,
val videoWidth: Int = 0, val videoHeight: Int = 0, val videoDurationMs: Int = 0,
val downloadState: String = "", /* "", "downloading", "downloaded", "error" */
val deliveryStatusRes: Int = 0, val inviteGroupName: String? = null)
val deliveryStatusRes: Int = 0, val inviteGroupName: String? = null, val authorNodeId: Long = 0)
data class ChatMember(val nodeId: Long, val name: String, val online: Boolean = false,
val isAdmin: Boolean = false, val isSuper: Boolean = false,
@@ -79,7 +79,8 @@ data class ChatMember(val nodeId: Long, val name: String, val online: Boolean =
val directUp: Boolean = false, val indirectUp: Boolean = false,
val directPresence: Boolean = false, val indirectPresence: Boolean = false,
val bgpPresence: Boolean = false, val bgpRtt: Int = 0,
val bgpNodes: Int = 0, val connecting: Boolean = false) {
val bgpNodes: Int = 0, val connecting: Boolean = false,
val avatarServer: Long = 0, val avatarFile: String = "", val storageRole: Boolean = false) {
val flags: Int get() = (if (isSuper) 1 else 0) or (if (isAdmin) 2 else 0) or (if (isModer) 4 else 0)
}
@@ -195,7 +196,8 @@ fun MemberWire.toChatMember(previous: ChatMember = ChatMember(nodeId, name)): Ch
isAdmin = (flags and 2) != 0, isSuper = (flags and 1) != 0,
isModer = (flags and 4) != 0, isStorage = storage,
isDeleted = (flags and 8) != 0, radioActive = (flags and 0x10) != 0,
rtt = if (rtt == 0xFFFF) 0 else rtt, isSelf = isSelf
rtt = if (rtt == 0xFFFF) 0 else rtt, isSelf = isSelf,
avatarServer = avatarServer, avatarFile = avatarFile, storageRole = storageRole
)
fun cleanSocketName(raw: String): String =
@@ -381,6 +383,7 @@ class ChatRepository {
list.add(Message(
id = obj.getLong("id"),
author = obj.getString("author"),
authorNodeId = obj.optLong("authorNodeId"),
text = displayText,
ts = obj.getLong("ts"),
isOutgoing = obj.optBoolean("isOutgoing", false),
@@ -597,6 +600,7 @@ class ChatRepository {
list.add(Message(
id = obj.optLong("seq", 0),
author = author,
authorNodeId = obj.optLong("author", 0),
text = obj.optString("data", ""),
ts = obj.optLong("ts", 0),
isOutgoing = outgoing,
@@ -117,6 +117,19 @@ object NativeLib {
fun getChannelMembers(channelId: String): String = nativeGetChannelMembers(channelId) ?: "[]"
/** Асинхронный live-снимок; ответ — событие MEMBER_LIST. */
fun requestMemberList(channelId: String): Boolean = nativeRequestMemberList(channelId)
fun requestGroupAvatar(id: Long, group: Long): Boolean = nativeAvatarOperation(0, id, group, 0, 0, null, null, null)
fun transferAvatar(id: Long, group: Long, server: Long, owner: Long, remote: String, local: String,
upload: Boolean = false): Boolean =
nativeAvatarOperation(if (upload) 2 else 1, id, group, server, owner, remote, local, null)
fun avatarMkdir(id: Long, group: Long, server: Long, owner: Long): Boolean =
nativeAvatarOperation(3, id, group, server, owner, "avatars", null, null)
fun avatarStat(id: Long, group: Long, server: Long, owner: Long, remote: String): Boolean =
nativeAvatarOperation(4, id, group, server, owner, remote, null, null)
fun setMemberAvatar(id: Long, group: Long, server: Long, file: String): Boolean =
nativeAvatarOperation(5, id, group, server, 0, file, null, null)
fun setGroupAvatar(id: Long, group: Long, data: ByteArray): Boolean =
nativeAvatarOperation(6, id, group, 0, 0, null, null, data)
fun cancelAvatar(id: Long): Boolean = nativeAvatarOperation(7, id, 0, 0, 0, null, null, null)
fun getMemberDetail(channelId: String, nodeId: Long): String = nativeGetMemberDetail(channelId, nodeId) ?: "{}"
fun setMemberNick(channelId: String, nodeId: Long, nick: String) { nativeSetMemberNick(channelId, nodeId, nick) }
fun isChannelOwner(channelId: String): Boolean = nativeIsChannelOwner(channelId)
@@ -243,6 +256,8 @@ object NativeLib {
/* ── Channel members JNI ── */
private external fun nativeGetChannelMembers(channelId: String): String?
private external fun nativeRequestMemberList(channelId: String): Boolean
private external fun nativeAvatarOperation(op: Int, id: Long, group: Long, server: Long, owner: Long,
remote: String?, local: String?, data: ByteArray?): Boolean
private external fun nativeGetMemberDetail(channelId: String, nodeId: Long): String?
private external fun nativeSetMemberNick(channelId: String, nodeId: Long, nick: String)
private external fun nativeIsChannelOwner(channelId: String): Boolean