diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt index 49613e34..c7477e16 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt @@ -400,12 +400,17 @@ class AvatarRepository(context: Context) { return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: throw IOException("image decode failed") } private fun prune(root: File) { - val pinned = defaultAvatar?.substringAfter('/') + val pinned=mutableSetOf() + defaultAvatar?.substringAfter('/')?.takeIf { AvatarProtocol.validFile(it) }?.let { name -> + pinned+=name + try { val r=AvatarAssets.record(root,name); pinned+=r.original; pinned+=r.preview } + catch (e: Exception) { log("WARN", "Cannot pin profile bundle=$name: ${e.message}") } + } 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() } + val files = entries.filter { AvatarProtocol.validObject(it.name) }.sortedBy { it.lastModified() } var size = files.sumOf { it.length() } - for (file in files) if (size > 100L * 1024 * 1024 && file.name != pinned) { + for (file in files) if (size > 100L * 1024 * 1024 && file.name !in pinned) { val bytes = file.length() if (file.delete()) size -= bytes else log("WARN", "Cache eviction failed file=$file") } @@ -420,25 +425,20 @@ class AvatarRepository(context: Context) { ensureGroup(group, true) } - fun editProfile(server: Long, uri: Uri?, expectedAccount: Long) = edit("profile", expectedAccount) { epoch -> + fun editProfile(server: Long, source: File?, expectedAccount: Long, crop: AvatarCrop? = null) = edit("profile", expectedAccount) { epoch -> val uid = account - val bytes = if (uri == null) null else prepare(uri, false) + val prepared=if (source==null) null else try { + withContext(Dispatchers.IO) { AvatarAssets.prepare(source,directory(uid),requireNotNull(crop)) } + } finally { withContext(NonCancellable+Dispatchers.IO) { + if (!source.delete()) log("WARN", "Cannot remove editor source=$source") + } } alive(epoch) - if (bytes != null) { + if (prepared != null) { require(server != 0L && storageServers().any { it.nodeId == server }) { "no eligible storage server" } - val file = "${digest(bytes)}.png" - withContext(Dispatchers.IO) { - val root = directory(uid) - if (!root.isDirectory && !root.mkdirs()) throw IOException("cannot create profile cache") - val temp = File(root, ".profile-${ids.incrementAndGet()}") - try { - temp.writeBytes(bytes) - if (!temp.renameTo(File(root, file))) throw IOException("cannot publish profile cache") - } finally { if (temp.exists() && !temp.delete()) log("WARN", "Cannot remove profile temp=$temp") } - } + val (file,bitmap)=prepared alive(epoch) defaultAvatar = "${AvatarProtocol.hex(server)}/$file" - cache.put(file, withContext(Dispatchers.IO) { decode(bytes, file.removeSuffix(".png"), false) }) + cache.put(file,bitmap) } else defaultAvatar = "-" alive(epoch) val desired = defaultAvatar @@ -486,18 +486,25 @@ class AvatarRepository(context: Context) { foreground.first { it } transfers.withPermit { if (server != 0L) { - val local = File(directory(uid), file) - withContext(Dispatchers.IO) { decode(readImage(local), file.removeSuffix(".png"), false) } + val root=directory(uid) + val record=withContext(Dispatchers.IO) { AvatarAssets.record(root,file) } 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") }) - if (stat.status == 2) { - val upload = AvatarProtocol.transfer(request(50, group, server, uid) { id, gid -> - NativeLib.transferAvatar(id, gid, server, uid, "avatars/$file", local.path, true) - }) ?: throw IOException("missing upload result") - require(upload.status == 0) { "upload status=${upload.status}" } - } else require(stat.status == 0 && stat.fileType == 1 && stat.size == local.length()) { - "stat status=${stat.status} type=${stat.fileType} bytes=${stat.size} expected=${local.length()}" + for (name in listOf(record.original,record.preview,file)) { + val local=File(root,name) + val limit=if (name.endsWith(".json")) AvatarProtocol.MAX_MANIFEST else AvatarProtocol.MAX_ORIGINAL + withContext(Dispatchers.IO) { AvatarAssets.read(local,name,limit) } + val stat=AvatarProtocol.response(request(49,group,server,uid,2) { id,gid -> + NativeLib.avatarStat(id,gid,server,uid,"avatars/$name") + }) + if (stat.status==2) { + val upload=AvatarProtocol.transfer(request(50,group,server,uid) { id,gid -> + NativeLib.transferAvatar(id,gid,server,uid,"avatars/$name",local.path,true,limit) + }) ?: throw IOException("missing upload result") + require(upload.status==0) { "upload file=$name status=${upload.status}" } + } else require(stat.status==0 && stat.fileType==1 && stat.size==local.length()) { + "stat file=$name status=${stat.status} bytes=${stat.size} expected=${local.length()}" + } } } alive(epoch)