From bb116bccb4b5270c7bb48300798ccdb9380903ac Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 30 Dec 2022 01:27:08 +0300 Subject: [PATCH] android: Fallback to manual parsing of apiChats and apiChat responses (#1660) * android: Fallback to manual parsing of apiChats and apiChat responses * Different icon * eol --- .../java/chat/simplex/app/model/ChatModel.kt | 55 ++++++++++++++++++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 41 +++++++++++++- .../app/views/chat/item/CIInvalidJSONView.kt | 46 ++++++++++++++++ .../app/views/chat/item/ChatItemView.kt | 1 + .../app/views/chatlist/ChatListNavLinkView.kt | 40 ++++++++++++++ .../app/src/main/res/values/strings.xml | 2 + 6 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 439d267de7..e3dcc4911b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -4,8 +4,6 @@ import android.net.Uri import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.Check -import androidx.compose.material.icons.outlined.Close import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @@ -26,6 +24,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* import java.io.File +import kotlin.random.Random import kotlin.time.* /* @@ -557,6 +556,30 @@ sealed class ChatInfo: SomeChat, NamedChat { ContactConnection(PendingContactConnection.getSampleData(status, viaContactUri)) } } + + @Serializable @SerialName("invalidJSON") + class InvalidJSON(val json: String): ChatInfo() { + override val chatType get() = ChatType.Direct + override val localDisplayName get() = invalidChatName + override val id get() = "" + override val apiId get() = 0L + override val ready get() = false + override val sendMsgEnabled get() = false + override val ntfsEnabled get() = false + override val incognito get() = false + override fun featureEnabled(feature: ChatFeature) = false + override val timedMessagesTTL: Int? get() = null + override val createdAt get() = Clock.System.now() + override val updatedAt get() = Clock.System.now() + override val displayName get() = invalidChatName + override val fullName get() = invalidChatName + override val image get() = null + override val localAlias get() = "" + + companion object { + private val invalidChatName = generalGetString(R.string.invalid_chat) + } + } } @Serializable @@ -1168,6 +1191,7 @@ data class ChatItem ( is CIContent.SndGroupFeature -> showNtfDir is CIContent.RcvChatFeatureRejected -> showNtfDir is CIContent.RcvGroupFeatureRejected -> showNtfDir + is CIContent.InvalidJSON -> false } fun withStatus(status: CIStatus): ChatItem = this.copy(meta = meta.copy(itemStatus = status)) @@ -1275,6 +1299,15 @@ data class ChatItem ( quotedItem = null, file = null ) + + fun invalidJSON(json: String): ChatItem = + ChatItem( + chatDir = CIDirection.DirectSnd(), + meta = CIMeta.invalidJSON(), + content = CIContent.InvalidJSON(json), + quotedItem = null, + file = null + ) } } @@ -1341,6 +1374,22 @@ data class CIMeta ( itemLive = itemLive, editable = editable ) + + fun invalidJSON(): CIMeta = + CIMeta( + // itemId can not be the same for different items, otherwise ChatView will crash + itemId = Random.nextLong(-1000000L, -1000L), + itemTs = Clock.System.now(), + itemText = "invalid JSON", + itemStatus = CIStatus.SndNew(), + createdAt = Clock.System.now(), + updatedAt = Clock.System.now(), + itemDeleted = false, + itemEdited = false, + itemTimed = null, + itemLive = false, + editable = false + ) } } @@ -1405,6 +1454,7 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null } override val text: String get() = when (this) { is SndMsgContent -> msgContent.text @@ -1428,6 +1478,7 @@ sealed class CIContent: ItemContent { is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param) is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}" is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}" + is InvalidJSON -> "invalid data" } companion object { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 9548bf24ed..82505b3d72 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -33,8 +33,9 @@ import kotlinx.coroutines.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.json.* import java.util.Date typealias ChatCtrl = Long @@ -2581,8 +2582,29 @@ class APIResponse(val resp: CR, val corr: String? = null) { try { Log.d(TAG, e.localizedMessage ?: "") val data = json.parseToJsonElement(str).jsonObject + val resp = data["resp"]!!.jsonObject + val type = resp["type"]?.jsonPrimitive?.content ?: "invalid" + try { + if (type == "apiChats") { + val chats: List = resp["chats"]!!.jsonArray.map { + parseChatData(it) + } + return APIResponse( + resp = CR.ApiChats(chats), + corr = data["corr"]?.toString() + ) + } else if (type == "apiChat") { + val chat = parseChatData(resp["chat"]!!) + return APIResponse( + resp = CR.ApiChat(chat), + corr = data["corr"]?.toString() + ) + } + } catch (e: Exception) { + Log.e(TAG, "Error while parsing chat(s): " + e.stackTraceToString()) + } APIResponse( - resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)), + resp = CR.Response(type, json.encodeToString(data)), corr = data["corr"]?.toString() ) } catch(e: Exception) { @@ -2593,6 +2615,19 @@ class APIResponse(val resp: CR, val corr: String? = null) { } } +private fun parseChatData(chat: JsonElement): Chat { + val chatInfo: ChatInfo = decodeObject(ChatInfo.serializer(), chat.jsonObject["chatInfo"]) + ?: ChatInfo.InvalidJSON(json.encodeToString(chat.jsonObject["chatInfo"])) + val chatStats = decodeObject(Chat.ChatStats.serializer(), chat.jsonObject["chatStats"])!! + val chatItems: List = chat.jsonObject["chatItems"]!!.jsonArray.map { + decodeObject(ChatItem.serializer(), it) ?: ChatItem.invalidJSON(json.encodeToString(it)) + } + return Chat(chatInfo, chatItems, chatStats) +} + +private fun decodeObject(deserializer: DeserializationStrategy, obj: JsonElement?): T? = + runCatching { json.decodeFromJsonElement(deserializer, obj!!) }.getOrNull() + // ChatResponse @Serializable sealed class CR { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt new file mode 100644 index 0000000000..51a6d92bce --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt @@ -0,0 +1,46 @@ +package chat.simplex.app.views.chat.item + +import SectionSpacer +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.ui.theme.DEFAULT_PADDING +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.SettingsActionItem + +@Composable +fun CIInvalidJSONView(json: String) { + Row(Modifier + .clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } } + .padding(horizontal = 10.dp, vertical = 6.dp) + ) { + Text(stringResource(R.string.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic) + } +} + +@Composable +fun InvalidJSONView(json: String) { + Column { + Spacer(Modifier.height(DEFAULT_PADDING)) + SectionView { + val context = LocalContext.current + SettingsActionItem(Icons.Outlined.Share, generalGetString(R.string.share_verb), click = { + shareText(context, json) + }) + } + Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) { + Text(json) + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 64fd1d935c..1eb4f21cee 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -235,6 +235,7 @@ fun ChatItemView( is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red) is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red) + is CIContent.InvalidJSON -> CIInvalidJSONView(c.json) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 6596b45054..ff88b3762b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -11,15 +11,20 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.* import chat.simplex.app.views.chat.group.deleteGroupDialog import chat.simplex.app.views.chat.group.leaveGroupDialog +import chat.simplex.app.views.chat.item.InvalidJSONView import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.ContactConnectionInfoView @@ -75,6 +80,18 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { showMenu, stopped ) + is ChatInfo.InvalidJSON -> + ChatListNavLinkLayout( + chatLinkPreview = { + InvalidDataView() + }, + click = { + ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) } + }, + dropdownMenuItems = null, + showMenu, + stopped + ) } } @@ -320,6 +337,29 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ) } +@Composable +private fun InvalidDataView() { + Row { + ProfileImage(72.dp, null, Icons.Filled.AccountCircle, HighOrLowlight) + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .weight(1F) + ) { + Text( + stringResource(R.string.invalid_data), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Bold, + color = Color.Red + ) + val height = with(LocalDensity.current) { 46.sp.toDp() } + Spacer(Modifier.height(height)) + } + } +} + fun markChatRead(c: Chat, chatModel: ChatModel) { var chat = c withApi { diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index e60c9c791a..7fd343c223 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -28,6 +28,8 @@ unknown message format invalid message format LIVE + invalid chat + invalid data connection %1$d