From 00fa5476b71931524291b9fe8eace72252169b99 Mon Sep 17 00:00:00 2001 From: Mykhailo Nester Date: Fri, 31 Jan 2025 15:55:55 +0200 Subject: [PATCH 1/3] - add new API request for ticket data; - provide ticket data with socket events; --- .../com/crowdin/platform/data/DataManager.kt | 4 ++ .../platform/data/model/TicketRequestBody.kt | 10 ++++ .../platform/data/model/TicketResponseBody.kt | 9 ++++ .../data/remote/CrowdingRepository.kt | 10 ++++ .../platform/data/remote/RemoteRepository.kt | 7 +++ .../platform/data/remote/api/CrowdinApi.kt | 7 +++ .../realtimeupdate/EchoWebSocketListener.kt | 52 ++++++++++++------- .../realtimeupdate/RealTimeUpdateManager.kt | 1 + .../platform/realtimeupdate/SocketEvents.kt | 31 ----------- .../platform/EchoWebSocketListenerTest.kt | 5 ++ .../com/crowdin/platform/SocketEventsTest.kt | 44 ---------------- 11 files changed, 87 insertions(+), 93 deletions(-) create mode 100644 crowdin/src/main/java/com/crowdin/platform/data/model/TicketRequestBody.kt create mode 100644 crowdin/src/main/java/com/crowdin/platform/data/model/TicketResponseBody.kt delete mode 100644 crowdin/src/main/java/com/crowdin/platform/realtimeupdate/SocketEvents.kt delete mode 100644 crowdin/src/test/java/com/crowdin/platform/SocketEventsTest.kt diff --git a/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt b/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt index f231e4bd..c30bede5 100644 --- a/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt +++ b/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt @@ -16,6 +16,7 @@ import com.crowdin.platform.data.model.ManifestData import com.crowdin.platform.data.model.PluralData import com.crowdin.platform.data.model.StringData import com.crowdin.platform.data.model.TextMetaData +import com.crowdin.platform.data.model.TicketResponseBody import com.crowdin.platform.data.remote.Connectivity import com.crowdin.platform.data.remote.NetworkType import com.crowdin.platform.data.remote.RemoteRepository @@ -266,4 +267,7 @@ internal class DataManager( return info } + + @WorkerThread + fun getTicket(event: String): TicketResponseBody? = remoteRepository.getTicket(event) } diff --git a/crowdin/src/main/java/com/crowdin/platform/data/model/TicketRequestBody.kt b/crowdin/src/main/java/com/crowdin/platform/data/model/TicketRequestBody.kt new file mode 100644 index 00000000..447c1f1a --- /dev/null +++ b/crowdin/src/main/java/com/crowdin/platform/data/model/TicketRequestBody.kt @@ -0,0 +1,10 @@ +package com.crowdin.platform.data.model + +internal data class TicketRequestBody( + val event: String, + val context: ContextData = ContextData(), +) + +internal data class ContextData( + val mode: String = "translate", +) diff --git a/crowdin/src/main/java/com/crowdin/platform/data/model/TicketResponseBody.kt b/crowdin/src/main/java/com/crowdin/platform/data/model/TicketResponseBody.kt new file mode 100644 index 00000000..6599a9a8 --- /dev/null +++ b/crowdin/src/main/java/com/crowdin/platform/data/model/TicketResponseBody.kt @@ -0,0 +1,9 @@ +package com.crowdin.platform.data.model + +internal data class TicketResponseBody( + val data: TicketData, +) + +internal data class TicketData( + val ticket: String, +) diff --git a/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt b/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt index f94f0027..063aa975 100644 --- a/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt +++ b/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt @@ -7,6 +7,8 @@ import com.crowdin.platform.data.LanguageDataCallback import com.crowdin.platform.data.model.LanguageInfo import com.crowdin.platform.data.model.LanguagesInfo import com.crowdin.platform.data.model.ManifestData +import com.crowdin.platform.data.model.TicketRequestBody +import com.crowdin.platform.data.model.TicketResponseBody import com.crowdin.platform.data.remote.api.CrowdinApi import com.crowdin.platform.data.remote.api.CrowdinDistributionApi import com.crowdin.platform.util.ThreadUtils @@ -94,6 +96,14 @@ internal abstract class CrowdingRepository( return info } + @WorkerThread + override fun getTicket(event: String): TicketResponseBody? { + Log.v(Crowdin.CROWDIN_TAG, "Get access to the websocket") + var result: TicketResponseBody? = null + executeIO { result = crowdinApi?.getTicket(TicketRequestBody(event))?.execute()?.body() } + return result + } + fun getLanguageInfo(sourceLanguage: String): LanguageInfo? { crowdinLanguages?.data?.forEach { val languageInfo = it.data diff --git a/crowdin/src/main/java/com/crowdin/platform/data/remote/RemoteRepository.kt b/crowdin/src/main/java/com/crowdin/platform/data/remote/RemoteRepository.kt index 31f82f8c..cb713c8b 100644 --- a/crowdin/src/main/java/com/crowdin/platform/data/remote/RemoteRepository.kt +++ b/crowdin/src/main/java/com/crowdin/platform/data/remote/RemoteRepository.kt @@ -5,6 +5,7 @@ import com.crowdin.platform.data.LanguageDataCallback import com.crowdin.platform.data.model.LanguageData import com.crowdin.platform.data.model.LanguagesInfo import com.crowdin.platform.data.model.ManifestData +import com.crowdin.platform.data.model.TicketResponseBody /** * Repository of strings from network. @@ -32,4 +33,10 @@ internal interface RemoteRepository { */ @WorkerThread fun getSupportedLanguages(): LanguagesInfo? + + /** + * Fetch ticket for WebSocket connection. + */ + @WorkerThread + fun getTicket(event: String): TicketResponseBody? } diff --git a/crowdin/src/main/java/com/crowdin/platform/data/remote/api/CrowdinApi.kt b/crowdin/src/main/java/com/crowdin/platform/data/remote/api/CrowdinApi.kt index 4b689962..6cdc0031 100644 --- a/crowdin/src/main/java/com/crowdin/platform/data/remote/api/CrowdinApi.kt +++ b/crowdin/src/main/java/com/crowdin/platform/data/remote/api/CrowdinApi.kt @@ -4,6 +4,8 @@ import com.crowdin.platform.data.model.BuildTranslationRequest import com.crowdin.platform.data.model.FileResponse import com.crowdin.platform.data.model.LanguagesInfo import com.crowdin.platform.data.model.ListScreenshotsResponse +import com.crowdin.platform.data.model.TicketRequestBody +import com.crowdin.platform.data.model.TicketResponseBody import com.crowdin.platform.data.model.TranslationResponse import okhttp3.RequestBody import okhttp3.ResponseBody @@ -80,4 +82,9 @@ internal interface CrowdinApi { fun getLanguagesInfo( @Query("limit") limit: Int = LANGUAGE_COUNT, ): Call + + @POST("/api/v2/user/websocket-ticket") + fun getTicket( + @Body body: TicketRequestBody, + ): Call } diff --git a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt index 71666eb9..9caaa80c 100644 --- a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt +++ b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt @@ -4,6 +4,7 @@ import android.icu.text.PluralRules import android.os.Build import android.util.Log import android.widget.TextView +import com.crowdin.platform.data.DataManager import com.crowdin.platform.data.getMappingValueForKey import com.crowdin.platform.data.model.LanguageData import com.crowdin.platform.data.model.TextMetaData @@ -23,7 +24,11 @@ import java.util.Collections import java.util.Locale import java.util.WeakHashMap +private const val UPDATE_DRAFT = "update-draft" +private const val TOP_SUGGESTION = "top-suggestion" + internal class EchoWebSocketListener( + private val dataManager: DataManager, private var mappingData: LanguageData, private var distributionData: DistributionInfoResponse.DistributionData, private var viewTransformerManager: ViewTransformerManager, @@ -41,7 +46,9 @@ internal class EchoWebSocketListener( val user = distributionData.user saveMatchedTextViewWithMappingId(mappingData) - subscribeViews(webSocket, project, user) + ThreadUtils.runInBackgroundPool({ + subscribeViews(webSocket, project, user) + }, false) viewTransformerManager.setOnViewsChangeListener( object : ViewsChangeListener { @@ -123,26 +130,35 @@ internal class EchoWebSocketListener( user: DistributionInfoResponse.DistributionData.UserData, mappingValue: String, ) { - webSocket.send( - SubscribeUpdateEvent( - project.wsHash, - project.id, - user.id, - languageCode, - mappingValue, - ).toString(), - ) + try { + val updateEvent = "$UPDATE_DRAFT:${project.wsHash}:${project.id}:${user.id}:$languageCode:$mappingValue" + dataManager.getTicket(updateEvent)?.data?.let { + webSocket.send(getSubscribeEventJson(updateEvent, it.ticket)) + } + } catch (e: Exception) { + Log.e("SubscribeView", "Get ticket for update event failed", e) + } - webSocket.send( - SubscribeSuggestionEvent( - project.wsHash, - project.id, - languageCode, - mappingValue, - ).toString(), - ) + try { + val suggestionEvent = "$TOP_SUGGESTION:${project.wsHash}:${project.id}:$languageCode:$mappingValue" + dataManager.getTicket(suggestionEvent)?.data?.let { + webSocket.send(getSubscribeEventJson(suggestionEvent, it.ticket)) + } + } catch (e: Exception) { + Log.e("SubscribeView", "Get ticket for suggestion event failed", e) + } } + private fun getSubscribeEventJson( + eventType: String, + ticket: String, + ): String = + "{" + + "\"action\":\"subscribe\", " + + "\"event\":\"$eventType\", " + + "\"ticket\": \"$ticket\"" + + "}" + private fun handleMessage(message: String?) { message?.let { val eventResponse = parseResponse(it) diff --git a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/RealTimeUpdateManager.kt b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/RealTimeUpdateManager.kt index c7d210c4..3fac476b 100644 --- a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/RealTimeUpdateManager.kt +++ b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/RealTimeUpdateManager.kt @@ -59,6 +59,7 @@ internal class RealTimeUpdateManager( val languageCode = getMatchedCode(it.languages, it.customLanguages) ?: return@let val listener = EchoWebSocketListener( + dataManager, mappingData, distributionData, viewTransformerManager, diff --git a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/SocketEvents.kt b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/SocketEvents.kt deleted file mode 100644 index 1c0b2f9f..00000000 --- a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/SocketEvents.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.crowdin.platform.realtimeupdate - -internal const val UPDATE_DRAFT = "update-draft" -internal const val TOP_SUGGESTION = "top-suggestion" - -internal class SubscribeUpdateEvent( - private var wsHash: String, - private var projectId: String, - private var userId: String, - private var language: String, - private var mappingId: String, -) { - override fun toString(): String = - "{" + - "\"action\":\"subscribe\", " + - "\"event\": \"$UPDATE_DRAFT:$wsHash:$projectId:$userId:$language:$mappingId\"" + - "}" -} - -internal class SubscribeSuggestionEvent( - private var wsHash: String, - private var projectId: String, - private var language: String, - private var mappingId: String, -) { - override fun toString(): String = - "{" + - "\"action\":\"subscribe\", " + - "\"event\": \"$TOP_SUGGESTION:$wsHash:$projectId:$language:$mappingId\"" + - "}" -} diff --git a/crowdin/src/test/java/com/crowdin/platform/EchoWebSocketListenerTest.kt b/crowdin/src/test/java/com/crowdin/platform/EchoWebSocketListenerTest.kt index bb30f395..2ac91553 100644 --- a/crowdin/src/test/java/com/crowdin/platform/EchoWebSocketListenerTest.kt +++ b/crowdin/src/test/java/com/crowdin/platform/EchoWebSocketListenerTest.kt @@ -1,5 +1,6 @@ package com.crowdin.platform +import com.crowdin.platform.data.DataManager import com.crowdin.platform.data.model.LanguageData import com.crowdin.platform.data.remote.api.DistributionInfoResponse import com.crowdin.platform.realtimeupdate.EchoWebSocketListener @@ -17,9 +18,11 @@ class EchoWebSocketListenerTest { // Given val mockMappingData = spy(LanguageData::class.java) val mockDistributionData = mock(DistributionInfoResponse.DistributionData::class.java) + val mockDataManager = mock(DataManager::class.java) val mockViewTransformerManager = spy(ViewTransformerManager::class.java) val echoWebSocketListener = EchoWebSocketListener( + mockDataManager, mockMappingData, mockDistributionData, mockViewTransformerManager, @@ -38,9 +41,11 @@ class EchoWebSocketListenerTest { // Given val mockMappingData = spy(LanguageData::class.java) val mockDistributionData = mock(DistributionInfoResponse.DistributionData::class.java) + val mockDataManager = mock(DataManager::class.java) val mockViewTransformerManager = spy(ViewTransformerManager::class.java) val echoWebSocketListener = EchoWebSocketListener( + mockDataManager, mockMappingData, mockDistributionData, mockViewTransformerManager, diff --git a/crowdin/src/test/java/com/crowdin/platform/SocketEventsTest.kt b/crowdin/src/test/java/com/crowdin/platform/SocketEventsTest.kt deleted file mode 100644 index bddb4e35..00000000 --- a/crowdin/src/test/java/com/crowdin/platform/SocketEventsTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.crowdin.platform - -import com.crowdin.platform.realtimeupdate.SubscribeSuggestionEvent -import com.crowdin.platform.realtimeupdate.SubscribeUpdateEvent -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test - -class SocketEventsTest { - @Test - fun updateEventTest() { - val wsHash = "wsHash" - val projectId = "projectId" - val userId = "userId" - val language = "EN" - val mappingId = "mappingId" - val expected = - "{" + - "\"action\":\"subscribe\", " + - "\"event\": \"update-draft:$wsHash:$projectId:$userId:$language:$mappingId\"" + - "}" - - val update = SubscribeUpdateEvent(wsHash, projectId, userId, language, mappingId) - - assertThat(update.toString(), `is`(expected)) - } - - @Test - fun suggestionEventTest() { - val wsHash = "wsHash" - val projectId = "projectId" - val language = "EN" - val mappingId = "mappingId" - val expected = - "{" + - "\"action\":\"subscribe\", " + - "\"event\": \"top-suggestion:$wsHash:$projectId:$language:$mappingId\"" + - "}" - - val update = SubscribeSuggestionEvent(wsHash, projectId, language, mappingId) - - assertThat(update.toString(), `is`(expected)) - } -} From f37afcc2fb66e68f691191a1bbddd0649ac3f9fd Mon Sep 17 00:00:00 2001 From: Mykhailo Nester Date: Fri, 31 Jan 2025 15:59:46 +0200 Subject: [PATCH 2/3] - update logs; --- .../com/crowdin/platform/data/remote/CrowdingRepository.kt | 1 - .../crowdin/platform/realtimeupdate/EchoWebSocketListener.kt | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt b/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt index 063aa975..dcec3aaf 100644 --- a/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt +++ b/crowdin/src/main/java/com/crowdin/platform/data/remote/CrowdingRepository.kt @@ -98,7 +98,6 @@ internal abstract class CrowdingRepository( @WorkerThread override fun getTicket(event: String): TicketResponseBody? { - Log.v(Crowdin.CROWDIN_TAG, "Get access to the websocket") var result: TicketResponseBody? = null executeIO { result = crowdinApi?.getTicket(TicketRequestBody(event))?.execute()?.body() } return result diff --git a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt index 9caaa80c..61920378 100644 --- a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt +++ b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt @@ -4,6 +4,7 @@ import android.icu.text.PluralRules import android.os.Build import android.util.Log import android.widget.TextView +import com.crowdin.platform.Crowdin import com.crowdin.platform.data.DataManager import com.crowdin.platform.data.getMappingValueForKey import com.crowdin.platform.data.model.LanguageData @@ -136,7 +137,7 @@ internal class EchoWebSocketListener( webSocket.send(getSubscribeEventJson(updateEvent, it.ticket)) } } catch (e: Exception) { - Log.e("SubscribeView", "Get ticket for update event failed", e) + Log.e(Crowdin.CROWDIN_TAG, "Get ticket for update event failed", e) } try { @@ -145,7 +146,7 @@ internal class EchoWebSocketListener( webSocket.send(getSubscribeEventJson(suggestionEvent, it.ticket)) } } catch (e: Exception) { - Log.e("SubscribeView", "Get ticket for suggestion event failed", e) + Log.e(Crowdin.CROWDIN_TAG, "Get ticket for suggestion event failed", e) } } From 480c7dc6fe88671d6aaaad6601af66feffff95e9 Mon Sep 17 00:00:00 2001 From: Mykhailo Nester Date: Sun, 16 Feb 2025 18:58:30 +0200 Subject: [PATCH 3/3] - cache subscription ticket; --- .../com/crowdin/platform/data/DataManager.kt | 39 ++++++++++++++++++- .../realtimeupdate/EchoWebSocketListener.kt | 9 +++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt b/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt index c30bede5..d66f6521 100644 --- a/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt +++ b/crowdin/src/main/java/com/crowdin/platform/data/DataManager.kt @@ -16,13 +16,13 @@ import com.crowdin.platform.data.model.ManifestData import com.crowdin.platform.data.model.PluralData import com.crowdin.platform.data.model.StringData import com.crowdin.platform.data.model.TextMetaData -import com.crowdin.platform.data.model.TicketResponseBody import com.crowdin.platform.data.remote.Connectivity import com.crowdin.platform.data.remote.NetworkType import com.crowdin.platform.data.remote.RemoteRepository import com.crowdin.platform.util.FeatureFlags import com.crowdin.platform.util.ThreadUtils import com.crowdin.platform.util.getFormattedCode +import com.google.gson.reflect.TypeToken import java.lang.reflect.Type import java.util.Locale @@ -42,6 +42,8 @@ internal class DataManager( const val SUPPORTED_LANGUAGES = "supported_languages" const val MANIFEST_DATA = "manifest_data" const val SYNC_DATA = "sync_data" + const val EVENT_TICKETS = "event_tickets" + const val EVENT_TICKETS_EXPIRATION = 1000 * 60 * 4 } private var loadingStateListeners: ArrayList? = null @@ -269,5 +271,38 @@ internal class DataManager( } @WorkerThread - fun getTicket(event: String): TicketResponseBody? = remoteRepository.getTicket(event) + fun getTicket(event: String): String? { + var ticketValue: String? = null + val type = object : TypeToken>() {}.type + var ticketsMap: MutableMap? = localRepository.getData(EVENT_TICKETS, type) + if (ticketsMap == null) { + ticketsMap = mutableMapOf() + } + + val ticketItem = ticketsMap[event] + if (ticketItem == null || ticketItem.isExpired()) { + Log.d(CROWDIN_TAG, "Ticket expired for event: $event") + remoteRepository.getTicket(event)?.data?.ticket?.let { + ticketsMap[event] = TicketItem(it, System.currentTimeMillis() + EVENT_TICKETS_EXPIRATION) + localRepository.saveData(EVENT_TICKETS, ticketsMap) + ticketValue = it + } + } else { + Log.d(CROWDIN_TAG, "Ticket not expired for event: $event") + ticketValue = ticketItem.ticket + } + + return ticketValue + } + + fun clearSocketData() { + localRepository.saveData(EVENT_TICKETS, null) + } + + data class TicketItem( + val ticket: String, + val expirationTime: Long, + ) { + fun isExpired(): Boolean = System.currentTimeMillis() > expirationTime + } } diff --git a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt index 61920378..4fdcaabe 100644 --- a/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt +++ b/crowdin/src/main/java/com/crowdin/platform/realtimeupdate/EchoWebSocketListener.kt @@ -88,6 +88,7 @@ internal class EchoWebSocketListener( code: Int, reason: String, ) { + dataManager.clearSocketData() dataHolderMap.clear() webSocket.close(NORMAL_CLOSURE_STATUS, reason) output("Closing : $code / $reason") @@ -133,8 +134,8 @@ internal class EchoWebSocketListener( ) { try { val updateEvent = "$UPDATE_DRAFT:${project.wsHash}:${project.id}:${user.id}:$languageCode:$mappingValue" - dataManager.getTicket(updateEvent)?.data?.let { - webSocket.send(getSubscribeEventJson(updateEvent, it.ticket)) + dataManager.getTicket(updateEvent)?.let { + webSocket.send(getSubscribeEventJson(updateEvent, it)) } } catch (e: Exception) { Log.e(Crowdin.CROWDIN_TAG, "Get ticket for update event failed", e) @@ -142,8 +143,8 @@ internal class EchoWebSocketListener( try { val suggestionEvent = "$TOP_SUGGESTION:${project.wsHash}:${project.id}:$languageCode:$mappingValue" - dataManager.getTicket(suggestionEvent)?.data?.let { - webSocket.send(getSubscribeEventJson(suggestionEvent, it.ticket)) + dataManager.getTicket(suggestionEvent)?.let { + webSocket.send(getSubscribeEventJson(suggestionEvent, it)) } } catch (e: Exception) { Log.e(Crowdin.CROWDIN_TAG, "Get ticket for suggestion event failed", e)