lil update

This commit is contained in:
2021-08-04 23:01:22 +03:00
parent dabf2f86fd
commit c77ebae57a
33 changed files with 548 additions and 223 deletions
+3 -2
View File
@@ -4,6 +4,7 @@ plugins {
id("kotlin-kapt")
id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin")
id("kotlin-parcelize")
}
android {
@@ -64,7 +65,7 @@ kapt {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.21")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
@@ -103,7 +104,7 @@ dependencies {
implementation("com.github.yogacp:android-viewbinding:1.0.2")
implementation("io.coil-kt:coil:1.2.2")
implementation("io.coil-kt:coil:1.3.0")
implementation("com.google.code.gson:gson:2.8.7")
implementation("org.jsoup:jsoup:1.14.1")
+1
View File
@@ -12,6 +12,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:testOnly="false"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity"
@@ -27,14 +27,19 @@ object VKAuth {
const val redirectUrl = "https://oauth.vk.com/blank.html"
fun getDirectAuthUrl(login: String, password: String, captcha: String = ""): String {
fun getDirectAuthUrl(
login: String,
password: String,
captchaSid: String? = null,
captchaKey: String? = null
): String {
return "https://oauth.vk.com/token?grant_type=password&" +
"client_id=${VKConstants.VK_APP_ID}&" +
"scope=$settings&" +
"client_secret=${VKConstants.VK_APP_SECRET}&" +
"username=$login&" +
"password=$password" +
(if (captcha.isBlank()) "" else "&$captcha") +
(if (captchaSid == null || captchaKey == null) "" else "&captcha_sid=$captchaSid&captcha_key=$captchaKey") +
"&v=${VKApi.API_VERSION}"
// return "https://oauth.vk.com/token?grant_type=password&" +
// "client_id=2274003&" +
@@ -1,7 +1,17 @@
package com.meloda.fast.api
import com.meloda.fast.api.model.response.GetConversationsResponse
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface VKRepo {
@FormUrlEncoded
@POST(VKUrls.getConversations)
suspend fun getAllChats(
@Field("user_id") chatId: Int,
@Field("token") token: String
): Answer<GetConversationsResponse>
}
@@ -0,0 +1,7 @@
package com.meloda.fast.api
object VKUrls {
const val getConversations = "messages.getConversations"
}
@@ -39,7 +39,7 @@ class VKConversation() : VKModel(), Cloneable {
var isNoSound: Boolean = false
var membersCount: Int = 0
var title: String = ""
var title: String? = null
var pinnedMessage: VKMessage? = null
@@ -84,6 +84,7 @@ class VKConversation() : VKModel(), Cloneable {
o.optJSONObject("chat_settings")?.let {
membersCount = it.optInt("members_count")
title = it.optString("title")
if (title?.isBlank() == true) title = null
it.optJSONObject("pinned_message")?.let { pinned ->
pinnedMessage = VKMessage(pinned)
@@ -109,13 +110,9 @@ class VKConversation() : VKModel(), Cloneable {
fun isGroup() = type == Type.GROUP
override fun toString(): String {
return title
}
override fun toString() = title ?: ""
public override fun clone(): VKConversation {
return super.clone() as VKConversation
}
public override fun clone() = super.clone() as VKConversation
enum class Type(val value: String) {
NULL("null"),
@@ -1,8 +1,9 @@
package com.meloda.fast.api.model
import com.meloda.fast.base.adapter.BaseItem
import java.io.Serializable
abstract class VKModel : Serializable {
abstract class VKModel : BaseItem(), Serializable {
abstract val attachmentType: VKAttachments.Type
@@ -0,0 +1,13 @@
package com.meloda.fast.api.model.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
class MessagesResponse(
val count: Int
) {
}
@Parcelize
data class GetConversationsResponse(val a: String) : Parcelable
// TODO: 7/12/2021 use hilt for this like in LIR and make simple conversations' screen
@@ -1,6 +0,0 @@
package com.meloda.fast.api.model.response
class ResponseMessagesGetConversations(
val count: Int
) {
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.util
import androidx.annotation.WorkerThread
import com.meloda.fast.api.model.*
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
@@ -27,9 +28,9 @@ object VKUtil {
}
fun sortMessagesByDate(
values: ArrayList<com.meloda.fast.api.model.VKMessage>,
values: ArrayList<VKMessage>,
firstOnTop: Boolean
): ArrayList<com.meloda.fast.api.model.VKMessage> {
): ArrayList<VKMessage> {
values.sortWith { m1, m2 ->
val d1 = m1.date
val d2 = m2.date
@@ -45,9 +46,9 @@ object VKUtil {
}
fun sortConversationsByDate(
values: ArrayList<com.meloda.fast.api.model.VKConversation>,
values: ArrayList<VKConversation>,
firstOnTop: Boolean
): ArrayList<com.meloda.fast.api.model.VKConversation> {
): ArrayList<VKConversation> {
values.sortWith { c1, c2 ->
val d1 = c1.lastMessage.date
val d2 = c2.lastMessage.date
@@ -121,25 +122,29 @@ object VKUtil {
}
fun getTitle(conversation: com.meloda.fast.api.model.VKConversation, peerUser: com.meloda.fast.api.model.VKUser?, peerGroup: com.meloda.fast.api.model.VKGroup?): String {
fun getTitle(
conversation: VKConversation,
peerUser: VKUser?,
peerGroup: VKGroup?
): String {
return when {
conversation.isUser() -> {
peerUser?.let { return it.toString() } ?: ""
}
conversation.isUser() -> peerUser?.let { return it.toString() } ?: ""
conversation.isGroup() -> {
peerGroup?.let { return it.name } ?: ""
}
conversation.isChat() -> {
conversation.title
}
conversation.isGroup() -> peerGroup?.let { return it.name } ?: ""
conversation.isChat() -> conversation.title ?: ""
else -> ""
}
}
fun getMessageTitle(message: com.meloda.fast.api.model.VKMessage, fromUser: com.meloda.fast.api.model.VKUser?, fromGroup: com.meloda.fast.api.model.VKGroup?): String {
fun getMessageTitle(
message: VKMessage,
fromUser: VKUser?,
fromGroup: VKGroup?
): String {
return when {
message.isFromUser() -> {
fromUser?.let { return it.toString() } ?: ""
@@ -153,7 +158,11 @@ object VKUtil {
}
}
fun getAvatar(conversation: com.meloda.fast.api.model.VKConversation, peerUser: com.meloda.fast.api.model.VKUser?, peerGroup: com.meloda.fast.api.model.VKGroup?): String {
fun getAvatar(
conversation: VKConversation,
peerUser: VKUser?,
peerGroup: VKGroup?
): String {
return when {
conversation.isUser() -> {
peerUser?.let { return it.photo200 } ?: ""
@@ -171,7 +180,11 @@ object VKUtil {
}
}
fun getUserAvatar(message: com.meloda.fast.api.model.VKMessage, fromUser: com.meloda.fast.api.model.VKUser?, fromGroup: com.meloda.fast.api.model.VKGroup?): String {
fun getUserAvatar(
message: VKMessage,
fromUser: VKUser?,
fromGroup: VKGroup?
): String {
return when {
message.isFromUser() -> {
fromUser?.let { return it.photo100 } ?: ""
@@ -185,7 +198,7 @@ object VKUtil {
}
}
fun getUserPhoto(user: com.meloda.fast.api.model.VKUser): String {
fun getUserPhoto(user: VKUser): String {
if (user.photo200.isEmpty()) {
if (user.photo100.isEmpty()) {
if (user.photo50.isEmpty()) {
@@ -201,7 +214,7 @@ object VKUtil {
return ""
}
fun getGroupPhoto(group: com.meloda.fast.api.model.VKGroup): String {
fun getGroupPhoto(group: VKGroup): String {
if (group.photo200.isEmpty()) {
if (group.photo100.isEmpty()) {
if (group.photo50.isEmpty()) {
@@ -218,26 +231,26 @@ object VKUtil {
}
fun parseConversations(array: JSONArray): ArrayList<com.meloda.fast.api.model.VKConversation> {
val conversations = arrayListOf<com.meloda.fast.api.model.VKConversation>()
fun parseConversations(array: JSONArray): ArrayList<VKConversation> {
val conversations = arrayListOf<VKConversation>()
for (i in 0 until array.length()) {
conversations.add(com.meloda.fast.api.model.VKConversation(array.optJSONObject(i)))
conversations.add(VKConversation(array.optJSONObject(i)))
}
return conversations
}
fun parseMessages(array: JSONArray): ArrayList<com.meloda.fast.api.model.VKMessage> {
val messages = arrayListOf<com.meloda.fast.api.model.VKMessage>()
fun parseMessages(array: JSONArray): ArrayList<VKMessage> {
val messages = arrayListOf<VKMessage>()
for (i in 0 until array.length()) {
messages.add(com.meloda.fast.api.model.VKMessage(array.optJSONObject(i)))
messages.add(VKMessage(array.optJSONObject(i)))
}
return messages
}
fun isMessageHasFlag(mask: Int, flagName: String): Boolean {
val o: Any? = com.meloda.fast.api.model.VKMessage.flags[flagName]
val o: Any? = VKMessage.flags[flagName]
return if (o != null) { //has flag
val flag = o as Int
flag and mask > 0
@@ -248,8 +261,8 @@ object VKUtil {
//fromUser and fromGroup are null
@Deprecated("need to rewrite")
@WorkerThread
fun parseLongPollMessage(array: JSONArray): com.meloda.fast.api.model.VKMessage {
val message = com.meloda.fast.api.model.VKMessage()
fun parseLongPollMessage(array: JSONArray): VKMessage {
val message = VKMessage()
val id = array.optInt(1)
val flags = array.optInt(2)
@@ -276,33 +289,34 @@ object VKUtil {
}
if (it.has("source_act")) {
message.action = com.meloda.fast.api.model.VKMessageAction().also { action ->
action.type = com.meloda.fast.api.model.VKMessageAction.Type.fromString(it.optString("source_act"))
message.action = VKMessageAction().also { action ->
action.type =
VKMessageAction.Type.fromString(it.optString("source_act"))
when (action.type) {
com.meloda.fast.api.model.VKMessageAction.Type.CHAT_CREATE -> {
VKMessageAction.Type.CHAT_CREATE -> {
action.text = it.optString("source_text")
}
com.meloda.fast.api.model.VKMessageAction.Type.TITLE_UPDATE -> {
VKMessageAction.Type.TITLE_UPDATE -> {
action.oldText = it.optString("source_old_text")
action.text = it.optString("source_text")
}
com.meloda.fast.api.model.VKMessageAction.Type.PIN_MESSAGE -> {
VKMessageAction.Type.PIN_MESSAGE -> {
action.memberId = it.optInt("source_mid")
action.conversationMessageId = it.optInt("source_chat_local_id")
it.optJSONObject("source_message")?.let { message ->
action.message = com.meloda.fast.api.model.VKMessage(message)
action.message = VKMessage(message)
}
}
com.meloda.fast.api.model.VKMessageAction.Type.UNPIN_MESSAGE -> {
VKMessageAction.Type.UNPIN_MESSAGE -> {
action.memberId = it.optInt("source_mid")
action.conversationMessageId = it.optInt("source_chat_local_id")
}
com.meloda.fast.api.model.VKMessageAction.Type.INVITE_USER,
com.meloda.fast.api.model.VKMessageAction.Type.KICK_USER,
com.meloda.fast.api.model.VKMessageAction.Type.SCREENSHOT,
com.meloda.fast.api.model.VKMessageAction.Type.INVITE_USER_BY_CALL -> {
VKMessageAction.Type.INVITE_USER,
VKMessageAction.Type.KICK_USER,
VKMessageAction.Type.SCREENSHOT,
VKMessageAction.Type.INVITE_USER_BY_CALL -> {
action.memberId = it.optInt("source_mid")
}
}
@@ -6,8 +6,8 @@ import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.base.viewmodel.BaseVM
import com.meloda.fast.base.viewmodel.VKEvent
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
abstract class BaseVMFragment<VM : BaseVM> : BaseFragment {
@@ -24,10 +24,6 @@ abstract class BaseVMFragment<VM : BaseVM> : BaseFragment {
}
}
protected open fun onEvent(event: VKEvent) {
when (event) {
}
}
protected open fun onEvent(event: VKEvent) {}
}
@@ -5,37 +5,25 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
import com.meloda.fast.base.BaseHolder
import com.meloda.fast.extensions.LiveDataExtensions.add
import com.meloda.fast.extensions.LiveDataExtensions.addAll
import com.meloda.fast.extensions.LiveDataExtensions.clear
import com.meloda.fast.extensions.LiveDataExtensions.get
import com.meloda.fast.extensions.LiveDataExtensions.isEmpty
import com.meloda.fast.extensions.LiveDataExtensions.isNotEmpty
import com.meloda.fast.extensions.LiveDataExtensions.plusAssign
import com.meloda.fast.extensions.LiveDataExtensions.remove
import com.meloda.fast.extensions.LiveDataExtensions.removeAll
import com.meloda.fast.extensions.LiveDataExtensions.removeAt
import com.meloda.fast.extensions.LiveDataExtensions.set
import com.meloda.fast.extensions.LiveDataExtensions.size
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@Suppress("UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate", "CanBeParameter")
abstract class BaseAdapter<Item, VH : BaseHolder>(
abstract class BaseAdapter<Item : BaseItem, VH : BaseHolder>(
var context: Context,
values: ArrayList<Item>
) : RecyclerView.Adapter<VH>() {
values: ArrayList<Item>,
diffUtil: DiffUtil.ItemCallback<Item>
) : ListAdapter<Item, VH>(diffUtil) {
val cleanValues = MutableLiveData<MutableList<Item>>(arrayListOf())
val values = MutableLiveData<MutableList<Item>>(arrayListOf())
protected var inflater: LayoutInflater = LayoutInflater.from(context)
val cleanValues = arrayListOf<Item>()
val values = arrayListOf<Item>()
init {
this.values.value = values
addAll(values)
}
protected var inflater: LayoutInflater = LayoutInflater.from(context)
var itemClickListener: OnItemClickListener? = null
var itemLongClickListener: OnItemLongClickListener? = null
@@ -44,13 +32,13 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
itemLongClickListener = null
}
open fun getItem(position: Int): Item {
override fun getItem(position: Int): Item {
return values[position]
}
fun add(position: Int, item: Item) {
values.add(item, position)
cleanValues.add(item, position)
values.add(position, item)
cleanValues.add(position, item)
}
fun add(item: Item) {
@@ -64,8 +52,8 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
}
fun addAll(position: Int, items: List<Item>) {
values.addAll(items, position)
cleanValues.addAll(items, position)
values.addAll(position, items)
cleanValues.addAll(position, items)
}
fun removeAll(items: List<Item>) {
@@ -0,0 +1,3 @@
package com.meloda.fast.base.adapter
abstract class BaseItem
@@ -0,0 +1,35 @@
package com.meloda.fast.base.adapter
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView
import com.meloda.fast.util.AndroidUtils
import kotlin.math.roundToInt
class EmptyHeaderAdapter(
var context: Context
) : RecyclerView.Adapter<EmptyHeaderAdapter.Holder>() {
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(generateHeaderView())
override fun onBindViewHolder(holder: Holder, position: Int) {
}
override fun getItemCount() = 1
private fun generateHeaderView() = View(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(56).roundToInt()
)
isClickable = false
isEnabled = false
isFocusable = false
isInvisible = true
}
}
@@ -1,14 +1,15 @@
package com.meloda.fast.base
package com.meloda.fast.base.adapter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
open fun bind(position: Int) {
bind(position, mutableListOf())
}
open fun bind(position: Int) {}
open fun bind(position: Int, payloads: MutableList<Any>?) {}
}
}
abstract class BindingHolder<B : ViewBinding>(protected val binding: B) : BaseHolder(binding.root)
@@ -2,11 +2,13 @@ package com.meloda.fast.common
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.database.sqlite.SQLiteDatabase
import android.os.Handler
import android.view.inputmethod.InputMethodManager
import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager
import com.meloda.fast.BuildConfig
@@ -34,6 +36,8 @@ class AppGlobal : Application() {
companion object {
lateinit var inputMethodManager: InputMethodManager
lateinit var preferences: SharedPreferences
lateinit var locale: Locale
lateinit var handler: Handler
@@ -82,6 +86,8 @@ class AppGlobal : Application() {
screenWidth = AndroidUtils.getDisplayWidth()
screenHeight = AndroidUtils.getDisplayHeight()
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}
}
@@ -2,14 +2,11 @@ package com.meloda.fast.fragment.login
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding
import android.webkit.CookieManager
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
@@ -19,14 +16,15 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import coil.load
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputEditText
import coil.transform.RoundedCornersTransformation
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.meloda.fast.R
import com.meloda.fast.base.BaseVMFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.DialogCaptchaBinding
import com.meloda.fast.databinding.FragmentLoginBinding
import com.meloda.fast.fragment.main.MainFragment
import com.meloda.fast.util.KeyboardUtils
@@ -36,7 +34,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@AndroidEntryPoint
class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
@@ -48,6 +45,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
private var lastPassword: String = ""
private var errorTimer: Timer? = null
private var captchaInputLayout: TextInputLayout? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -56,20 +54,28 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
prepareViews()
binding.loginInput.clearFocus()
setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(bundle) }
}
// showCaptchaDialog(
// "https://www.vets4pets.com/syssiteassets/species/cat/kitten/tiny-kitten-in-field.jpg?width=1040",
// ""
// )
}
override fun onEvent(event: VKEvent) {
super.onEvent(event)
when (event) {
is ShowError -> showErrorSnackbar(event.errorDescription)
is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid)
is GoToValidationEvent -> goToValidation(event.redirectUrl)
is GoToMainEvent -> goToMain(event.haveAuthorized)
StartProgressEvent -> onProgressStarted()
StopProgressEvent -> onProgressEnded()
StopProgressEvent -> onProgressStopped()
}
}
@@ -80,7 +86,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
binding.progress.isVisible = true
}
private fun onProgressEnded() {
private fun onProgressStopped() {
binding.loginContainer.isVisible = true
binding.passwordContainer.isVisible = true
binding.auth.isVisible = true
@@ -150,7 +156,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
if (!validateInputData(loginString, passwordString)) return@setOnClickListener
KeyboardUtils.hideKeyboardFrom(it)
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
lifecycleScope.launch {
viewModel.login(
@@ -162,19 +168,29 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
}
}
private fun validateInputData(loginString: String, passwordString: String): Boolean {
// TODO: 7/27/2021 extract strings to resources
private fun validateInputData(
loginString: String?,
passwordString: String?,
captchaCode: String? = null
): Boolean {
var isValidated = true
if (loginString.isEmpty()) {
if (loginString?.isEmpty() == true) {
isValidated = false
setError("Input login", binding.loginLayout)
}
if (passwordString.isEmpty()) {
if (passwordString?.isEmpty() == true) {
isValidated = false
setError("Input password", binding.passwordLayout)
}
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
isValidated = false
setError("Input code", captchaInputLayout!!)
}
return isValidated
}
@@ -198,59 +214,60 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
private fun clearErrors() {
binding.loginLayout.error = ""
binding.passwordLayout.error = ""
captchaInputLayout?.error = ""
}
// TODO: 7/10/2021 extract layout to resources
private fun showCaptchaDialog(captchaImage: String, captchaSid: String) {
val metrics = resources.displayMetrics
val captchaBinding = DialogCaptchaBinding.inflate(layoutInflater, null, false)
captchaInputLayout = captchaBinding.captchaLayout
val width = (metrics.widthPixels / 3.5).roundToInt()
val height = metrics.heightPixels / 7
val image = ShapeableImageView(requireContext()).also {
it.layoutParams = ViewGroup.LayoutParams(width, height)
captchaBinding.image.load(captchaImage) {
crossfade(100)
transformations(RoundedCornersTransformation(4f))
}
val shapeModel = image.shapeAppearanceModel
image.shapeAppearanceModel = shapeModel.withCornerSize { 12f }
image.load(captchaImage) { crossfade(100) }
val captchaCodeEditText = TextInputEditText(requireContext())
captchaCodeEditText.setHint(R.string.captcha_hint)
captchaCodeEditText.layoutParams =
LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val builder = AlertDialog.Builder(requireContext())
.setView(captchaBinding.root)
.setCancelable(false)
.setTitle(R.string.input_captcha)
val layout = LinearLayout(requireContext())
val dialog = builder.show()
layout.orientation = LinearLayout.VERTICAL
layout.gravity = Gravity.CENTER
layout.addView(image)
layout.addView(captchaCodeEditText)
captchaBinding.ok.setOnClickListener {
val captchaCode = captchaBinding.captchaInput.text.toString().trim()
builder.setView(layout)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
val captchaCode = captchaCodeEditText.text.toString().trim()
if (!validateInputData(
loginString = null,
passwordString = null,
captchaCode = captchaCode
)
) return@setOnClickListener
dialog.dismiss()
lifecycleScope.launch {
viewModel.login(
binding.webView,
lastEmail,
lastPassword,
"&captcha_sid=$captchaSid&captcha_key=$captchaCode"
webView = binding.webView,
email = lastEmail,
password = lastPassword,
captchaSid = captchaSid,
captchaKey = captchaCode
)
}
}
captchaBinding.cancel.setOnClickListener { dialog.dismiss() }
}
builder.setTitle(R.string.input_captcha)
builder.show()
private fun showErrorSnackbar(errorDescription: String) {
val snackbar = Snackbar.make(
requireView(),
getString(R.string.error, errorDescription),
Snackbar.LENGTH_LONG
)
snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE
snackbar.show()
}
private fun goToValidation(redirectUrl: String) {
@@ -1,6 +1,7 @@
package com.meloda.fast.fragment.login
import android.os.Bundle
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
@@ -19,20 +20,21 @@ import org.jsoup.Jsoup
class LoginVM : BaseVM() {
private var isWebViewPrepared = true
private var isWebViewPrepared = false
suspend fun login(
webView: WebView,
email: String,
password: String,
captcha: String = ""
captchaSid: String? = null,
captchaKey: String? = null
) {
sendEvent(StartProgressEvent)
val urlToGo = VKAuth.getDirectAuthUrl(email, password, captcha)
val urlToGo = VKAuth.getDirectAuthUrl(email, password, captchaSid, captchaKey)
if (isWebViewPrepared) {
isWebViewPrepared = false
if (!isWebViewPrepared) {
isWebViewPrepared = true
webView.addJavascriptInterface(WebViewHandlerInterface(), "HtmlHandler")
@@ -52,11 +54,14 @@ class LoginVM : BaseVM() {
@Suppress("MoveVariableDeclarationIntoWhen")
private fun checkResponse(response: JSONObject) {
viewModelScope.launch(Dispatchers.Default) {
delay(1500)
sendEvent(StopProgressEvent)
if (response.has("error")) {
sendEvent(StopProgressEvent)
val errorString = response.optString("error")
val errorDescription = response.optString("error_description")
// TODO: 7/27/2021 use this with localized resources
// val errorType = response.optString("error_type")
when (errorString) {
"need_validation" -> {
@@ -68,10 +73,18 @@ class LoginVM : BaseVM() {
val captchaImage = response.optString("captcha_img")
val captchaSid = response.optString("captcha_sid")
Log.d("CAPTCHA", "captchaImage: $captchaImage")
tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid))
}
else -> {
tasksEventChannel.send(ShowError(errorDescription))
}
}
} else {
delay(1500)
sendEvent(StopProgressEvent)
val userId = response.optInt("user_id", -1)
val accessToken = response.optString("access_token")
@@ -108,6 +121,7 @@ class LoginVM : BaseVM() {
}
data class ShowError(val errorDescription: String) : VKEvent()
data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent()
data class GoToValidationEvent(val redirectUrl: String) : VKEvent()
data class GoToMainEvent(val haveAuthorized: Boolean = true) : VKEvent()
@@ -0,0 +1,41 @@
package com.meloda.fast.fragment.messages
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BindingHolder
import com.meloda.fast.databinding.ItemConversationBinding
class ConversationsAdapter(context: Context, values: ArrayList<VKConversation>) :
BaseAdapter<VKConversation, ConversationsAdapter.ItemHolder>(
context, values, COMPARATOR
) {
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<VKConversation>() {
override fun areItemsTheSame(
oldItem: VKConversation,
newItem: VKConversation
) = false
override fun areContentsTheSame(
oldItem: VKConversation,
newItem: VKConversation
) = false
}
}
inner class ItemHolder(binding: ItemConversationBinding) :
BindingHolder<ItemConversationBinding>(binding) {
override fun bind(position: Int) {
binding.title.text = getItem(position).title ?: "HUI"
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ItemHolder(ItemConversationBinding.inflate(inflater, parent, false))
}
@@ -3,19 +3,81 @@ package com.meloda.fast.fragment.messages
import android.os.Bundle
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import com.meloda.fast.R
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.base.BaseVMFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.FragmentConversationsBinding
import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.roundToInt
@AndroidEntryPoint
class ConversationsFragment : BaseFragment(R.layout.fragment_conversations) {
class ConversationsFragment : BaseVMFragment<ConversationsVM>(R.layout.fragment_conversations) {
override val viewModel: ConversationsVM by viewModels()
private val binding: FragmentConversationsBinding by viewBinding()
private lateinit var adapter: ConversationsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prepareViews()
viewModel.loadConversations()
}
override fun onEvent(event: VKEvent) {
super.onEvent(event)
when (event) {
StartProgressEvent -> onProgressStarted()
StopProgressEvent -> onProgressStopped()
}
}
private fun onProgressStarted() {
if (adapter.isEmpty())
binding.progressBar.isVisible = true
}
private fun onProgressStopped() {
binding.progressBar.isVisible = false
}
private fun prepareViews() {
prepareRecyclerView()
prepareRefreshLayout()
}
private fun prepareRecyclerView() {
}
private fun prepareRefreshLayout() {
with(binding.refreshLayout) {
setProgressViewOffset(
true,
AndroidUtils.px(40).roundToInt(),
AndroidUtils.px(96).roundToInt()
)
setProgressBackgroundColorSchemeColor(
AndroidUtils.getThemeAttrColor(
requireContext(),
R.attr.colorSurface
)
)
setColorSchemeColors(
AndroidUtils.getThemeAttrColor(
requireContext(),
R.attr.colorAccent
)
)
setOnRefreshListener { }
}
}
}
@@ -0,0 +1,13 @@
package com.meloda.fast.fragment.messages
import androidx.lifecycle.viewModelScope
import com.meloda.fast.base.viewmodel.BaseVM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ConversationsVM : BaseVM() {
fun loadConversations() = viewModelScope.launch(Dispatchers.Default) {
}
}
@@ -1,16 +1,16 @@
package com.meloda.fast.util
import android.view.View
import com.meloda.fast.common.AppGlobal
//TODO
object KeyboardUtils {
fun hideKeyboardFrom(view: View) {
// AppGlobal.inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
fun hideKeyboardFrom(focusedView: View?) {
AppGlobal.inputMethodManager.hideSoftInputFromWindow(focusedView?.windowToken, 0)
}
fun showKeyboard(focusedView: View) {
// AppGlobal.inputMethodManager.showSoftInput(focusedView, 0)
fun showKeyboard(viewToFocus: View) {
AppGlobal.inputMethodManager.showSoftInput(viewToFocus, 0)
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M12,12H19C18.47,16.11 15.72,19.78 12,20.92V12H5V6.3L12,3.19M12,1L3,5V11C3,16.55 6.84,21.73 12,23C17.16,21.73 21,16.55 21,11V5L12,1Z" />
</vector>
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="50dp"
tools:src="@tools:sample/backgrounds/scenic" />
<LinearLayout
android:id="@+id/captchaContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/captchaImage"
style="@style/AppTheme.Login.EditText.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_security"
app:tint="?colorAccent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/captchaLayout"
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/captchaInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/captcha_hint"
android:imeOptions="actionGo"
android:lines="1"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_weight="1"
android:text="@android:string/cancel"
app:elevation="0dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ok"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/ok"
app:elevation="0dp" />
</LinearLayout>
</LinearLayout>
</layout>
@@ -1,45 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="conversations" />
<com.google.android.material.appbar.AppBarLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_conversation" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:titleCentered="true" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/recycler_view" />
android:background="@drawable/toolbar_background"
android:elevation="3dp"
app:title="@string/conversations"
app:titleCentered="true" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" />
<include
layout="@layout/no_items_view"
+6 -6
View File
@@ -73,7 +73,8 @@
android:id="@+id/loginLayout"
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:boxStrokeErrorColor="@android:color/transparent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginInput"
@@ -81,12 +82,10 @@
android:layout_height="48dp"
android:hint="@string/login_hint"
android:imeOptions="actionGo"
android:inputType="textEmailAddress"
android:textCursorDrawable="@null" />
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
@@ -110,6 +109,7 @@
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxStrokeErrorColor="@android:color/transparent"
app:passwordToggleEnabled="true"
app:passwordToggleTint="?colorAccent">
@@ -117,9 +117,9 @@
android:id="@+id/passwordInput"
android:layout_width="match_parent"
android:layout_height="48dp"
android:imeOptions="actionGo"
android:hint="@string/password_login_hint"
android:inputType="textPassword"
android:textCursorDrawable="@null" />
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
+16 -16
View File
@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/conversationRoot"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
@@ -12,7 +12,7 @@
android:paddingBottom="2dp">
<LinearLayout
android:id="@+id/conversationContainer"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="6dp"
@@ -27,15 +27,15 @@
android:layout_weight="0">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversationAvatar"
android:id="@+id/peerAvatar"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|end"
tools:actualImageResource="@color/accent"
app:shapeAppearanceOverlay="@style/CircleImageView.56"
tools:src="?colorAccent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/conversationUserOnline"
android:id="@+id/peerOnline"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="bottom|end"
@@ -59,7 +59,7 @@
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/conversationType"
android:id="@+id/type"
android:layout_width="20dp"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
@@ -70,7 +70,7 @@
tools:src="@drawable/ic_dialog_type_conversation" />
<TextView
android:id="@+id/conversationTitle"
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@@ -93,17 +93,17 @@
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversationUserAvatar"
android:layout_width="20dp"
android:layout_height="20dp"
android:id="@+id/fromAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="6dp"
tools:actualImageResource="@color/accent"
app:shapeAppearanceOverlay="@style/CircleImageView.24"
tools:src="?colorAccent"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/conversationTextAttachment"
android:id="@+id/textAttachment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -114,7 +114,7 @@
tools:visibility="visible" />
<TextView
android:id="@+id/conversationText"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -138,7 +138,7 @@
android:paddingEnd="6dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversationOut"
android:id="@+id/out"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_gravity="bottom"
@@ -151,7 +151,7 @@
</LinearLayout>
<TextView
android:id="@+id/conversationDate"
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|top"
@@ -164,7 +164,7 @@
tools:text="now" />
<TextView
android:id="@+id/conversationCounter"
android:id="@+id/counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#ffffff</color>
<color name="primary">@color/accent</color>
<color name="primaryDark">#ffffff</color>
<color name="accent">#4284F4</color>
<color name="navigationBar">#ffffff</color>
+3 -1
View File
@@ -150,9 +150,11 @@
<string name="password_login_hint">Password</string>
<string name="email_login_hint">E-mail or phone number</string>
<string name="log_in">Log in</string>
<string name="captcha_hint">Captcha</string>
<string name="captcha_hint">Captcha code</string>
<string name="input_captcha">Input code from picture</string>
<string name="login_hint">Login</string>
<string name="conversations">Conversations</string>
</resources>
+14
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme.ActivityAnimation" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/activity_open_enter</item>
<item name="android:activityOpenExitAnimation">@anim/activity_open_exit</item>
@@ -11,4 +12,17 @@
<item name="android:windowEnterAnimation">@anim/slide_up</item>
<item name="android:windowExitAnimation">@anim/slide_down</item>
</style>
<style name="CircleImageView">
<item name="cornerFamily">rounded</item>
</style>
<style name="CircleImageView.56">
<item name="cornerRadius">56dp</item>
</style>
<style name="CircleImageView.24">
<item name="cornerRadius">24dp</item>
</style>
</resources>
+6 -11
View File
@@ -27,6 +27,7 @@
<item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">
@android:color/transparent
</item>
</style>
<style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.PrimarySurface">
@@ -37,21 +38,15 @@
</style>
<style name="Toolbar.Title" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textSize">24sp</item>
<item name="android:fontFamily">@font/tt_commons_medium</item>
<item name="android:textSize">20sp</item>
<item name="android:textColor">@color/accent</item>
<item name="android:fontFamily">@font/google_sans_medium</item>
</style>
<style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert" />
<style name="AppTheme.Login.EditText" parent="">
<item name="android:layout_height">52dp</item>
<item name="android:background">@drawable/edittext_filled_background</item>
<item name="android:paddingStart">16dp</item>
<item name="android:paddingEnd">16dp</item>
<item name="android:layout_marginEnd">16dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textColorHint">?textColorSecondary</item>
<style name="AppTheme.Login.EditText" parent="Widget.Design.TextInputEditText">
<item name="android:layout_height">48dp</item>
<item name="fontFamily">@font/google_sans_regular</item>
<item name="android:singleLine">true</item>
<item name="android:maxLines">1</item>
+1 -1
View File
@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20")
classpath("com.android.tools.build:gradle:4.2.2")
classpath("com.android.tools.build:gradle:7.0.0")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5")
classpath("com.google.dagger:hilt-android-gradle-plugin:2.37")
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip