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 dadf3b23..9e910e1b 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 @@ -234,7 +234,7 @@ class AvatarRepository(context: Context) { 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 cache.get(groupKey(group, state)) ?: withContext(Dispatchers.IO) { - decode(record.image, record.hash, true) + decodeGroup(record.image, record.hash) } alive(epoch) if (!_groups.value.containsKey(group)) return@launch @@ -383,30 +383,15 @@ class AvatarRepository(context: Context) { } } - 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" } + private fun decodeGroup(bytes: ByteArray, hash: String): Bitmap { + require(bytes.size in 1..AvatarProtocol.MAX_GROUP) { "image byte limit" } require(digest(bytes) == hash) { "image SHA256 mismatch" } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) - val edge = if (group) 128 else 512 + val edge = 128 require(options.outWidth in 1..edge && options.outHeight == options.outWidth && - options.outMimeType == if (group) "image/jpeg" else "image/png") { + options.outMimeType == "image/jpeg") { "invalid image ${options.outMimeType} ${options.outWidth}x${options.outHeight} limit=$edge" } return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: throw IOException("image decode failed") @@ -429,7 +414,7 @@ class AvatarRepository(context: Context) { } fun editGroup(group: String, uri: Uri?, expectedAccount: Long) = edit("group:$group", expectedAccount) { epoch -> - val bytes = if (uri == null) ByteArray(0) else prepare(uri, true) + val bytes = if (uri == null) ByteArray(0) else prepareGroup(uri) alive(epoch) require(editTokens["group:$group"] == epoch) { "group edit superseded" } val reply = AvatarProtocol.published(request(52, group) { id, gid -> NativeLib.setGroupAvatar(id, gid, bytes) }) @@ -466,14 +451,14 @@ class AvatarRepository(context: Context) { if (account == 0L) start() if (expectedAccount == 0L || expectedAccount != account) { log("WARN", "Ignored stale picker result target=$key expected=${AvatarProtocol.hex(expectedAccount)} actual=${AvatarProtocol.hex(account)}") - rejectedSource?.let { file -> scope.launch(Dispatchers.IO) { + rejectedSource?.let { file -> scope.launch(Dispatchers.IO+NonCancellable) { if (!file.delete()) log("WARN", "Cannot remove stale editor source=$file") } } return } if (account == 0L || _edits.value[key] == AvatarEditState.WORKING) { log("WARN", "Rejected concurrent avatar edit target=$key") - rejectedSource?.let { file -> scope.launch(Dispatchers.IO) { + rejectedSource?.let { file -> scope.launch(Dispatchers.IO+NonCancellable) { if (!file.delete()) log("WARN", "Cannot remove rejected editor source=$file") } } return @@ -551,7 +536,7 @@ class AvatarRepository(context: Context) { } /** Чтение URI, EXIF, crop и кодирование выполняются вне UI и потока ядра. */ - private suspend fun prepare(uri: Uri, group: Boolean): ByteArray = preparations.withPermit { withContext(Dispatchers.IO) { + private suspend fun prepareGroup(uri: Uri): ByteArray = preparations.withPermit { withContext(Dispatchers.IO) { val temp = File.createTempFile("avatar-source-", ".image", app.cacheDir) try { app.contentResolver.openInputStream(uri)?.use { input -> @@ -571,15 +556,15 @@ class AvatarRepository(context: Context) { val decoded = PhotoUtil.decodeOriented(temp.path, sample) ?: throw IOException("source decode failed") val edge = minOf(decoded.width, decoded.height) val crop = Bitmap.createBitmap(decoded, (decoded.width - edge) / 2, (decoded.height - edge) / 2, edge, edge) - val scaled = Bitmap.createScaledBitmap(crop, if (group) 128 else 256, if (group) 128 else 256, true) + val scaled = Bitmap.createScaledBitmap(crop,128,128,true) try { - for (quality in if (group) listOf(85, 70, 50, 35, 20) else listOf(100)) { + for (quality in listOf(85,70,50,35,20)) { val output = java.io.ByteArrayOutputStream() - require(scaled.compress(if (group) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, quality, output)) { + require(scaled.compress(Bitmap.CompressFormat.JPEG,quality,output)) { "avatar compression failed" } val bytes = output.toByteArray() - if (bytes.size <= if (group) AvatarProtocol.MAX_GROUP else AvatarProtocol.MAX_USER) return@withContext bytes + if (bytes.size<=AvatarProtocol.MAX_GROUP) return@withContext bytes } throw IOException("encoded avatar exceeds limit") } finally {