diff --git a/application/webserver/build.gradle b/application/webserver/build.gradle index bf3bb105c..1820f7425 100644 --- a/application/webserver/build.gradle +++ b/application/webserver/build.gradle @@ -84,7 +84,6 @@ dependencies { implementation project(':application:vlc-android') implementation "io.ktor:ktor:$rootProject.ext.ktorVersion" implementation "io.ktor:ktor-server-netty:$rootProject.ext.ktorVersion" - implementation "io.ktor:ktor-gson:1.6.8" implementation "io.ktor:ktor-server-websockets:$rootProject.ext.ktorVersion" implementation "io.ktor:ktor-server-caching-headers:$rootProject.ext.ktorVersion" implementation "io.ktor:ktor-server-cors:$rootProject.ext.ktorVersion" diff --git a/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessRouting.kt b/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessRouting.kt index f64b6ab9c..243dd2c6b 100644 --- a/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessRouting.kt +++ b/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessRouting.kt @@ -33,7 +33,11 @@ import android.util.Log import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.LiveData -import com.google.gson.Gson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import io.ktor.http.ContentDisposition import io.ktor.http.ContentType import io.ktor.http.HttpHeaders @@ -64,8 +68,6 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import org.json.JSONArray -import org.json.JSONObject import org.videolan.medialibrary.MLServiceLocator import org.videolan.medialibrary.interfaces.Medialibrary import org.videolan.medialibrary.interfaces.media.Album @@ -146,15 +148,9 @@ import java.io.FileOutputStream import java.io.IOException import java.io.OutputStreamWriter import java.text.DateFormat +import java.util.Date import java.util.Locale - -private val format by lazy { - object : ThreadLocal() { - override fun initialValue() = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault()) - } -} - /** * Setup the server routing * @@ -255,21 +251,13 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { call.respond(HttpStatusCode.Forbidden) return@get } - val logs = getLogsFiles().sortedBy { File(it.path).lastModified() }.reversed() - - val jsonArray = JSONArray() - for (log in logs) { - val json = JSONObject() - json.put("path", log.path) - json.put("date", format.get()?.format(File(log.path).lastModified())) - json.put("type", log.type) - jsonArray.put(json) - } - call.respondJson(jsonArray.toString()) + val logs = getLogsFiles().sortedByDescending { it.date } + + call.respondJson(convertToJson(logs)) } // Get the translation string list get("/translation") { - call.respondJson(TranslationMapping.generateTranslations(appContext.getContextWithLocale(AppContextProvider.locale))) + call.respondJson(convertToJson(TranslationMapping.generateTranslations(appContext.getContextWithLocale(AppContextProvider.locale)))) } get("/secure-url") { call.respondText(RemoteAccessServer.getInstance(appContext).getSecureUrl(call)) @@ -442,7 +430,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { } val result = RemoteAccessServer.VideoListResult(list, groupTitle) - call.respondJson(Gson().toJson(result)) + call.respondJson(convertToJson(result)) } get("/longpolling") { //Empty the queue if needed @@ -450,27 +438,27 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { val queue = mutableListOf().apply { RemoteAccessWebSockets.messageQueue.drainTo(this) } - call.respondJson(Gson().toJson(queue)) + call.respondJson(convertToJson(queue)) return@get } //block the request until a message is received // The 3 second timeout is to avoid blocking forever try { val message = withTimeout(3000) { RemoteAccessWebSockets.onPlaybackEventChannel.receive() } - if (message.contains("\"type\":\"browser-description\"")) { - call.respondJson("[$message]") + if (message.type == RemoteAccessServer.WSMessageType.BROWSER_DESCRIPTION) { + call.respondJson(convertToJson(listOf(message))) return@get } } catch (e: TimeoutCancellationException) { // Fall through to the next block of code } val remoteAccessServer = RemoteAccessServer.getInstance(appContext) - val messages = arrayListOf() - remoteAccessServer.generatePlayQueue()?.let { playQueue -> messages.add(Gson().toJson(playQueue)) } - val isPlaying = PlaylistManager.showAudioPlayer.value ?: false - messages.add(Gson().toJson(PlayerStatus(isPlaying))) - remoteAccessServer.generateNowPlaying()?.let { it1 -> messages.add(Gson().toJson(it1)) } - call.respondJson("[${messages.joinToString(",")}]") + val messages = listOfNotNull( + remoteAccessServer.generatePlayQueue(), + PlayerStatus(PlaylistManager.showAudioPlayer.value ?: false), + remoteAccessServer.generateNowPlaying() + ) + call.respondJson(convertToJson(messages)) } // Manage playback events get("/playback-event") { @@ -499,7 +487,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { albums.forEach { album -> list.add(album.toPlayQueueItem()) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // List of all the artists get("/artist-list") { @@ -514,7 +502,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { artists.forEach { artist -> list.add(artist.toPlayQueueItem(appContext)) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // List of all the audio tracks get("/track-list") { @@ -529,7 +517,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { tracks.forEach { track -> list.add(track.toPlayQueueItem(defaultArtist = appContext.getString(R.string.unknown_artist))) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // List of all the audio genres get("/genre-list") { @@ -544,7 +532,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { genres.forEach { genre -> list.add(genre.toPlayQueueItem(appContext)) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // Get an album details get("/album") { @@ -562,7 +550,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { list.add(track.toPlayQueueItem(album.albumArtist)) } val result = RemoteAccessServer.AlbumResult(list, album.title) - call.respondJson(Gson().toJson(result)) + call.respondJson(convertToJson(result)) } // Get a genre details get("/genre") { @@ -580,7 +568,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { list.add(track.toPlayQueueItem()) } val result = RemoteAccessServer.AlbumResult(list, genre.title) - call.respondJson(Gson().toJson(result)) + call.respondJson(convertToJson(result)) } // Get an playlist details get("/playlist") { @@ -600,7 +588,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { }) } val result = RemoteAccessServer.PlaylistResult(list, playlist.title) - call.respondJson(Gson().toJson(result)) + call.respondJson(convertToJson(result)) } // Create a new playlist post("/playlist-create") { @@ -700,7 +688,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { list.add(album.toPlayQueueItem()) } val result = RemoteAccessServer.ArtistResult(list, listOf(), artist.title) - call.respondJson(Gson().toJson(result)) + call.respondJson(convertToJson(result)) } // List of all the playlists get("/playlist-list") { @@ -715,7 +703,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { playlists.forEach { playlist -> list.add(playlist.toPlayQueueItem(appContext)) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // Search media get("/search") { @@ -742,11 +730,11 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { result.tracks?.filterNotNull()?.map { it.toPlayQueueItem() } ?: listOf(), ) - call.respondJson(Gson().toJson(results)) + call.respondJson(convertToJson(results)) } } - call.respondJson(Gson().toJson(RemoteAccessServer.SearchResults(listOf(), listOf(), listOf(), listOf(), listOf(), listOf()))) + call.respondJson(convertToJson(RemoteAccessServer.SearchResults(listOf(), listOf(), listOf(), listOf(), listOf(), listOf()))) } // List of all the file storages get("/storage-list") { @@ -769,7 +757,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { call.respond(HttpStatusCode.InternalServerError) return@get } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // List of all the file favorites get("/favorite-list") { @@ -789,7 +777,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { call.respond(HttpStatusCode.InternalServerError) return@get } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } get("/history") { verifyLogin(settings) @@ -811,7 +799,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { call.respond(HttpStatusCode.InternalServerError) return@get } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } // List of all the network shares get("/network-list") { @@ -826,7 +814,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { list.add(RemoteAccessServer.PlayQueueItem(3000L + index, mediaLibraryItem.title, " ", 0, mediaLibraryItem.artworkMrl ?: "", false, "", (mediaLibraryItem as MediaWrapper).uri.toString(), true, favorite = mediaLibraryItem.isFavorite)) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } get("/stream-list") { verifyLogin(settings) @@ -838,7 +826,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { list.add(RemoteAccessServer.PlayQueueItem(3000L + index, mediaLibraryItem.title, " ", 0, mediaLibraryItem.artworkMrl ?: "", false, "", (mediaLibraryItem as MediaWrapper).uri.toString(), true, favorite = mediaLibraryItem.isFavorite)) } - call.respondJson(Gson().toJson(list)) + call.respondJson(convertToJson(list)) } //list of folders and files in a path get("/browse-list") { @@ -912,7 +900,7 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { } val result = RemoteAccessServer.BrowsingResult(list, breadcrumbItems) - call.respondJson(Gson().toJson(result)) + call.respondJson(convertToJson(result)) } // Resume playback get("/resume-playback") { @@ -1377,21 +1365,22 @@ private suspend fun getLogsFiles(): List = withContext(Dispatchers.IO) val folder = File(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY) val files = folder.listFiles() files.forEach { - if (it.isFile && it.name.startsWith("vlc_logcat_")) result.add(LogFile(it.path, if (it.name.startsWith("vlc_logcat_remote_access")) "web" else "device")) + if (it.isFile && it.name.startsWith("vlc_logcat_")) + result.add(LogFile(it.path, if (it.name.startsWith("vlc_logcat_remote_access")) "web" else "device", Date(it.lastModified()))) } val crashFolder = File(AppContextProvider.appContext.getExternalFilesDir(null)!!.absolutePath ) val crashFiles = crashFolder.listFiles() crashFiles.forEach { - if (it.isFile && it.name.startsWith("vlc_crash")) result.add(LogFile(it.path, "crash")) + if (it.isFile && it.name.startsWith("vlc_crash")) + result.add(LogFile(it.path, "crash", Date(it.lastModified()))) } return@withContext result } -data class LogFile(val path:String, val type:String) - +data class LogFile(val path: String, val type: String, val date: Date) private suspend fun getMediaFromProvider(provider: BrowserProvider, dataset: LiveDataset): Pair, ArrayList>> { dataset.await() @@ -1505,6 +1494,46 @@ private suspend fun getProviderContent(context:Context, provider: BrowserProvide return list } +fun convertToJson(data: Any?): String { + if (data == null) return "{}" + val moshi = Moshi.Builder().build() + val adapter = moshi.adapter(data::class.java) + return adapter.toJson(data) +} + +inline fun convertToJson(data: Map?): String { + val moshi = Moshi.Builder().build() + val type = Types.newParameterizedType(MutableMap::class.java, K::class.java, V::class.java) + val adapter = moshi.adapter>(type).nullSafe() + return adapter.toJson(data) +} + +inline fun convertToJson(data: List?): String { + val moshi = Moshi.Builder() + .add(Date::class.java, FormattedDateJsonAdapter().nullSafe()) + .build() + val type = Types.newParameterizedType(MutableList::class.java, Any::class.java) + val adapter = moshi.adapter>(type).nullSafe() + return adapter.toJson(data) +} + +private val format by lazy { + object : ThreadLocal() { + override fun initialValue() = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault()) + } +} + +class FormattedDateJsonAdapter : JsonAdapter() { + override fun fromJson(reader: JsonReader): Date? { + val string = reader.nextString() + return format.get().parse(string) + } + override fun toJson(writer: JsonWriter, value: Date?) { + val string = format.get().format(value) + writer.value(string) + } +} + private suspend fun ApplicationCall.respondJson(text: String, status: HttpStatusCode? = null, configure: OutgoingContent.() -> Unit = {}) { respond(TextContent(text, ContentType.Application.Json, status).apply(configure)) } diff --git a/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessServer.kt b/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessServer.kt index 322e623f1..a55bd6b27 100644 --- a/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessServer.kt +++ b/application/webserver/src/main/java/org/videolan/vlc/webserver/RemoteAccessServer.kt @@ -32,6 +32,7 @@ import android.net.Uri import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.squareup.moshi.Json import io.ktor.http.CacheControl import io.ktor.http.ContentType import io.ktor.http.HttpHeaders @@ -800,26 +801,26 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac return list } - abstract class WSMessage(val type: String) + abstract class WSMessage(val type: WSMessageType) data class NowPlaying(val title: String, val artist: String, val playing: Boolean, val isVideoPlaying: Boolean, val progress: Long, val duration: Long, val id: Long, val artworkURL: String, val uri: String, val volume: Int, val speed: Float, val sleepTimer: Long, val waitForMediaEnd:Boolean, val resetOnInteraction:Boolean, val shuffle: Boolean, val repeat: Int, val shouldShow: Boolean = PlaylistManager.playingState.value ?: false, - val bookmarks:List = listOf(), val chapters:List = listOf()) : WSMessage("now-playing") + val bookmarks: List = listOf(), val chapters: List = listOf()) : WSMessage(WSMessageType.NOW_PLAYING) data class WSBookmark(val id:Long, val title: String, val time: Long) data class WSChapter(val title: String, val time: Long) - data class PlayQueue(val medias: List) : WSMessage("play-queue") + data class PlayQueue(val medias: List) : WSMessage(WSMessageType.PLAY_QUEUE) data class PlayQueueItem(val id: Long, val title: String, val artist: String, val duration: Long, val artworkURL: String, val playing: Boolean, val resolution: String = "", val path: String = "", val isFolder: Boolean = false, val progress: Long = 0L, val played: Boolean = false, var fileType: String = "", val favorite: Boolean = false) - data class WebSocketAuthorization(val status:String, val initialMessage:String) : WSMessage("auth") - data class Volume(val volume: Int) : WSMessage("volume") - data class PlayerStatus(val playing: Boolean) : WSMessage("player-status") - data class LoginNeeded(val dialogOpened: Boolean) : WSMessage("login-needed") - data class ResumeConfirmationNeeded(val mediaTitle: String?, val consumed:Boolean) : WSMessage("resume-confirmation") - data class MLRefreshNeeded(val refreshNeeded: Boolean = true) : WSMessage("ml-refresh-needed") - data class BrowserDescription(val path: String, val description:String) : WSMessage("browser-description") - data class PlaybackControlForbidden(val forbidden: Boolean = true): WSMessage("playback-control-forbidden") + data class WebSocketAuthorization(val status:String, val initialMessage:String) : WSMessage(WSMessageType.AUTH) + data class Volume(val volume: Int) : WSMessage(WSMessageType.VOLUME) + data class PlayerStatus(val playing: Boolean) : WSMessage(WSMessageType.PLAYER_STATUS) + data class LoginNeeded(val dialogOpened: Boolean) : WSMessage(WSMessageType.LOGIN_NEEDED) + data class ResumeConfirmationNeeded(val mediaTitle: String?, val consumed: Boolean) : WSMessage(WSMessageType.RESUME_CONFIRMATION) + data class MLRefreshNeeded(val refreshNeeded: Boolean = true) : WSMessage(WSMessageType.ML_REFRESH_NEEDED) + data class BrowserDescription(val path: String, val description: String) : WSMessage(WSMessageType.BROWSER_DESCRIPTION) + data class PlaybackControlForbidden(val forbidden: Boolean = true): WSMessage(WSMessageType.PLAYBACK_CONTROL_FORBIDDEN) data class SearchResults(val albums: List, val artists: List, val genres: List, val playlists: List, val videos: List, val tracks: List) data class BreadcrumbItem(val title: String, val path: String) data class BrowsingResult(val content: List, val breadcrumb: List) @@ -846,4 +847,26 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac data class RemoteAccessConnection(val ip: String) + enum class WSMessageType { + @Json(name = "now-playing") + NOW_PLAYING, + @Json(name = "play-queue") + PLAY_QUEUE, + @Json(name = "auth") + AUTH, + @Json(name = "volume") + VOLUME, + @Json(name = "player-status") + PLAYER_STATUS, + @Json(name = "login-needed") + LOGIN_NEEDED, + @Json(name = "resume-confirmation") + RESUME_CONFIRMATION, + @Json(name = "ml-refresh-needed") + ML_REFRESH_NEEDED, + @Json(name = "browser-description") + BROWSER_DESCRIPTION, + @Json(name = "playback-control-forbidden") + PLAYBACK_CONTROL_FORBIDDEN + } } \ No newline at end of file diff --git a/application/webserver/src/main/java/org/videolan/vlc/webserver/TranslationMapping.kt b/application/webserver/src/main/java/org/videolan/vlc/webserver/TranslationMapping.kt index 154afb37c..900348ce2 100644 --- a/application/webserver/src/main/java/org/videolan/vlc/webserver/TranslationMapping.kt +++ b/application/webserver/src/main/java/org/videolan/vlc/webserver/TranslationMapping.kt @@ -27,11 +27,10 @@ package org.videolan.vlc.webserver import android.content.Context import android.os.Build import androidx.annotation.StringRes -import org.json.JSONObject import org.videolan.vlc.BuildConfig object TranslationMapping { - fun generateTranslations(context: Context): String { + fun generateTranslations(context: Context): Map { val map = HashMap() StringMapping.values().forEach { map[it.name] = context.getString(it.id).replace("%s", "{msg}") @@ -39,7 +38,7 @@ object TranslationMapping { map["PORT"] = "android" map["DEVICE_NAME"] = "${Build.MANUFACTURER} - ${Build.MODEL}" map["APP_VERSION"] = BuildConfig.VLC_VERSION_NAME - return JSONObject(map.toMap()).toString() + return map } enum class StringMapping(@StringRes val id:Int) { diff --git a/application/webserver/src/main/java/org/videolan/vlc/webserver/websockets/RemoteAccessWebSockets.kt b/application/webserver/src/main/java/org/videolan/vlc/webserver/websockets/RemoteAccessWebSockets.kt index edba545e9..685092354 100644 --- a/application/webserver/src/main/java/org/videolan/vlc/webserver/websockets/RemoteAccessWebSockets.kt +++ b/application/webserver/src/main/java/org/videolan/vlc/webserver/websockets/RemoteAccessWebSockets.kt @@ -29,7 +29,7 @@ import android.content.SharedPreferences import android.media.AudioManager import android.support.v4.media.session.PlaybackStateCompat import android.util.Log -import com.google.gson.Gson +import com.squareup.moshi.Moshi import io.ktor.server.routing.Routing import io.ktor.server.websocket.WebSocketServerSession import io.ktor.server.websocket.webSocket @@ -48,6 +48,7 @@ import org.videolan.vlc.R import org.videolan.vlc.gui.video.VideoPlayerActivity import org.videolan.vlc.webserver.BuildConfig import org.videolan.vlc.webserver.RemoteAccessServer +import org.videolan.vlc.webserver.convertToJson import org.videolan.vlc.webserver.ssl.SecretGenerator import org.videolan.vlc.webserver.websockets.IncomingMessageType.* import java.util.Calendar @@ -59,7 +60,7 @@ import java.util.concurrent.atomic.AtomicInteger object RemoteAccessWebSockets { private const val TAG = "VLC/HttpSharingServerWS" - val onPlaybackEventChannel = Channel() + val onPlaybackEventChannel = Channel() val messageQueue: LinkedBlockingQueue = LinkedBlockingQueue() private val webSocketSessions: MutableMap = ConcurrentHashMap() private val tickets: MutableList = Collections.synchronizedList(mutableListOf()) @@ -71,16 +72,16 @@ object RemoteAccessWebSockets { try { webSocketSessions[sessionId] = this if (BuildConfig.DEBUG) Log.d(TAG, "WebSockets: Started session: $sessionId") + val moshi = Moshi.Builder().build().adapter(WSIncomingMessage::class.java) // Handle a WebSocket session for (frame in incoming) { try { frame as? Frame.Text ?: continue val message = frame.readText() - val gson = Gson() - val incomingMessage = gson.fromJson(message, WSIncomingMessage::class.java) + val incomingMessage = moshi.fromJson(message) ?: continue if (BuildConfig.DEBUG) Log.i(TAG, "WebSockets: Received message '$message'") if (!verifyWebsocketAuth(incomingMessage)) { - send(Frame.Text(Gson().toJson(RemoteAccessServer.WebSocketAuthorization("forbidden", initialMessage = message)))) + send(Frame.Text(convertToJson(RemoteAccessServer.WebSocketAuthorization("forbidden", initialMessage = message)))) } else { val service = RemoteAccessServer.getInstance(context).service manageIncomingMessages(incomingMessage, settings, service, context) @@ -287,7 +288,7 @@ object RemoteAccessWebSockets { private fun playbackControlAllowedOrSend(settings: SharedPreferences): Boolean { val allowed = settings.getBoolean(REMOTE_ACCESS_PLAYBACK_CONTROL, true) if (!allowed) { - val message = Gson().toJson(RemoteAccessServer.PlaybackControlForbidden()) + val message = convertToJson(RemoteAccessServer.PlaybackControlForbidden()) AppScope.launch { webSocketSessions.forEach { (_, session) -> session.send(Frame.Text(message)) } } } return allowed @@ -310,7 +311,7 @@ object RemoteAccessWebSockets { ?: 0 } } - return Gson().toJson(RemoteAccessServer.Volume(volume)) + return convertToJson(RemoteAccessServer.Volume(volume)) } fun createTicket(): String { @@ -320,9 +321,9 @@ object RemoteAccessWebSockets { } suspend fun sendToAll(messageObj: RemoteAccessServer.WSMessage) { - val message = Gson().toJson(messageObj) + val message = convertToJson(messageObj) addToQueue(messageObj) - onPlaybackEventChannel.trySend(message) + onPlaybackEventChannel.trySend(messageObj) if (BuildConfig.DEBUG) Log.d(TAG, "WebSockets: sendToAll called on ${webSocketSessions.size} sessions with message '$message'") webSocketSessions.forEach { (sessionId, session) -> try { @@ -342,7 +343,7 @@ object RemoteAccessWebSockets { private fun addToQueue(wsMessage: RemoteAccessServer.WSMessage) { when (wsMessage.type) { // Duplicate browser description messages are OK - "browser-description" -> {} + RemoteAccessServer.WSMessageType.BROWSER_DESCRIPTION -> {} else -> messageQueue.removeIf { it.type == wsMessage.type } } messageQueue.add(wsMessage)