illegal token checking

fixes
This commit is contained in:
2021-09-17 16:25:26 +03:00
parent a3a282c32c
commit d1ed98691c
57 changed files with 968 additions and 251 deletions
@@ -5,7 +5,7 @@ object VKConstants {
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
const val USER_FIELDS =
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex"
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
const val API_VERSION = "5.132"
@@ -3,7 +3,12 @@ package com.meloda.fast.api
import org.json.JSONObject
import java.io.IOException
class VKException(var url: String = "", var description: String = "", var error: String) :
open class VKException(
var url: String = "",
var code: Int = -1,
var description: String = "",
var error: String
) :
IOException(description) {
var captcha: Pair<String, String>? = null
@@ -13,18 +13,18 @@ import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.*
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import com.meloda.fast.api.network.VKErrors
import com.meloda.fast.api.network.VkErrors
object VkUtils {
fun isValidationRequired(throwable: Throwable): Boolean {
if (throwable !is VKException) return false
return throwable.error == VKErrors.NEED_VALIDATION
return throwable.error == VkErrors.NEED_VALIDATION
}
fun isCaptchaRequired(throwable: Throwable): Boolean {
if (throwable !is VKException) return false
return throwable.error == VKErrors.NEED_CAPTCHA
return throwable.error == VkErrors.NEED_CAPTCHA
}
fun prepareMessageText(text: String): String {
@@ -94,9 +94,7 @@ object VkUtils {
}
BaseVkAttachmentItem.AttachmentType.STICKER -> {
val sticker = baseAttachment.sticker ?: continue
attachments += VkSticker(
link = sticker.images[0].url
)
attachments += sticker.asVkSticker()
}
BaseVkAttachmentItem.AttachmentType.GIFT -> {
val gift = baseAttachment.gift ?: continue
@@ -275,9 +273,9 @@ object VkUtils {
else -> return null
} ?: return null
val actionMessage = message.actionMessage ?: return null
val actionMessage = message.actionMessage
"$prefix pinned message «$actionMessage»"
"$prefix pinned message ${if (actionMessage == null) "" else "«$actionMessage»"}".trim()
}
VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
val prefix = when {
@@ -1,11 +1,11 @@
package com.meloda.fast.api.base
import com.google.gson.annotations.SerializedName
import java.io.IOException
import com.meloda.fast.api.VKException
data class ApiError(
@SerializedName("error_code")
val errorCode: Int,
@SerializedName("error_msg")
override var message: String
) : IOException()
) : VKException(error = message, code = errorCode)
@@ -33,8 +33,8 @@ data class VkConversation(
fun isUser() = type == "user"
fun isGroup() = type == "group"
fun isInUnread() = inRead != lastMessageId
fun isOutUnread() = outRead != lastMessageId
fun isInUnread() = inRead < lastMessageId
fun isOutUnread() = outRead < lastMessageId
fun isUnread() = isInUnread() || isOutUnread()
@@ -12,8 +12,9 @@ data class VkGroup(
val id: Int,
val name: String,
val screenName: String,
val photo200: String?
): Parcelable {
val photo200: String?,
val membersCount: Int?
) : Parcelable {
override fun toString() = name.trim()
@@ -42,14 +42,31 @@ data class VkMessage(
fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation) = conversation.outRead < id
fun isRead(conversation: VkConversation) =
if (isOut) conversation.outRead < id
else conversation.inRead < id
fun getPreparedAction(): Action? {
if (action == null) return null
return Action.parse(action)
}
fun changeId(id: Int) = VkMessage(
fun copyMessage(
id: Int = this.id,
text: String? = this.text,
isOut: Boolean = this.isOut,
peerId: Int = this.peerId,
fromId: Int = this.fromId,
date: Int = this.date,
randomId: Int = this.randomId,
action: String? = this.action,
actionMemberId: Int? = this.actionMemberId,
actionText: String? = this.actionText,
actionConversationMessageId: Int? = this.actionConversationMessageId,
actionMessage: String? = this.actionMessage,
geoType: String? = this.geoType,
important: Boolean = this.important
) = VkMessage(
id = id,
text = text,
isOut = isOut,
@@ -64,7 +81,10 @@ data class VkMessage(
actionMessage = actionMessage,
geoType = geoType,
important = important
)
).also {
it.attachments = attachments
it.forwards = forwards
}
enum class Action(val value: String) {
CHAT_CREATE("chat_create"),
@@ -13,9 +13,13 @@ data class VkUser(
val firstName: String,
val lastName: String,
val online: Boolean,
val photo200: String?
val photo200: String?,
val lastSeen: Int?,
val lastSeenStatus: String?
) : Parcelable {
override fun toString() = "$firstName $lastName".trim()
override fun toString() = fullName
val fullName get() = "$firstName $lastName".trim()
}
@@ -1,8 +1,23 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
import com.meloda.fast.api.model.base.attachments.StickerSize
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkSticker(
val link: String
) : VkAttachment()
val id: Int,
val productId: Int,
val images: List<BaseVkSticker.Image>,
val backgroundImages: List<BaseVkSticker.Image>
) : VkAttachment() {
fun urlForSize(@StickerSize size: Int): String? {
for (image in images) {
if (image.width == size) return image.url
}
return null
}
}
@@ -25,14 +25,17 @@ data class BaseVkGroup(
@SerializedName("photo_100")
val photo100: String?,
@SerializedName("photo_200")
val photo200: String?
val photo200: String?,
@SerializedName("members_count")
val membersCount: Int?
) : Parcelable {
fun asVkGroup() = VkGroup(
id = -id,
name = name,
screenName = screenName,
photo200 = photo200
photo200 = photo200,
membersCount = membersCount
)
}
@@ -0,0 +1,12 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkLongPoll(
val server: String,
val key: String,
val ts: Int,
val pts: Int
) : Parcelable
@@ -52,7 +52,9 @@ data class BaseVkUser(
firstName = firstName,
lastName = lastName,
online = online == 1,
photo200 = photo200
photo200 = photo200,
lastSeen = onlineInfo?.lastSeen,
lastSeenStatus = onlineInfo?.status
)
}
@@ -1,7 +1,9 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import androidx.annotation.IntDef
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkSticker
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -18,6 +20,13 @@ data class BaseVkSticker(
val animations: List<Animation>?
) : Parcelable {
fun asVkSticker() = VkSticker(
id = stickerId,
productId = productId,
images = images,
backgroundImages = imagesWithBackground
)
@Parcelize
data class Image(
val width: Int,
@@ -31,5 +40,7 @@ data class BaseVkSticker(
val url: String
) : Parcelable
}
}
@IntDef(64, 128, 256, 352)
annotation class StickerSize
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -59,7 +58,6 @@ data class MessagesSendRequest(
@Parcelize
data class MessagesMarkAsImportantRequest(
@SerializedName("message_ids")
val messagesIds: List<Int>,
val important: Boolean
) : Parcelable {
@@ -70,4 +68,17 @@ data class MessagesMarkAsImportantRequest(
"important" to (if (important) 1 else 0).toString()
)
}
@Parcelize
data class MessagesGetLongPollServerRequest(
val needPts: Boolean,
val version: Int
) : Parcelable {
val map
get() = mutableMapOf(
"need_pts" to (if (needPts) 1 else 0).toString(),
"version" to version.toString()
)
}
@@ -1,6 +1,6 @@
package com.meloda.fast.api.network
object ErrorCodes {
object VkErrorCodes {
const val UNKNOWN_ERROR = 1
const val APP_DISABLED = 2
const val UNKNOWN_METHOD = 3
@@ -41,7 +41,7 @@ object ErrorCodes {
const val ACCESS_TO_DOC_DENIED = 1153
}
object VKErrors {
object VkErrors {
const val UNKNOWN = "unknown_error"
const val NEED_VALIDATION = "need_validation"
@@ -1,10 +1,9 @@
package com.meloda.fast.api.network
import com.meloda.fast.api.VKException
import com.meloda.fast.api.base.ApiResponse
import okhttp3.Request
import okio.IOException
import okio.Timeout
import org.json.JSONObject
import retrofit2.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
@@ -76,11 +75,16 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val result: Answer<T> = if (response.isSuccessful)
Answer.Success(response.body() as T)
else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
if (result is Answer.Error) if (checkErrors(call, result)) return
val result: Answer<T> =
if (response.isSuccessful) {
val baseBody = response.body()
if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T)
else {
val body = baseBody as ApiResponse<*>
if (body.error != null) Answer.Error(body.error)
else Answer.Success(body as T)
}
} else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
callback.onResponse(proxy, Response.success(result))
}
@@ -91,23 +95,6 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
Response.success(Answer.Error(throwable = error))
)
}
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean {
val json = JSONObject(result.throwable.message ?: "{}")
return if (json.has("error")) {
val error = json.optString("error", "")
val description = json.optString("error_description", "")
val exception = VKException(
error = error,
description = description,
).also { it.json = json }
onFailure(call, exception)
true
} else false
}
}
override fun timeout(): Timeout {
@@ -22,6 +22,8 @@ object VKUrls {
const val GetHistory = "$API/messages.getHistory"
const val Send = "$API/messages.send"
const val MarkAsImportant = "$API/messages.markAsImportant"
const val GetLongPollServer = "$API/messages.getLongPollServer"
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.network.repo.MessagesRepo
@@ -21,4 +22,7 @@ class MessagesDataSource @Inject constructor(
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
repo.markAsImportant(params.map)
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
repo.getLongPollServer(params.map)
}
@@ -0,0 +1,18 @@
package com.meloda.fast.api.network.repo
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.Answer
import org.json.JSONObject
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.QueryMap
interface LongPollRepo {
@GET("https://{serverUrl}")
suspend fun getResponse(
@Path("serverUrl") serverUrl: String,
@QueryMap params: Map<String, String>
): Answer<ApiResponse<JSONObject>>
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.network.repo
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.BaseVkLongPoll
import com.meloda.fast.api.model.response.MessagesGetHistoryResponse
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.VKUrls
@@ -22,4 +23,8 @@ interface MessagesRepo {
@POST(VKUrls.Messages.MarkAsImportant)
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
@FormUrlEncoded
@POST(VKUrls.Messages.GetLongPollServer)
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>>
}
@@ -1,10 +1,16 @@
package com.meloda.fast.base
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.R
import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.IllegalTokenEvent
import com.meloda.fast.base.viewmodel.VKEvent
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
@@ -24,6 +30,16 @@ abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
}
}
protected open fun onEvent(event: VKEvent) {}
protected open fun onEvent(event: VKEvent) {
if (event is IllegalTokenEvent) {
Toast.makeText(
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
).show()
UserConfig.clear()
requireActivity().finishAffinity()
requireActivity().startActivity(Intent(requireContext(), MainActivity::class.java))
}
}
}
@@ -4,9 +4,10 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.VKException
import com.meloda.fast.api.base.ApiError
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.VKErrors
import com.meloda.fast.util.Utils
import com.meloda.fast.api.network.VkErrorCodes
import com.meloda.fast.api.network.VkErrors
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
@@ -26,23 +27,32 @@ abstract class BaseViewModel : ViewModel() {
onStart?.invoke()
when (val response = job()) {
is Answer.Success -> onAnswer(response.data)
is Answer.Error -> onError?.invoke(response.throwable)
is Answer.Error -> {
checkErrors(response.throwable)
onError?.invoke(response.throwable)
}
}
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
protected suspend fun checkErrors(throwable: Throwable) {
// TODO: 8/31/2021 check illegal token
if (throwable is VKException) {
private suspend fun checkErrors(throwable: Throwable) {
if (throwable is ApiError) {
when (throwable.errorCode) {
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
sendEvent(IllegalTokenEvent)
return
}
}
} else if (throwable is VKException) {
when (throwable.error) {
VKErrors.NEED_CAPTCHA -> {
VkErrors.NEED_CAPTCHA -> {
throwable.captcha =
(throwable.json?.optString("captcha_sid")
?: "") to (throwable.json?.optString("captcha_img") ?: "")
return
}
VKErrors.NEED_VALIDATION -> {
VkErrors.NEED_VALIDATION -> {
throwable.validationSid = throwable.json?.optString("validation_sid")
return
}
@@ -51,7 +61,7 @@ abstract class BaseViewModel : ViewModel() {
}
}
tasksEventChannel.send(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable)))
sendEvent(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable)))
}
}
@@ -7,6 +7,10 @@ data class ShowDialogInfoEvent(
val negativeBtn: String? = null
) : VKEvent()
data class ErrorEvent(val errorText: String) : VKEvent()
object IllegalTokenEvent : VKEvent()
object StartProgressEvent : VKEvent()
object StopProgressEvent : VKEvent()
@@ -18,7 +18,7 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class,
VkGroup::class
],
version = 16,
version = 18,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
@@ -2,16 +2,13 @@ package com.meloda.fast.di
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.meloda.fast.api.network.AuthInterceptor
import com.meloda.fast.api.network.ResultCallFactory
import com.meloda.fast.api.network.datasource.AuthDataSource
import com.meloda.fast.api.network.datasource.ConversationsDataSource
import com.meloda.fast.api.network.datasource.MessagesDataSource
import com.meloda.fast.api.network.datasource.UsersDataSource
import com.meloda.fast.api.network.AuthInterceptor
import com.meloda.fast.api.network.ResultCallFactory
import com.meloda.fast.api.network.repo.AuthRepo
import com.meloda.fast.api.network.repo.ConversationsRepo
import com.meloda.fast.api.network.repo.MessagesRepo
import com.meloda.fast.api.network.repo.UsersRepo
import com.meloda.fast.api.network.repo.*
import com.meloda.fast.database.dao.ConversationsDao
import com.meloda.fast.database.dao.MessagesDao
import com.meloda.fast.database.dao.UsersDao
@@ -81,6 +78,10 @@ object NetworkModule {
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
retrofit.create(MessagesRepo::class.java)
@Provides
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo =
retrofit.create(LongPollRepo::class.java)
@Provides
@Singleton
fun provideAuthDataSource(
@@ -88,9 +88,10 @@ class ConversationsAdapter constructor(
}
binding.avatar.isVisible = avatar != null
binding.avatarPlaceholder.isVisible = avatar == null
if (avatar == null) {
binding.avatarPlaceholder.isVisible = true
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
binding.placeholderBack.setImageDrawable(
ColorDrawable(
@@ -114,7 +115,13 @@ class ConversationsAdapter constructor(
binding.avatar.setImageDrawable(null)
}
} else {
binding.avatar.load(avatar) { crossfade(200) }
binding.avatar.load(avatar) {
crossfade(200)
target {
binding.avatarPlaceholder.isVisible = false
binding.avatar.setImageDrawable(it)
}
}
}
binding.online.isVisible = chatUser?.online == true
@@ -155,7 +162,8 @@ class ConversationsAdapter constructor(
message = message
) else null
val messageText = (if (actionMessage != null ||
val messageText = (if (
actionMessage != null ||
forwardsMessage != null ||
attachmentText != null
) ""
@@ -45,6 +45,7 @@ class ConversationsFragment :
}
private var isPaused = false
private var isExpanded = true
override fun onPause() {
super.onPause()
@@ -63,7 +64,9 @@ class ConversationsFragment :
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
}
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
if (isPaused) return@OnOffsetChangedListener
if (verticalOffset <= -100) {
binding.avatarContainer.alpha = 0f
return@OnOffsetChangedListener
@@ -6,6 +6,7 @@ import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
@@ -48,6 +49,11 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
private var captchaInputLayout: TextInputLayout? = null
private var validationInputLayout: TextInputLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.unknownErrorDefaultText = getString(R.string.unknown_error_occurred)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -156,7 +162,6 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
)
}
// TODO: 7/27/2021 extract strings to resources
private fun validateInputData(
loginString: String?,
passwordString: String?,
@@ -167,22 +172,22 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
if (loginString?.isEmpty() == true) {
isValidated = false
setError("Input login", binding.loginLayout)
setError(getString(R.string.input_login_hint), binding.loginLayout)
}
if (passwordString?.isEmpty() == true) {
isValidated = false
setError("Input password", binding.passwordLayout)
setError(getString(R.string.input_password_hint), binding.passwordLayout)
}
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
isValidated = false
setError("Input code", captchaInputLayout!!)
setError(getString(R.string.input_code_hint), captchaInputLayout!!)
}
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
isValidated = false
setError("Input code", validationInputLayout!!)
setError(getString(R.string.input_code_hint), validationInputLayout!!)
}
return isValidated
@@ -281,9 +286,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
}
// TODO: 8/31/2021 show snackbar
private fun showValidationRequired() {
Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show()
}
private fun showErrorSnackbar(errorDescription: String) {
@@ -6,12 +6,9 @@ import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.VKException
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.network.datasource.AuthDataSource
import com.meloda.fast.api.model.request.RequestAuthDirect
import com.meloda.fast.base.viewmodel.BaseViewModel
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.api.network.datasource.AuthDataSource
import com.meloda.fast.base.viewmodel.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -21,6 +18,8 @@ class LoginViewModel @Inject constructor(
private val dataSource: AuthDataSource
) : BaseViewModel() {
lateinit var unknownErrorDefaultText: String
fun login(
login: String,
password: String,
@@ -45,8 +44,8 @@ class LoginViewModel @Inject constructor(
)
},
onAnswer = {
// TODO: 8/31/2021 do something
if (it.userId == null || it.accessToken == null) {
sendEvent(ErrorEvent(unknownErrorDefaultText))
return@makeJob
}
@@ -56,7 +55,6 @@ class LoginViewModel @Inject constructor(
sendEvent(SuccessAuth(haveAuthorized = true))
},
onError = {
checkErrors(it)
if (it !is VKException) return@makeJob
twoFaCode?.let { sendEvent(CodeSent) }
@@ -21,9 +21,8 @@ class MainFragment : BaseViewModelFragment<MainViewModel>(R.layout.fragment_main
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) setupBottomBar()
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin)
else if (savedInstanceState == null) setupBottomBar()
}
private fun setupBottomBar() {
@@ -1,10 +1,14 @@
package com.meloda.fast.screens.messages
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import coil.load
@@ -15,9 +19,9 @@ import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkPhoto
import com.meloda.fast.api.model.attachments.VkSticker
import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.databinding.*
import com.meloda.fast.util.AndroidUtils
import kotlin.math.roundToInt
@@ -31,30 +35,31 @@ class MessagesHistoryAdapter constructor(
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.Holder>(context, values, COMPARATOR) {
override fun getItemViewType(position: Int): Int {
var viewType: Int = when {
isPositionHeader(position) -> HEADER
isPositionFooter(position) -> FOOTER
else -> -1
when {
isPositionHeader(position) -> return HEADER
isPositionFooter(position) -> return FOOTER
}
if (viewType == -1) {
getItem(position).let {
if (it.action != null) viewType = SERVICE
getItem(position).let { message ->
if (message.action != null) return SERVICE
val attachments = it.attachments ?: return@let
if (attachments.isEmpty()) return@let
if (!message.attachments.isNullOrEmpty()) {
val attachments = message.attachments ?: return@let
if (VkUtils.isAttachmentsHaveOneType(attachments) &&
attachments[0] is VkPhoto
) {
return if (it.isOut) ATTACHMENT_PHOTOS_OUT else ATTACHMENT_PHOTOS_IN
}
) return if (message.isOut) ATTACHMENT_PHOTOS_OUT
else ATTACHMENT_PHOTOS_IN
if (it.isOut) viewType = OUTGOING
if (!it.isOut) viewType = INCOMING
if (attachments[0] is VkSticker) return if (message.isOut) ATTACHMENT_STICKER_OUT
else ATTACHMENT_STICKER_IN
}
if (message.isOut) return OUTGOING
if (!message.isOut) return INCOMING
}
return viewType
return -1
}
private fun isPositionHeader(position: Int) = position == 0
@@ -67,11 +72,17 @@ class MessagesHistoryAdapter constructor(
SERVICE -> ServiceMessage(
ItemMessageServiceBinding.inflate(inflater, parent, false)
)
ATTACHMENT_STICKER_IN -> AttachmentStickerIncoming(
ItemMessageAttachmentStickerInBinding.inflate(inflater, parent, false)
)
ATTACHMENT_STICKER_OUT -> AttachmentStickerOutgoing(
ItemMessageAttachmentStickerOutBinding.inflate(inflater, parent, false)
)
ATTACHMENT_PHOTOS_IN -> AttachmentPhotosIncoming(
ItemMessageAttachmentPhotoInBinding.inflate(inflater, parent, false)
ItemMessageAttachmentPhotosInBinding.inflate(inflater, parent, false)
)
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
ItemMessageAttachmentPhotoOutBinding.inflate(inflater, parent, false)
ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false)
)
OUTGOING -> OutgoingMessage(
ItemMessageOutBinding.inflate(inflater, parent, false)
@@ -79,7 +90,7 @@ class MessagesHistoryAdapter constructor(
INCOMING -> IncomingMessage(
ItemMessageInBinding.inflate(inflater, parent, false)
)
else -> Holder()
else -> throw IllegalStateException("Wrong viewType: $viewType")
}
}
@@ -107,56 +118,126 @@ class MessagesHistoryAdapter constructor(
inner class Footer(v: View) : Holder(v)
inner class AttachmentPhotosIncoming(
private val binding: ItemMessageAttachmentPhotoInBinding
inner class IncomingMessage(
private val binding: ItemMessageInBinding
) : Holder(binding.root) {
private val backgroundNormal =
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background)
private val backgroundMiddle =
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background_middle)
init {
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
AndroidUtils.px(12)
}
MessagesManager.setRootMaxWidth(binding.bubble)
}
override fun bind(position: Int) {
val message = getItem(position)
val photo = message.attachments?.get(0) as? VkPhoto ?: return
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
val size = photo.sizeOfType('m') ?: return
binding.unread.isVisible = message.isRead(conversation)
binding.photo.layoutParams = FrameLayout.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
binding.bubble.background =
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle
else backgroundNormal
if (!message.isPeerChat()) {
binding.title.isVisible = false
binding.avatar.isVisible = false
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
} else {
binding.title.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
binding.spacer.isVisible = binding.title.isVisible
binding.avatar.visibility =
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
else if (nextMessage.date - message.date >= 60) View.VISIBLE
else View.INVISIBLE
}
val messageUser: VkUser? = if (message.isUser()) {
profiles[message.fromId]
} else null
val messageGroup: VkGroup? = if (message.isGroup()) {
groups[message.fromId]
} else null
MessagesManager.loadMessageAvatar(
message = message,
messageUser = messageUser,
messageGroup = messageGroup,
imageView = binding.avatar
)
binding.photo.load(size.url)
}
val title = when {
message.isUser() && messageUser != null -> messageUser.firstName
message.isGroup() && messageGroup != null -> messageGroup.name
else -> null
}
binding.title.text = title
binding.title.measure(0, 0)
if (binding.title.isVisible) {
binding.bubble.minimumWidth = binding.title.measuredWidth + 60
} else {
binding.bubble.minimumWidth = 0
}
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
}
}
inner class AttachmentPhotosOutgoing(
private val binding: ItemMessageAttachmentPhotoOutBinding
inner class OutgoingMessage(
private val binding: ItemMessageOutBinding
) : Holder(binding.root) {
private val backgroundNormal =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddle =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
private val backgroundStroke =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
private val backgroundMiddleStroke =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
init {
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
AndroidUtils.px(12)
}
MessagesManager.setRootMaxWidth(binding.bubble)
}
override fun bind(position: Int) {
val message = getItem(position)
val photo = message.attachments?.get(0) as? VkPhoto ?: return
val prevMessage = getOrNull(position - 1)
val size = photo.sizeOfType('m') ?: return
binding.text.text = message.text ?: "[no_message]"
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
binding.unread.isVisible = message.isRead(conversation)
binding.photo.load(size.url)
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
binding.bubble.background =
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle
else backgroundNormal
binding.bubbleStroke.background =
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStroke
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStroke
else backgroundStroke
}
}
@@ -167,6 +248,12 @@ class MessagesHistoryAdapter constructor(
private val youPrefix = context.getString(R.string.you_message_prefix)
init {
binding.photo.shapeAppearanceModel.run {
withCornerSize { AndroidUtils.px(4) }
}
}
override fun bind(position: Int) {
val message = getItem(position)
@@ -188,49 +275,112 @@ class MessagesHistoryAdapter constructor(
messageUser = messageUser,
messageGroup = messageGroup
)
val attachments = message.attachments ?: return
attachments[0].let { attachment ->
if (attachment !is VkPhoto) return@let
binding.photo.isVisible = true
val size = attachment.sizeOfType('m') ?: return@let
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
size.width,
size.height
)
binding.photo.load(size.url) {
crossfade(150)
fallback(ColorDrawable(Color.LTGRAY))
}
}
}
}
inner class OutgoingMessage(
private val binding: ItemMessageOutBinding
inner class AttachmentPhotosIncoming(
private val binding: ItemMessageAttachmentPhotosInBinding
) : Holder(binding.root) {
init {
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.75).roundToInt()
}
override fun bind(position: Int) {
val message = getItem(position)
binding.text.text = message.text ?: "[no_message]"
binding.unread.isVisible = message.isRead(conversation)
MessagesManager.loadPhotos(
context = context,
message = message,
binding.photosContainer
)
}
}
inner class IncomingMessage(
private val binding: ItemMessageInBinding
inner class AttachmentPhotosOutgoing(
private val binding: ItemMessageAttachmentPhotosOutBinding
) : Holder(binding.root) {
init {
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
}
override fun bind(position: Int) {
val message = getItem(position)
MessagesManager.loadPhotos(
context = context,
message = message,
photosContainer = binding.photosContainer
)
}
}
inner class AttachmentStickerOutgoing(
private val binding: ItemMessageAttachmentStickerOutBinding
) : Holder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
binding.title.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
if (!message.isPeerChat()) {
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
} else {
binding.spacer.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
}
binding.avatar.visibility =
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
else if (nextMessage.date - message.date >= 60) View.VISIBLE
else View.INVISIBLE
val sticker = message.attachments?.get(0) as? VkSticker ?: return
val url = sticker.urlForSize(352)!!
binding.photo.layoutParams.also {
it.width = 352
it.height = 352
}
binding.photo.load(url) { crossfade(150) }
}
}
inner class AttachmentStickerIncoming(
private val binding: ItemMessageAttachmentStickerInBinding
) : Holder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
if (!message.isPeerChat()) {
binding.avatar.isVisible = false
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
} else {
binding.spacer.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
binding.avatar.visibility =
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
else if (nextMessage.date - message.date >= 60) View.VISIBLE
else View.INVISIBLE
}
val messageUser: VkUser? = if (message.isUser()) {
profiles[message.fromId]
@@ -246,24 +396,34 @@ class MessagesHistoryAdapter constructor(
else -> null
}
binding.avatar.load(avatar) { crossfade(100) }
val title = when {
message.isUser() && messageUser != null -> messageUser.firstName
message.isUser() && messageUser != null -> messageUser.fullName
message.isGroup() && messageGroup != null -> messageGroup.name
else -> null
}
binding.avatar.load(avatar) { crossfade(100) }
binding.text.text = message.text ?: "[no_message]"
binding.title.text = title
binding.title.measure(0, 0)
if (binding.title.isVisible) {
binding.bubble.minimumWidth = binding.title.measuredWidth + 60
} else {
binding.bubble.minimumWidth = 0
binding.avatar.setOnLongClickListener {
Toast.makeText(context, title, Toast.LENGTH_SHORT).apply {
setGravity(
Gravity.START or Gravity.BOTTOM,
0,
-50
)
}.show()
true
}
val sticker = message.attachments?.get(0) as? VkSticker ?: return
val url = sticker.urlForSize(352)!!
binding.photo.layoutParams.also {
it.width = 352
it.height = 352
}
binding.photo.load(url) { crossfade(150) }
}
}
@@ -281,8 +441,11 @@ class MessagesHistoryAdapter constructor(
private const val INCOMING = 3
private const val OUTGOING = 4
private const val ATTACHMENT_PHOTOS_IN = 101
private const val ATTACHMENT_PHOTOS_OUT = 1011
private const val ATTACHMENT_PHOTOS_OUT = 102
private const val ATTACHMENT_STICKER_IN = 111
private const val ATTACHMENT_STICKER_OUT = 112
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
override fun areItemsTheSame(
@@ -80,8 +80,15 @@ class MessagesHistoryFragment :
val status = when {
conversation.isChat() -> "${conversation.membersCount} members"
conversation.isUser() -> if (user?.online == true) "Online" else "Last seen at [...]"
conversation.isGroup() -> "[Group status]"
conversation.isUser() -> when {
// TODO: 9/15/2021 user normal time
user?.online == true -> "Online"
user?.lastSeen != null -> "Last seen at ${
SimpleDateFormat("HH:mm", Locale.getDefault()).format(user?.lastSeen!! * 1000L)
}"
else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently"
}
conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group"
else -> null
}
@@ -162,7 +169,6 @@ class MessagesHistoryFragment :
}
action.observe(viewLifecycleOwner) {
binding.action.animate()
.scaleX(1.25f)
.scaleY(1.25f)
@@ -216,18 +222,19 @@ class MessagesHistoryFragment :
peerId = conversation.id,
message = messageText,
randomId = 0
) { message = message.changeId(it) }
) { message = message.copyMessage(id = it) }
}
}
override fun onEvent(event: VKEvent) {
super.onEvent(event)
when (event) {
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
is MessagesLoaded -> refreshMessages(event)
is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped()
}
super.onEvent(event)
}
private fun onProgressStarted() {
@@ -276,7 +283,9 @@ class MessagesHistoryFragment :
val message = adapter.values[i]
if (event.messagesIds.contains(message.id)) {
if (!changed) changed = true
adapter.values[i] = message.copy(important = event.important)
adapter.values[i] = message.copyMessage(
important = event.important
)
}
}
@@ -303,6 +312,7 @@ class MessagesHistoryFragment :
private fun onItemClick(position: Int) {
val message = adapter.values[position]
if (message.action != null) return
val important = if (message.important) "Unmark as important" else "Mark as important"
@@ -1,6 +1,7 @@
package com.meloda.fast.screens.messages
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
@@ -28,10 +29,10 @@ class MessagesHistoryViewModel @Inject constructor(
makeJob({
dataSource.getHistory(
MessagesGetHistoryRequest(
count = 90,
count = 30,
peerId = peerId,
extended = true,
fields = "photo_200,sex"
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
)
)
},
@@ -0,0 +1,100 @@
package com.meloda.fast.screens.messages
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Space
import android.widget.TextView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isNotEmpty
import coil.load
import com.google.android.material.imageview.ShapeableImageView
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkPhoto
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.BoundedFrameLayout
import com.meloda.fast.widget.BoundedLinearLayout
import kotlin.math.roundToInt
object MessagesManager {
fun setRootMaxWidth(
layout: View
) {
val maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
if (layout is BoundedFrameLayout) {
layout.maxWidth = maxWidth
} else if (layout is BoundedLinearLayout) {
layout.maxWidth = maxWidth
}
}
fun loadPhotos(
context: Context,
message: VkMessage,
photosContainer: LinearLayoutCompat
) {
photosContainer.removeAllViews()
message.attachments?.let { attachments ->
val photos = attachments.map { it as VkPhoto }
photos.forEach { photo ->
val size = photo.sizeOfType('m') ?: return
val newPhoto = ShapeableImageView(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
it.shapeAppearanceModel =
it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(4) }
it.scaleType = ImageView.ScaleType.CENTER_CROP
}
val spacer = Space(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(5).roundToInt()
)
}
if (photosContainer.isNotEmpty())
photosContainer.addView(spacer)
photosContainer.addView(newPhoto)
newPhoto.load(size.url)
}
}
}
fun loadMessageAvatar(
message: VkMessage,
messageUser: VkUser?,
messageGroup: VkGroup?,
imageView: ImageView
) {
val avatar = when {
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
else -> null
}
imageView.load(avatar) { crossfade(100) }
}
fun setMessageText(
message: VkMessage,
textView: TextView
) {
textView.text = message.text ?: "[no_message]"
}
}
@@ -0,0 +1,50 @@
package com.meloda.fast.service
import android.util.Log
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
import com.meloda.fast.api.network.datasource.MessagesDataSource
import com.meloda.fast.api.network.repo.LongPollRepo
import kotlinx.coroutines.*
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
class LongPollService {
}
class LongPollTask @Inject constructor(
private val dataSource: MessagesDataSource,
private val longPollRepo: LongPollRepo
) : CoroutineScope {
companion object {
const val TAG = "LongPollTask"
}
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "error: $throwable")
throwable.printStackTrace()
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job + exceptionHandler
fun startPolling(): Job {
if (job.isCompleted || job.isCancelled) throw Exception("Job is over")
return launch {
val serverInfo = dataSource.getLongPollServer(
MessagesGetLongPollServerRequest(
needPts = true,
version = 10
)
)
println("TESTJOPAAAAAA: $serverInfo")
// val response = serverInfo.response ?: return@launch
}
}
}
@@ -8,7 +8,6 @@ import android.util.AttributeSet
import android.view.ViewTreeObserver
import androidx.appcompat.widget.AppCompatImageView
// TODO: 8/31/2021 extend ShapeableImageView and set corners for half of size
class CircleImageView : AppCompatImageView {
companion object {
@@ -27,7 +26,6 @@ class CircleImageView : AppCompatImageView {
attrs,
defStyleAttr
) {
init()
}