<repo_name>ch1ngz/SocketMessageReceiver<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/socket/SubscriptionStatus.kt
package com.example.surrogatemessagereceiver.socket

import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
import java.util.Locale

@Parcelize
data class SubscriptionStatus(
    @SerializedName("status")
    val status: String
) : Parcelable {

    companion object {
        private const val STATUS_OK = "ok"
    }

    fun isOk() = status.toLowerCase(Locale.getDefault()) == STATUS_OK
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/ui/ChatViewModel.kt
package com.example.surrogatemessagereceiver.chat.ui

import androidx.lifecycle.MutableLiveData
import com.example.surrogatemessagereceiver.chat.interactor.ChatInteractor
import com.example.surrogatemessagereceiver.chat.model.ChatMessage
import com.example.surrogatemessagereceiver.common.BaseViewModel
import com.example.surrogatemessagereceiver.socket.UpdatesManager
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import timber.log.Timber

class ChatViewModel(
    private val updatesManager: UpdatesManager,
    chatInteractor: ChatInteractor
) : BaseViewModel() {

    val messagesLiveData = MutableLiveData<List<ChatMessage>>()

    init {
        chatInteractor.getMessagesFlowable()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeBy(
                onNext = { messagesLiveData.postValue(it) },
                onError = { Timber.e(it, "Error getting messages") }
            )
            .disposeOnCleared()

        updatesManager.start()
    }

    override fun onCleared() {
        updatesManager.stop()
        super.onCleared()
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/ui/adapter/ChatAuthorViewHolder.kt
package com.example.surrogatemessagereceiver.chat.ui.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.surrogatemessagereceiver.R
import com.example.surrogatemessagereceiver.chat.model.ChatMessage
import com.example.surrogatemessagereceiver.utils.ColorUtils
import kotlinx.android.synthetic.main.item_message.view.messageTextView
import kotlinx.android.synthetic.main.item_message.view.nameTextView

class ChatAuthorViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false)
) {

    private val nameTextView = itemView.nameTextView
    private val messageTextView = itemView.messageTextView

    fun onBind(item: ChatMessage) {
        nameTextView.setTextColor(ColorUtils.getUserColor(itemView.context, item.userId))
        nameTextView.text = item.name
        messageTextView.text = item.message
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/model/MessageConverter.kt
package com.example.surrogatemessagereceiver.chat.model

object MessageConverter {

    fun fromNetwork(response: MessageResponse): ChatMessage =
        ChatMessage(
            userId = response.userId,
            name = response.username,
            message = response.message
        )
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/socket/SocketUpdatesManager.kt
package com.example.surrogatemessagereceiver.socket

import com.example.surrogatemessagereceiver.chat.model.MessageResponse
import com.example.surrogatemessagereceiver.config.AppConstants
import com.example.surrogatemessagereceiver.config.AppConstants.ROOM_ID
import com.github.nkzawa.emitter.Emitter
import com.github.nkzawa.engineio.client.transports.WebSocket
import com.github.nkzawa.socketio.client.Ack
import com.github.nkzawa.socketio.client.IO
import com.github.nkzawa.socketio.client.Socket
import com.google.gson.Gson
import org.json.JSONObject
import timber.log.Timber

class SocketUpdatesManager(
    private val gson: Gson
) : UpdatesManager {

    companion object {
        private const val EVENT_TAG = "SOCKET_EVENT"
        private const val EVENT_SUBSCRIBE = "subscribe"
        private const val EVENT_UNSUBSCRIBE = "unsubscribe"
    }

    private var messageCallback: ((MessageResponse) -> Unit)? = null

    private val socket: Socket

    private var isStarted = false

    init {
        val options = IO.Options()
        options.transports = arrayOf(WebSocket.NAME)
        socket = IO.socket(AppConstants.MB_URL, options)
    }

    override fun start() {
        if (isStarted) {
            Timber.tag(EVENT_TAG).d("Trying to start updates manager while it is connecting")
            return
        }

        isStarted = true
        Timber.tag(EVENT_TAG).d("Starting updates manager connection")
        socket.connect()
        socket.on(Socket.EVENT_CONNECT, onConnectListener)
        socket.on(Socket.EVENT_DISCONNECT, onDisconnectListener)
    }

    override fun stop() {
        if (!socket.connected()) {
            Timber.tag(EVENT_TAG).d("Trying to stop updates manager while it is already stopped")
            return
        }

        Timber.tag(EVENT_TAG).d("Stopping updates manager")
        socket.emit(EVENT_UNSUBSCRIBE, ROOM_ID, onStatusAck)
        socket.disconnect()
    }

    override fun onMessageReceived(onReceived: (MessageResponse) -> Unit) {
        messageCallback = onReceived
    }

    private val onConnectListener = Emitter.Listener {
        Timber.tag(EVENT_TAG).d("Updates manager successfully connected")
        socket.emit(EVENT_SUBSCRIBE, ROOM_ID, onStatusAck)
    }

    private val onDisconnectListener = Emitter.Listener {
        Timber.tag(EVENT_TAG).e("Updates manager disconnected: ${it.firstOrNull()}")
    }

    private val onStatusAck = Ack {
        val status = gson.fromJson(it.firstOrNull().toString(), SubscriptionStatus::class.java)
        Timber.tag(EVENT_TAG).d("Subscription status: ${status.status}")

        if (status.isOk()) {
            Timber.tag(EVENT_TAG).d("Waiting for messages...")
            socket.on(ROOM_ID, onNewMessageListener)
        }
    }

    private val onNewMessageListener = Emitter.Listener { args ->
        val response = args.firstOrNull() as? JSONObject
        Timber.tag(EVENT_TAG).d("Message received: $response")
        if (response == null) return@Listener

        val message = gson.fromJson(response.toString(), MessageResponse::class.java)
        messageCallback?.invoke(message)
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/interactor/ChatInteractor.kt
package com.example.surrogatemessagereceiver.chat.interactor

import com.example.surrogatemessagereceiver.chat.model.ChatMessage
import com.example.surrogatemessagereceiver.chat.repository.ChatRepository
import io.reactivex.Flowable

class ChatInteractor(
    private val chatRepository: ChatRepository
) {

    fun getMessagesFlowable(): Flowable<List<ChatMessage>> =
        chatRepository.getMessagesFlowable()
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/model/ChatMessage.kt
package com.example.surrogatemessagereceiver.chat.model

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class ChatMessage(
    val userId: String,
    val name: String,
    val message: String
) : Parcelable {

    fun isSameAuthor(userId: String?) = this.userId == userId
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/utils/ViewModelExtensions.kt
package com.example.surrogatemessagereceiver.utils

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer


fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, observer: (data: T) -> Unit) {
    this.observe(owner, Observer {
        if (it != null) observer.invoke(it)
    })
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/socket/UpdatesManager.kt
package com.example.surrogatemessagereceiver.socket

import com.example.surrogatemessagereceiver.chat.model.MessageResponse

interface UpdatesManager {
    fun start()
    fun stop()
    fun onMessageReceived(onReceived: (MessageResponse) -> Unit)
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/ui/ChatActivity.kt
package com.example.surrogatemessagereceiver.chat.ui

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.surrogatemessagereceiver.R
import com.example.surrogatemessagereceiver.chat.ui.adapter.ChatAdapter
import com.example.surrogatemessagereceiver.utils.observeNonNull
import kotlinx.android.synthetic.main.activity_main.emptyTextView
import kotlinx.android.synthetic.main.activity_main.messagesRecyclerView
import org.koin.androidx.viewmodel.ext.android.viewModel

class ChatActivity : AppCompatActivity() {

    private val chatViewModel: ChatViewModel by viewModel()

    private val chatAdapter: ChatAdapter by lazy {
        ChatAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        with(messagesRecyclerView) {
            layoutManager = LinearLayoutManager(this@ChatActivity)
            adapter = chatAdapter
        }

        chatViewModel.messagesLiveData.observeNonNull(this) { messages ->
            chatAdapter.setItems(messages)

            val isEmpty = messages.isEmpty()
            messagesRecyclerView.isVisible = !isEmpty
            emptyTextView.isVisible = isEmpty
        }
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/config/AppConstants.kt
package com.example.surrogatemessagereceiver.config

object AppConstants {

    const val MB_URL = "https://broker.surrogate.tv"
    const val ROOM_ID = "/chat/df6d222d-ea21-46ed-972c-3a2363935cab"
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/repository/ChatRepository.kt
package com.example.surrogatemessagereceiver.chat.repository

import com.example.surrogatemessagereceiver.chat.model.MessageConverter
import com.example.surrogatemessagereceiver.chat.model.ChatMessage
import com.example.surrogatemessagereceiver.socket.UpdatesManager
import io.reactivex.Flowable
import io.reactivex.processors.BehaviorProcessor
import timber.log.Timber

interface ChatRepository {
    fun getMessagesFlowable(): Flowable<List<ChatMessage>>
}

class ChatRepositoryImpl(
    updatesManager: UpdatesManager
) : ChatRepository {

    private val messagesProcessor = BehaviorProcessor.create<MutableList<ChatMessage>>()

    init {
        updatesManager.onMessageReceived { response ->
            if (!response.isMessage()) {
                Timber.w("Response is incorrect, skipping")
                return@onMessageReceived
            }

            val messages = messagesProcessor.value ?: mutableListOf()
            val newMessage = MessageConverter.fromNetwork(response)
            messages.add(newMessage)
            messagesProcessor.offer(messages)
        }
    }

    override fun getMessagesFlowable(): Flowable<List<ChatMessage>> = messagesProcessor.map { it }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/ChatModule.kt
package com.example.surrogatemessagereceiver.chat

import com.example.surrogatemessagereceiver.chat.interactor.ChatInteractor
import com.example.surrogatemessagereceiver.chat.repository.ChatRepository
import com.example.surrogatemessagereceiver.chat.repository.ChatRepositoryImpl
import com.example.surrogatemessagereceiver.chat.ui.ChatViewModel
import com.example.surrogatemessagereceiver.socket.SocketUpdatesManager
import com.example.surrogatemessagereceiver.socket.UpdatesManager
import com.google.gson.Gson
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module

object ChatModule {

    fun create() = module {
        single { Gson() }
        single { SocketUpdatesManager(get()) } bind UpdatesManager::class
        single { ChatRepositoryImpl(get()) } bind ChatRepository::class
        single { ChatInteractor(get()) }

        viewModel { ChatViewModel(get(), get()) }
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/ui/adapter/ChatViewHolder.kt
package com.example.surrogatemessagereceiver.chat.ui.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.surrogatemessagereceiver.R
import com.example.surrogatemessagereceiver.chat.model.ChatMessage

class ChatViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.item_message_rounded, parent, false)
) {

    private val messageTextView = itemView as TextView

    fun onBind(item: ChatMessage) {
        messageTextView.text = item.message
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/ui/adapter/ChatAdapter.kt
package com.example.surrogatemessagereceiver.chat.ui.adapter

import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.surrogatemessagereceiver.R
import com.example.surrogatemessagereceiver.chat.model.ChatMessage

class ChatAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var data = listOf<ChatMessage>()

    fun setItems(new: List<ChatMessage>) {
        data = new
        notifyItemInserted(data.size)
    }

    override fun getItemViewType(position: Int): Int {
        val previous = if (position > 0) data[position - 1] else null
        val current = data[position]

        return if (current.isSameAuthor(previous?.userId)) {
            R.layout.item_message_rounded
        } else {
            R.layout.item_message
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
        R.layout.item_message_rounded -> ChatViewHolder(parent)
        R.layout.item_message -> ChatAuthorViewHolder(parent)
        else -> throw IllegalStateException("Unknown type $viewType")
    }

    override fun getItemCount(): Int = data.size

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ChatViewHolder -> holder.onBind(data[position])
            is ChatAuthorViewHolder -> holder.onBind(data[position])
        }
    }
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/chat/model/MessageResponse.kt
package com.example.surrogatemessagereceiver.chat.model

import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize

@Parcelize
data class MessageResponse(
    @SerializedName("type")
    val type: String,

    @SerializedName("username")
    val username: String,

    @SerializedName("userId")
    val userId: String,

    @SerializedName("message")
    val message: String
) : Parcelable {

    companion object {
        private const val TYPE_CHAT_MESSAGE = "chatMessage"
    }

    fun isMessage(): Boolean = type == TYPE_CHAT_MESSAGE
}<file_sep>/app/src/main/java/com/example/surrogatemessagereceiver/utils/ColorUtils.kt
package com.example.surrogatemessagereceiver.utils

import android.content.Context
import androidx.core.content.ContextCompat
import com.example.surrogatemessagereceiver.R
import kotlin.math.abs

private val USER_COLORS = arrayOf(
    R.color.username_0,
    R.color.username_1,
    R.color.username_2,
    R.color.username_3,
    R.color.username_4,
    R.color.username_5,
    R.color.username_6,
    R.color.username_7,
    R.color.username_8,
    R.color.username_9
)

object ColorUtils {

    fun getUserColor(context: Context, id: String): Int {
        val hash = id.substringBefore("-").toLongOrNull(radix = 16) ?: id.hashCode().toLong()
        val position = abs(hash % USER_COLORS.size).toInt()
        val colorResId = USER_COLORS[position]
        return ContextCompat.getColor(context, colorResId)
    }
}
