diff --git a/app/build.gradle b/app/build.gradle index 1154c4a..07b107a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,10 +24,18 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.0.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.squareup.okhttp3:okhttp:4.2.1' + implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' + implementation 'com.squareup.retrofit2:retrofit:2.4.0' + implementation 'com.squareup.retrofit2:converter-gson:2.4.0' + implementation 'android.arch.lifecycle:extensions:1.1.1' + implementation 'com.google.android.material:material:1.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4bdaf9e..d8b2197 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + package="itis.ru.wschat"> + + - + android:theme="@style/AppTheme"> + + + \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/Const.kt b/app/src/main/java/itis/ru/wschat/Const.kt new file mode 100644 index 0000000..ae9a966 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/Const.kt @@ -0,0 +1,9 @@ +package itis.ru.wschat + +object Const { + const val API_CHAT: String = "wss://backend-chat.cloud.technokratos.com/chat" + const val API_BASE: String = "https://backend-chat.cloud.technokratos.com/" + const val PREFERENCES: String = "SHARED_PREFERENCES" + const val DEVICE_ID: String = "device_id" + const val USERNAME: String = "username" +} \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/MainActivity.kt b/app/src/main/java/itis/ru/wschat/MainActivity.kt deleted file mode 100644 index c2e601c..0000000 --- a/app/src/main/java/itis/ru/wschat/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package itis.ru.wschat - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/itis/ru/wschat/Utils.kt b/app/src/main/java/itis/ru/wschat/Utils.kt new file mode 100644 index 0000000..1f120e3 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/Utils.kt @@ -0,0 +1,13 @@ +package itis.ru.wschat + +import java.util.* + +object Utils { + fun generateRandomString(length: Int): String? { + val AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_" + val rnd = Random() + val sb = StringBuilder(length) + for (i in 0 until length) sb.append(AB[rnd.nextInt(AB.length)]) + return sb.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/adapters/MessageAdapter.kt b/app/src/main/java/itis/ru/wschat/adapters/MessageAdapter.kt new file mode 100644 index 0000000..94f7983 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/adapters/MessageAdapter.kt @@ -0,0 +1,89 @@ +package itis.ru.wschat.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import itis.ru.wschat.R +import itis.ru.wschat.models.Message + +class MessageAdapter internal constructor( + private val uidFrom: String, private var data: MutableList +) : RecyclerView.Adapter() { + private var view: View? = null + + override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { + holder.setMessage(data[position]) + } + + override fun getItemCount(): Int { + return data.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { + return if (viewType == R.layout.item_message_to) { + view = + LayoutInflater.from(parent.context).inflate(R.layout.item_message_to, parent, false) + MessageViewHolder() + } else { + view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_message_from, parent, false) + MessageViewHolder() + } + } + + override fun getItemViewType(position: Int): Int { + return if (uidFrom != data[position].user) { + R.layout.item_message_to + } else { + R.layout.item_message_from + } + } + + override fun getItemId(position: Int): Long { + return data[position].id + } + + fun updateData(list: MutableList) { + val diffResult = DiffUtil.calculateDiff(RecentDiffUtilCallback(this.data, list)) + diffResult.dispatchUpdatesTo(this) + this.data = list + } + + inner class MessageViewHolder : RecyclerView.ViewHolder(view!!) { + internal fun setMessage(message: Message) { + val textView = view?.findViewById(R.id.text_view) + textView?.text = message.message + } + } + + class RecentDiffUtilCallback internal constructor( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + return old.id == new.id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + return old == new + } + } + + +} diff --git a/app/src/main/java/itis/ru/wschat/api/LoginApiService.kt b/app/src/main/java/itis/ru/wschat/api/LoginApiService.kt new file mode 100644 index 0000000..d8e99cb --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/api/LoginApiService.kt @@ -0,0 +1,13 @@ +package itis.ru.wschat.api + +import io.reactivex.Single +import itis.ru.wschat.Const +import itis.ru.wschat.models.LoginResponse +import itis.ru.wschat.models.User +import retrofit2.http.Body +import retrofit2.http.POST + +interface LoginApiService{ + @POST("login") + fun login(@Body user: User): Single +} diff --git a/app/src/main/java/itis/ru/wschat/api/SocketManager.kt b/app/src/main/java/itis/ru/wschat/api/SocketManager.kt new file mode 100644 index 0000000..7184ded --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/api/SocketManager.kt @@ -0,0 +1,71 @@ +package itis.ru.wschat.api + +import android.content.Context +import android.util.Log +import itis.ru.wschat.Const +import okhttp3.* +import java.util.concurrent.TimeUnit + +private const val NORMAL_CLOSURE_STATUS = 1000 + +class SocketManager( + private val context: Context, + private var messageCallback: (String?, Throwable?) -> Unit, + private var getMessagesCallback: (String?, Throwable?) -> Unit +) { + private val client: OkHttpClient = OkHttpClient().newBuilder().build() + private lateinit var socket: WebSocket + private var deviceIdSent = false + + fun initSocketManager() { + val request: Request = Request.Builder().url(Const.API_CHAT).build() + val listener = object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + Log.d("Socket", "connect") + if (!deviceIdSent) { + } + val deviceId = + context.getSharedPreferences(Const.PREFERENCES, Context.MODE_PRIVATE) + .getString(Const.DEVICE_ID, "") + val json = "{ \"device_id\":\"$deviceId\" }" + webSocket.send(json) + deviceIdSent = true + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + if (text == "{\"status\": \"ok\"}"){ + messageCallback(text, null) + }else if (text.contains("items")){ + getMessagesCallback(text, null) + } + Log.d("Socket", "onMessage $text") + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.d("Socket", "onClosing $reason") + webSocket.close(NORMAL_CLOSURE_STATUS, null) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + socket = client.newWebSocket(request, this) + messageCallback(null, t) + Log.d("Socket", "error $t response $response") + } + + } + socket = client.newWebSocket(request, listener) + } + + fun getMessages(count: Int) { + val json = "{ \"history\": { \"limit\": $count} }" + socket.send(json) + } + + fun sendMessage(message: String) { + val json = "{ \"message\":\"$message\" }" + socket.send(json) + } +} diff --git a/app/src/main/java/itis/ru/wschat/models/GetMessagesResponse.java b/app/src/main/java/itis/ru/wschat/models/GetMessagesResponse.java new file mode 100644 index 0000000..004d279 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/models/GetMessagesResponse.java @@ -0,0 +1,70 @@ +package itis.ru.wschat.models; + +import java.util.List; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GetMessagesResponse { + + @SerializedName("items") + @Expose + private List items = null; + @SerializedName("first") + @Expose + private Integer first; + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public Integer getFirst() { + return first; + } + + public void setFirst(Integer first) { + this.first = first; + } + +} + +class Item { + + @SerializedName("id") + @Expose + private Integer id; + @SerializedName("user") + @Expose + private String user; + @SerializedName("message") + @Expose + private String message; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/app/src/main/java/itis/ru/wschat/models/LoginError.kt b/app/src/main/java/itis/ru/wschat/models/LoginError.kt new file mode 100644 index 0000000..a5b4284 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/models/LoginError.kt @@ -0,0 +1,3 @@ +package itis.ru.wschat.models + +data class LoginError(val errors: String) \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/models/LoginResponse.kt b/app/src/main/java/itis/ru/wschat/models/LoginResponse.kt new file mode 100644 index 0000000..d65b09a --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/models/LoginResponse.kt @@ -0,0 +1,3 @@ +package itis.ru.wschat.models + +data class LoginResponse(val status: String) \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/models/Message.kt b/app/src/main/java/itis/ru/wschat/models/Message.kt new file mode 100644 index 0000000..2ecc002 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/models/Message.kt @@ -0,0 +1,7 @@ +package itis.ru.wschat.models + +data class Message( + val id: Long, + val message: String = "", + val user: String = "" +) diff --git a/app/src/main/java/itis/ru/wschat/models/Response.kt b/app/src/main/java/itis/ru/wschat/models/Response.kt new file mode 100644 index 0000000..4044d3d --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/models/Response.kt @@ -0,0 +1,11 @@ +package itis.ru.wschat.models + + +class Response(val data: T?, val error: Throwable?) { + + companion object { + fun success(data: T): Response = Response(data, null) + + fun error(error: Throwable): Response? = Response(null, error) + } +} diff --git a/app/src/main/java/itis/ru/wschat/models/User.kt b/app/src/main/java/itis/ru/wschat/models/User.kt new file mode 100644 index 0000000..f7547b0 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/models/User.kt @@ -0,0 +1,7 @@ +package itis.ru.wschat.models + +import com.google.gson.annotations.SerializedName + +data class User(val username: String, + @SerializedName("device_id") + val deviceId: String) \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/ui/chat/ChatActivity.kt b/app/src/main/java/itis/ru/wschat/ui/chat/ChatActivity.kt new file mode 100644 index 0000000..cbd2e90 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/ui/chat/ChatActivity.kt @@ -0,0 +1,47 @@ +package itis.ru.wschat.ui.chat + +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import itis.ru.wschat.Const +import itis.ru.wschat.R +import itis.ru.wschat.adapters.MessageAdapter +import kotlinx.android.synthetic.main.activity_chat.* + +class ChatActivity : AppCompatActivity() { + private var viewModel: ChatViewModel = ChatViewModel(this) + private var adapter: MessageAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_chat) + initRecycler() + btn_send_message.setOnClickListener { + viewModel.sendMessage(et_message.text.toString()) + et_message.setText("") + } + viewModel.getMessagesLiveData.observe(this, Observer { + if (it.data != null){ + adapter?.updateData(it.data.toMutableList()) + } + }) + viewModel.messageSendLiveData.observe(this, Observer { + if (it.data != null) { + Toast.makeText(this, "Message sent", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, "Error ${it.error?.message}", Toast.LENGTH_SHORT).show() + } + }) + } + + private fun initRecycler() { + val username: String = getSharedPreferences(Const.PREFERENCES, Context.MODE_PRIVATE) + .getString(Const.USERNAME, "").toString() + + adapter = MessageAdapter(username, arrayListOf()) + adapter?.setHasStableIds(true) + rv_messages.adapter = adapter + } +} diff --git a/app/src/main/java/itis/ru/wschat/ui/chat/ChatViewModel.kt b/app/src/main/java/itis/ru/wschat/ui/chat/ChatViewModel.kt new file mode 100644 index 0000000..24d6710 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/ui/chat/ChatViewModel.kt @@ -0,0 +1,47 @@ +package itis.ru.wschat.ui.chat + +import android.app.Activity +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.google.gson.Gson +import itis.ru.wschat.api.SocketManager +import itis.ru.wschat.models.GetMessagesResponse +import itis.ru.wschat.models.Message +import itis.ru.wschat.models.Response + + +class ChatViewModel(context: Activity) : ViewModel() { + private val socketManager: SocketManager = SocketManager(context, { message, error -> + context.runOnUiThread { + if (message != null) { + messageSendLiveData.value = Response.success(message) + getMessages(1000) + } else if (error != null) { + messageSendLiveData.value = Response.error(error) + } + } + }, { messages, error -> + context.runOnUiThread { + if (messages != null) { + val response: GetMessagesResponse = + Gson().fromJson(messages, GetMessagesResponse::class.java) + getMessagesLiveData.value = Response.success(response.items) + } + } + }) + + val messageSendLiveData = MutableLiveData>() + val getMessagesLiveData = MutableLiveData>>() + + init { + socketManager.initSocketManager() + } + + fun getMessages(count: Int) { + socketManager.getMessages(count) + } + + fun sendMessage(message: String) { + socketManager.sendMessage(message) + } +} \ No newline at end of file diff --git a/app/src/main/java/itis/ru/wschat/ui/login/LoginActivity.kt b/app/src/main/java/itis/ru/wschat/ui/login/LoginActivity.kt new file mode 100644 index 0000000..9d86217 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/ui/login/LoginActivity.kt @@ -0,0 +1,30 @@ +package itis.ru.wschat.ui.login + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import itis.ru.wschat.R +import itis.ru.wschat.models.User +import itis.ru.wschat.ui.chat.ChatActivity +import kotlinx.android.synthetic.main.activity_login.* + +class LoginActivity : AppCompatActivity() { + private val viewModel: LoginViewModel = LoginViewModel(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + btn_login.setOnClickListener { + viewModel.login(User(et_name.text.toString(), et_device_id.text.toString())) + } + viewModel.loginLiveData.observe(this, Observer { response -> + if (response.error == null) { + startActivity(Intent(this, ChatActivity::class.java)) + } else { + Toast.makeText(this, "Error ${response.error.message}", Toast.LENGTH_SHORT).show() + } + }) + } +} diff --git a/app/src/main/java/itis/ru/wschat/ui/login/LoginViewModel.kt b/app/src/main/java/itis/ru/wschat/ui/login/LoginViewModel.kt new file mode 100644 index 0000000..6fd2de9 --- /dev/null +++ b/app/src/main/java/itis/ru/wschat/ui/login/LoginViewModel.kt @@ -0,0 +1,51 @@ +package itis.ru.wschat.ui.login + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.google.gson.GsonBuilder +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import itis.ru.wschat.Const +import itis.ru.wschat.Const.DEVICE_ID +import itis.ru.wschat.Const.PREFERENCES +import itis.ru.wschat.Const.USERNAME +import itis.ru.wschat.api.LoginApiService +import itis.ru.wschat.models.Response +import itis.ru.wschat.models.User +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +class LoginViewModel(private val context: Context) : ViewModel() { + val loginLiveData = MutableLiveData>() + private val disposables = CompositeDisposable() + private val api = Retrofit.Builder() + .baseUrl(Const.API_BASE) + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build() + .create(LoginApiService::class.java) + + fun login(user: User) { + disposables.add( + api.login(user) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + if (response.status == "ok") { + val sharedPref = + context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + sharedPref.edit() + .putString(DEVICE_ID, user.deviceId) + .putString(USERNAME, user.username).apply() + loginLiveData.value = Response.success(response.status) + } + }, { + loginLiveData.value = Response.error(it) + it.printStackTrace() + }) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index c7bd21d..1f6bb29 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -2,13 +2,13 @@ xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" - android:viewportHeight="108" - android:viewportWidth="108"> + android:viewportWidth="108" + android:viewportHeight="108"> + android:strokeWidth="1" + android:strokeColor="#00000000"> + android:strokeWidth="1" + android:strokeColor="#00000000" /> diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 2408e30..0d025f9 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,74 +1,170 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:viewportHeight="108"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/message_from.xml b/app/src/main/res/drawable/message_from.xml new file mode 100644 index 0000000..b71efff --- /dev/null +++ b/app/src/main/res/drawable/message_from.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_to.xml b/app/src/main/res/drawable/message_to.xml new file mode 100644 index 0000000..883f1d0 --- /dev/null +++ b/app/src/main/res/drawable/message_to.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml new file mode 100644 index 0000000..7143601 --- /dev/null +++ b/app/src/main/res/layout/activity_chat.xml @@ -0,0 +1,49 @@ + + + + + + + + + +