Lot of global changes (#10)

Global update
This commit is contained in:
2022-08-30 19:49:52 +03:00
committed by GitHub
parent 8d0cd19322
commit 7a99347841
230 changed files with 9172 additions and 3157 deletions
@@ -1,54 +0,0 @@
package com.meloda.fast.activity
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.Router
import com.github.terrakok.cicerone.androidx.AppNavigator
import com.github.terrakok.cicerone.androidx.FragmentScreen
import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.Screens
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : BaseActivity(R.layout.activity_main) {
private val navigator = object : AppNavigator(this, R.id.root_fragment_container) {
override fun setupFragmentTransaction(
screen: FragmentScreen,
fragmentTransaction: FragmentTransaction,
currentFragment: Fragment?,
nextFragment: Fragment
) {
// fragmentTransaction.setCustomAnimations(
// R.anim.activity_open_enter, R.anim.activity_close_exit,
// R.anim.activity_close_enter, R.anim.activity_open_exit
// )
}
}
@Inject
lateinit var navigatorHolder: NavigatorHolder
@Inject
lateinit var router: Router
override fun onResumeFragments() {
navigatorHolder.setNavigator(navigator)
super.onResumeFragments()
}
override fun onPause() {
navigatorHolder.removeNavigator()
super.onPause()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
router.newRootScreen(Screens.Main())
}
}
@@ -1,24 +1,24 @@
package com.meloda.fast.api
enum class ApiEvent(val value: Int) {
MESSAGE_SET_FLAGS(2),
MESSAGE_CLEAR_FLAGS(3),
MESSAGE_NEW(4),
MESSAGE_EDIT(5),
MESSAGE_READ_INCOMING(6),
MESSAGE_READ_OUTGOING(7),
FRIEND_ONLINE(8),
FRIEND_OFFLINE(9),
MESSAGES_DELETED(13),
PIN_UNPIN_CONVERSATION(20),
PRIVATE_TYPING(61),
CHAT_TYPING(62),
ONE_MORE_TYPING(63),
VOICE_RECORDING(64),
PHOTO_UPLOADING(65),
VIDEO_UPLOADING(66),
FILE_UPLOADING(67),
UNREAD_COUNT_UPDATE(80)
MessageSetFlags(2),
MessageClearFlags(3),
MessageNew(4),
MessageEdit(5),
MessageReadIncoming(6),
MessageReadOutgoing(7),
FriendOnline(8),
FriendOffline(9),
MessagesDeleted(13),
PinUnpinConversation(20),
PrivateTyping(61),
ChatTyping(62),
OneMoreTyping(63),
VoiceRecording(64),
PhotoUploading(65),
VideoUploading(66),
FileUploading(67),
UnreadCountUpdate(80)
;
companion object {
@@ -1,45 +1,46 @@
package com.meloda.fast.api
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.model.AppAccount
object UserConfig {
private const val FAST_TOKEN = "fast_token"
private const val TOKEN = "token"
private const val USER_ID = "user_id"
private const val ARG_CURRENT_USER_ID = "current_user_id"
const val FAST_APP_ID = "6964679"
private val preferences get() = AppGlobal.preferences
var currentUserId: Int = -1
get() = preferences.getInt(ARG_CURRENT_USER_ID, -1)
set(value) {
field = value
preferences.edit { putInt(ARG_CURRENT_USER_ID, value) }
}
var userId: Int = -1
get() = AppGlobal.preferences.getInt(USER_ID, -1)
set(value) {
field = value
AppGlobal.preferences.edit().putInt(USER_ID, value).apply()
}
var accessToken: String = ""
get() = AppGlobal.preferences.getString(TOKEN, "") ?: ""
set(value) {
field = value
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
}
var fastToken: String? = ""
var fastToken: String = ""
get() = AppGlobal.preferences.getString(FAST_TOKEN, "") ?: ""
set(value) {
field = value
AppGlobal.preferences.edit().putString(FAST_TOKEN, value).apply()
}
fun parse(account: AppAccount) {
this.userId = account.userId
this.accessToken = account.accessToken
this.fastToken = account.fastToken
}
fun clear() {
currentUserId = -1
accessToken = ""
fastToken = ""
userId = -1
}
fun isLoggedIn() = userId > 0 && accessToken.isNotBlank()
fun isLoggedIn(): Boolean {
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
}
val vkUser = MutableLiveData<VkUser?>(null)
@@ -12,7 +12,7 @@ object VKConstants {
const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
const val API_VERSION = "5.132"
const val API_VERSION = "5.189"
const val LP_VERSION = 10
const val VK_APP_ID = "2274003"
@@ -53,22 +53,4 @@ object VKConstants {
VkVoiceMessage::class.java,
VkWidget::class.java
)
val separatedFromTextAttachments = listOf<Class<out VkAttachment>>(
VkPhoto::class.java,
VkVideo::class.java,
VkSticker::class.java,
VkStory::class.java,
VkWidget::class.java,
VkGroupCall::class.java,
VkGroupCall::class.java,
VkCurator::class.java,
VkEvent::class.java,
VkGift::class.java,
VkGraffiti::class.java,
VkPoll::class.java,
VkWall::class.java,
VkWallReply::class.java,
VkLink::class.java
)
}
@@ -1,20 +0,0 @@
package com.meloda.fast.api
import org.json.JSONObject
import java.io.IOException
open class VKException(
var url: String = "",
var code: Int = -1,
var description: String = "",
var error: String
) : IOException(description) {
// TODO: 10-Oct-21 remove this
var json: JSONObject? = null
override fun toString(): String {
return "error: $error; description: $description;"
}
}
@@ -6,7 +6,9 @@ import android.graphics.drawable.Drawable
import android.text.SpannableString
import android.text.style.StyleSpan
import androidx.core.content.ContextCompat
import com.google.gson.Gson
import com.meloda.fast.R
import com.meloda.fast.api.base.ApiError
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
@@ -14,7 +16,10 @@ 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.*
import com.meloda.fast.extensions.orDots
@Suppress("MemberVisibilityCanBePrivate")
object VkUtils {
fun <T> attachmentToString(
@@ -44,12 +49,12 @@ object VkUtils {
fun getMessageUser(message: VkMessage, profiles: Map<Int, VkUser>): VkUser? {
return (if (!message.isUser()) null
else profiles[message.fromId]).also { message.user.value = it }
else profiles[message.fromId]).also { message.user = it }
}
fun getMessageGroup(message: VkMessage, groups: Map<Int, VkGroup>): VkGroup? {
return (if (!message.isGroup()) null
else groups[message.fromId]).also { message.group.value = it }
else groups[message.fromId]).also { message.group = it }
}
fun getMessageAvatar(
@@ -66,9 +71,19 @@ object VkUtils {
fun getMessageTitle(
message: VkMessage,
messageUser: VkUser?,
messageGroup: VkGroup?
defMessageUser: VkUser? = null,
defMessageGroup: VkGroup? = null,
profiles: Map<Int, VkUser>? = null,
groups: Map<Int, VkGroup>? = null
): String? {
val messageUser: VkUser? =
defMessageUser ?: if (profiles == null) null
else profiles[message.fromId]
val messageGroup: VkGroup? =
defMessageGroup ?: if (groups == null) null
else groups[message.fromId]
return when {
message.isUser() -> messageUser?.fullName
message.isGroup() -> messageGroup?.name
@@ -78,12 +93,12 @@ object VkUtils {
fun getConversationUser(conversation: VkConversation, profiles: Map<Int, VkUser>): VkUser? {
return (if (!conversation.isUser()) null
else profiles[conversation.id]).also { conversation.user.value = it }
else profiles[conversation.id]).also { conversation.user.postValue(it) }
}
fun getConversationGroup(conversation: VkConversation, groups: Map<Int, VkGroup>): VkGroup? {
return (if (!conversation.isGroup()) null
else groups[conversation.id]).also { conversation.group.value = it }
else groups[conversation.id]).also { conversation.group.postValue(it) }
}
fun getConversationAvatar(
@@ -92,7 +107,7 @@ object VkUtils {
conversationGroup: VkGroup?
): String? {
return when {
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
conversation.isAccount() -> null
conversation.isUser() -> conversationUser?.photo200
conversation.isGroup() -> conversationGroup?.photo200
conversation.isChat() -> conversation.photo200
@@ -100,6 +115,53 @@ object VkUtils {
}
}
fun getConversationTitle(
context: Context,
conversation: VkConversation,
defConversationUser: VkUser? = null,
defConversationGroup: VkGroup? = null,
profiles: Map<Int, VkUser>? = null,
groups: Map<Int, VkGroup>? = null
): String? {
val conversationUser: VkUser? =
defConversationUser ?: if (profiles == null) null
else getConversationUser(conversation, profiles)
val conversationGroup: VkGroup? =
defConversationGroup ?: if (groups == null) null
else getConversationGroup(conversation, groups)
return when {
conversation.isAccount() -> context.getString(R.string.favorites)
conversation.isChat() -> conversation.title
conversation.isUser() -> conversationUser?.fullName
conversation.isGroup() -> conversationGroup?.name
else -> null
}
}
fun getConversationUserGroup(
conversation: VkConversation,
profiles: Map<Int, VkUser>,
groups: Map<Int, VkGroup>
): Pair<VkUser?, VkGroup?> {
val user: VkUser? = getConversationUser(conversation, profiles)
val group: VkGroup? = getConversationGroup(conversation, groups)
return user to group
}
fun getMessageUserGroup(
message: VkMessage,
profiles: Map<Int, VkUser>,
groups: Map<Int, VkGroup>
): Pair<VkUser?, VkGroup?> {
val user: VkUser? = getMessageUser(message, profiles)
val group: VkGroup? = getMessageGroup(message, groups)
return user to group
}
fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
return text.apply {
if (forConversations == true) replace("\n", "")
@@ -231,6 +293,7 @@ object VkUtils {
messageUser: VkUser? = null,
messageGroup: VkGroup? = null
): SpannableString? {
@Suppress("REDUNDANT_ELSE_IN_WHEN")
return when (message.getPreparedAction()) {
VkMessage.Action.CHAT_CREATE -> {
val text = message.actionText ?: return null
@@ -245,12 +308,14 @@ object VkUtils {
val spanText =
context.getString(R.string.message_action_chat_created, prefix, text)
val startIndex = spanText.indexOf(text, startIndex = prefix.length)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD),
spanText.indexOf(text, startIndex = prefix.length),
text.length, 0
startIndex,
startIndex + text.length, 0
)
}
}
@@ -329,7 +394,7 @@ object VkUtils {
} else {
val prefix =
if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..."
else messageUser?.toString() ?: messageGroup?.toString().orDots()
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
@@ -374,7 +439,7 @@ object VkUtils {
}
} else {
val prefix = if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..."
else messageUser?.toString() ?: messageGroup?.toString().orDots()
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
@@ -410,6 +475,20 @@ object VkUtils {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText =
context.getString(R.string.message_action_chat_user_joined_by_call, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
@@ -520,8 +599,8 @@ object VkUtils {
}
fun getAttachmentText(context: Context, message: VkMessage): String? {
message.geoType?.let {
return when (it) {
message.geo?.let {
return when (it.type) {
"point" -> context.getString(R.string.message_geo_point)
else -> context.getString(R.string.message_geo)
}
@@ -551,14 +630,14 @@ object VkUtils {
}
fun getAttachmentConversationIcon(context: Context, message: VkMessage): Drawable? {
message.geoType?.let {
return ContextCompat.getDrawable(context, R.drawable.ic_map_marker)
}
if (message.attachments.isNullOrEmpty()) return null
return message.attachments?.let { attachments ->
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
message.geo?.let {
return ContextCompat.getDrawable(context, R.drawable.ic_map_marker)
}
if (attachments.isEmpty()) return null
getAttachmentTypeByClass(attachments[0])?.let {
getAttachmentIconByType(
context,
@@ -683,4 +762,37 @@ object VkUtils {
else -> attachmentType.value
}
}
fun getApiError(gson: Gson, errorString: String?): ApiAnswer.Error {
try {
val defaultError = gson.fromJson(errorString, ApiError::class.java)
val error: ApiError =
when (defaultError.error) {
VkErrorCodes.UserAuthorizationFailed.toString() -> {
val authorizationError =
gson.fromJson(errorString, AuthorizationError::class.java)
authorizationError
}
VkErrors.NeedValidation -> {
val validationError =
gson.fromJson(errorString, ValidationRequiredError::class.java)
validationError
}
VkErrors.NeedCaptcha -> {
val captchaRequiredError =
gson.fromJson(errorString, CaptchaRequiredError::class.java)
captchaRequiredError
}
else -> defaultError
}
return ApiAnswer.Error(error)
} catch (e: Exception) {
return ApiAnswer.Error(ApiError(throwable = e))
}
}
}
@@ -1,11 +1,18 @@
package com.meloda.fast.api.base
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.VKException
import okio.IOException
data class ApiError(
@SerializedName("error_code")
val errorCode: Int,
@SerializedName("error_msg")
override var message: String
) : VKException(error = message, code = errorCode)
open class ApiError(
@SerializedName("error", alternate = ["error_code"])
val error: String? = null,
@SerializedName("error_msg", alternate = ["error_description"])
open val errorMessage: String? = null,
val throwable: Throwable? = null
) : IOException() {
override fun toString(): String {
return Gson().toJson(this)
}
}
@@ -1,4 +1,4 @@
package com.meloda.fast.api
package com.meloda.fast.api.longpoll
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
@@ -1,31 +1,27 @@
package com.meloda.fast.api
package com.meloda.fast.api.longpoll
import android.util.Log
import com.google.gson.JsonArray
import com.meloda.fast.api.ApiEvent
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.messages.MessagesDataSource
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
import com.meloda.fast.base.viewmodel.VkEventCallback
import com.meloda.fast.data.messages.MessagesRepository
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Suppress("UNCHECKED_CAST")
class LongPollUpdatesParser(
private val messagesDataSource: MessagesDataSource
) : CoroutineScope {
companion object {
private const val TAG = "LongPollUpdatesParser"
}
@Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate")
class LongPollUpdatesParser(private val messagesRepository: MessagesRepository) : CoroutineScope {
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "error: $throwable")
Log.d("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace()
}
@@ -36,51 +32,51 @@ class LongPollUpdatesParser(
mutableMapOf()
fun parseNextUpdate(event: JsonArray) {
val eventType: ApiEvent? =
try {
ApiEvent.parse(event[0].asInt)
} catch (e: Exception) {
null
}
val eventId = event[0].asInt
val eventType: ApiEvent? = ApiEvent.parse(eventId)
if (eventType != null) {
println("$TAG: $eventType: $event")
} else {
println("$TAG: unknown event: $event")
if (eventType == null) {
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
return
}
when (eventType) {
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
ApiEvent.FRIEND_ONLINE -> parseFriendOnline(eventType, event)
ApiEvent.FRIEND_OFFLINE -> parseFriendOffline(eventType, event)
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
// ApiEvent.PIN_UNPIN_CONVERSATION -> TODO()
// ApiEvent.TYPING -> TODO()
// ApiEvent.VOICE_RECORDING -> TODO()
// ApiEvent.PHOTO_UPLOADING -> TODO()
// ApiEvent.VIDEO_UPLOADING -> TODO()
// ApiEvent.FILE_UPLOADING -> TODO()
// ApiEvent.UNREAD_COUNT_UPDATE -> TODO()
ApiEvent.MessageSetFlags -> parseMessageSetFlags(eventType, event)
ApiEvent.MessageClearFlags -> parseMessageClearFlags(eventType, event)
ApiEvent.MessageNew -> parseMessageNew(eventType, event)
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
ApiEvent.FriendOnline -> parseFriendOnline(eventType, event)
ApiEvent.FriendOffline -> parseFriendOffline(eventType, event)
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
ApiEvent.PinUnpinConversation -> onNewEvent(eventType, event)
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
ApiEvent.VoiceRecording -> onNewEvent(eventType, event)
ApiEvent.PhotoUploading -> onNewEvent(eventType, event)
ApiEvent.VideoUploading -> onNewEvent(eventType, event)
ApiEvent.FileUploading -> onNewEvent(eventType, event)
ApiEvent.UnreadCountUpdate -> onNewEvent(eventType, event)
}
}
private fun onNewEvent(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
}
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt
launch {
@@ -90,7 +86,7 @@ class LongPollUpdatesParser(
messageId
)
listenersMap[ApiEvent.MESSAGE_NEW]?.let {
listenersMap[ApiEvent.MessageNew]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
.onEvent(newMessageEvent)
@@ -100,8 +96,7 @@ class LongPollUpdatesParser(
}
private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt
launch {
@@ -111,7 +106,7 @@ class LongPollUpdatesParser(
messageId
)
listenersMap[ApiEvent.MESSAGE_EDIT]?.let {
listenersMap[ApiEvent.MessageEdit]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
.onEvent(editedMessageEvent)
@@ -121,11 +116,12 @@ class LongPollUpdatesParser(
}
private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt
val messageId = event[2].asInt
launch {
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners ->
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
.onEvent(
@@ -140,11 +136,12 @@ class LongPollUpdatesParser(
}
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt
val messageId = event[2].asInt
launch {
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners ->
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
.onEvent(
@@ -159,22 +156,22 @@ class LongPollUpdatesParser(
}
private fun parseFriendOnline(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseFriendOffline(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event")
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
coroutineScope {
suspendCoroutine<T> {
launch {
val normalMessageResponse = messagesDataSource.getById(
val normalMessageResponse = messagesRepository.getById(
MessagesGetByIdRequest(
messagesIds = listOf(messageId),
extended = true,
@@ -182,17 +179,19 @@ class LongPollUpdatesParser(
)
)
if (normalMessageResponse !is Answer.Success) {
(normalMessageResponse as Answer.Error).throwable.let { throw it }
if (!normalMessageResponse.isSuccessful()) {
normalMessageResponse.error.throwable?.run { throw this }
}
val messagesResponse = normalMessageResponse.data.response ?: return@launch
val messagesResponse =
(normalMessageResponse as? ApiAnswer.Success)?.data?.response
?: return@launch
val messagesList = messagesResponse.items
if (messagesList.isEmpty()) return@launch
val normalMessage = messagesList[0].asVkMessage()
messagesDataSource.store(listOf(normalMessage))
messagesRepository.store(listOf(normalMessage))
val profiles = hashMapOf<Int, VkUser>()
messagesResponse.profiles?.forEach { baseUser ->
@@ -205,13 +204,13 @@ class LongPollUpdatesParser(
}
val resumeValue: LongPollEvent? = when (eventType) {
ApiEvent.MESSAGE_NEW ->
ApiEvent.MessageNew ->
LongPollEvent.VkMessageNewEvent(
normalMessage,
profiles,
groups
)
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(normalMessage)
ApiEvent.MessageEdit -> LongPollEvent.VkMessageEditEvent(normalMessage)
else -> null
}
@@ -221,7 +220,7 @@ class LongPollUpdatesParser(
}
fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
private fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
listenersMap.let { map ->
map[eventType] = (map[eventType] ?: mutableListOf()).also {
it.add(listener)
@@ -230,7 +229,7 @@ class LongPollUpdatesParser(
}
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener)
registerListener(ApiEvent.MessageReadIncoming, listener)
}
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
@@ -238,7 +237,7 @@ class LongPollUpdatesParser(
}
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener)
registerListener(ApiEvent.MessageReadOutgoing, listener)
}
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
@@ -246,7 +245,7 @@ class LongPollUpdatesParser(
}
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
registerListener(ApiEvent.MESSAGE_NEW, listener)
registerListener(ApiEvent.MessageNew, listener)
}
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
@@ -254,7 +253,7 @@ class LongPollUpdatesParser(
}
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
registerListener(ApiEvent.MESSAGE_EDIT, listener)
registerListener(ApiEvent.MessageEdit, listener)
}
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
@@ -266,9 +265,7 @@ class LongPollUpdatesParser(
}
}
internal inline fun <R : Any> assembleEventCallback(
crossinline block: (R) -> Unit
): VkEventCallback<R> {
internal inline fun <R : Any> assembleEventCallback(crossinline block: (R) -> Unit): VkEventCallback<R> {
return object : VkEventCallback<R> {
override fun onEvent(event: R) = block.invoke(event)
}
@@ -5,6 +5,7 @@ import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig
import com.meloda.fast.model.SelectableItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@@ -25,10 +26,11 @@ data class VkConversation(
var outRead: Int,
var isMarkedUnread: Boolean,
var lastMessageId: Int,
var unreadCount: Int?,
var unreadCount: Int,
var membersCount: Int?,
var isPinned: Boolean,
var canChangePin: Boolean,
var majorId: Int,
var minorId: Int,
@Embedded(prefix = "pinnedMessage_")
var pinnedMessage: VkMessage? = null,
@@ -49,9 +51,13 @@ data class VkConversation(
fun isUser() = type == "user"
fun isGroup() = type == "group"
fun isInUnread() = inRead < lastMessageId
fun isOutUnread() = outRead < lastMessageId
fun isInUnread() = inRead - lastMessageId < 0
fun isOutUnread() = outRead - lastMessageId < 0
fun isUnread() = isInUnread() || isOutUnread()
fun isAccount() = id == UserConfig.userId
fun isPinned() = majorId > 0
}
@@ -1,12 +1,12 @@
package com.meloda.fast.api.model
import androidx.lifecycle.MutableLiveData
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.model.SelectableItem
import com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel
@@ -14,7 +14,7 @@ import kotlinx.parcelize.Parcelize
@Entity(tableName = "messages")
@Parcelize
data class VkMessage(
data class VkMessage constructor(
@PrimaryKey(autoGenerate = false)
var id: Int,
var text: String? = null,
@@ -28,21 +28,29 @@ data class VkMessage(
val actionText: String? = null,
val actionConversationMessageId: Int? = null,
val actionMessage: String? = null,
val geoType: String? = null,
var updateTime: Int? = null,
var important: Boolean = false,
var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null,
var replyMessage: VkMessage? = null
var replyMessage: VkMessage? = null,
val geo: BaseVkMessage.Geo? = null,
) : SelectableItem(id) {
@Ignore
@IgnoredOnParcel
val user = MutableLiveData<VkUser?>()
var user: VkUser? = null
@Ignore
@IgnoredOnParcel
val group = MutableLiveData<VkGroup?>()
var group: VkGroup? = null
@Ignore
@IgnoredOnParcel
var state: State = State.Sent
fun isPeerChat() = peerId > 2_000_000_000
@@ -51,8 +59,11 @@ data class VkMessage(
fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation) =
if (isOut) conversation.outRead - id >= 0
else conversation.inRead - id >= 0
if (isOut) {
conversation.outRead - id >= 0
} else {
conversation.inRead - id >= 0
}
fun getPreparedAction(): Action? {
if (action == null) return null
@@ -61,10 +72,27 @@ data class VkMessage(
fun canEdit() =
fromId == UserConfig.userId &&
(attachments == null || !VKConstants.restrictedToEditAttachments.contains(
attachments!![0].javaClass
)) &&
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS)
(attachments == null ||
!VKConstants.restrictedToEditAttachments.contains(
requireNotNull(attachments).first().javaClass
)) &&
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.OneDayInSeconds)
fun hasAttachments(): Boolean = !attachments.isNullOrEmpty()
fun hasReply(): Boolean = replyMessage != null
fun hasForwards(): Boolean = !forwards.isNullOrEmpty()
fun hasGeo(): Boolean = geo != null
fun isUpdated(): Boolean = updateTime != null && requireNotNull(updateTime) > 0
fun isSending(): Boolean = state == State.Sending
fun isError(): Boolean = state == State.Error
fun isSent(): Boolean = state == State.Sent
enum class Action(val value: String) {
CHAT_CREATE("chat_create"),
@@ -78,14 +106,17 @@ data class VkMessage(
CHAT_KICK_USER("chat_kick_user"),
CHAT_SCREENSHOT("chat_screenshot"),
// TODO: 9/11/2021 catch this shit
CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"),
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"),
CHAT_STYLE_UPDATE("conversation_style_update");
companion object {
fun parse(value: String) = values().first { it.value == value }
fun parse(value: String?): Action? = values().firstOrNull { it.value == value }
}
}
enum class State {
Sending, Sent, Error
}
}
@@ -1,10 +1,15 @@
package com.meloda.fast.api.model.attachments
import android.os.Parcelable
import com.meloda.fast.model.DataItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
open class VkAttachment : Parcelable {
open class VkAttachment : DataItem<Int>(), Parcelable {
@IgnoredOnParcel
override val dataItemId: Int = -1
open fun asString(withAccessKey: Boolean = true) = ""
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.base.attachments.BaseVkFile
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@@ -12,7 +13,8 @@ data class VkFile(
val ext: String,
val size: Int,
val url: String,
val accessKey: String?
val accessKey: String?,
val preview: BaseVkFile.Preview?
) : VkAttachment() {
@IgnoredOnParcel
@@ -12,7 +12,8 @@ data class VkVideo(
val ownerId: Int,
val images: List<VideoImage>,
val firstFrames: List<BaseVkVideo.FirstFrame>?,
val accessKey: String?
val accessKey: String?,
val title: String
) : VkAttachment() {
@IgnoredOnParcel
@@ -12,12 +12,12 @@ data class VkWall(
val date: Int,
val text: String,
val attachments: List<BaseVkAttachmentItem>?,
val comments: Int,
val likes: Int,
val reposts: Int,
val views: Int,
val comments: Int?,
val likes: Int?,
val reposts: Int?,
val views: Int?,
val isFavorite: Boolean,
val accessKey: String
val accessKey: String?
) : VkAttachment() {
@IgnoredOnParcel
@@ -37,10 +37,11 @@ data class BaseVkConversation(
outRead = out_read,
isMarkedUnread = is_marked_unread,
lastMessageId = last_message_id,
unreadCount = unread_count,
unreadCount = unread_count ?: 0,
membersCount = chat_settings?.members_count,
ownerId = chat_settings?.owner_id,
isPinned = sort_id.major_id > 0,
majorId = sort_id.major_id,
minorId = sort_id.minor_id,
canChangePin = chat_settings?.acl?.can_change_pin == true
).apply {
this.lastMessage = lastMessage
@@ -24,7 +24,8 @@ data class BaseVkMessage(
val geo: Geo?,
val action: Action?,
val ttl: Int,
val reply_message: BaseVkMessage?
val reply_message: BaseVkMessage?,
val update_time: Int?
) : Parcelable {
fun asVkMessage() = VkMessage(
@@ -40,8 +41,9 @@ data class BaseVkMessage(
actionText = action?.text,
actionConversationMessageId = action?.conversation_message_id,
actionMessage = action?.message,
geoType = geo?.type,
important = important
geo = geo,
important = important,
updateTime = update_time
).also {
it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwd_messages)
@@ -55,7 +57,6 @@ data class BaseVkMessage(
val place: Place
) : Parcelable {
@Parcelize
data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable
@@ -27,7 +27,8 @@ data class BaseVkFile(
ext = ext,
url = url,
size = size,
accessKey = access_key
accessKey = access_key,
preview = preview
)
@Parcelize
@@ -43,7 +43,8 @@ data class BaseVkVideo(
ownerId = owner_id,
images = image.map { it.asVideoImage() },
firstFrames = first_frame,
accessKey = access_key
accessKey = access_key,
title = title
)
@Parcelize
@@ -12,14 +12,14 @@ data class BaseVkWall(
val date: Int,
val text: String,
val attachments: List<BaseVkAttachmentItem>?,
val post_source: PostSource,
val comments: Comments,
val likes: Likes,
val reposts: Reposts,
val views: Views,
val post_source: PostSource?,
val comments: Comments?,
val likes: Likes?,
val reposts: Reposts?,
val views: Views?,
val is_favorite: Boolean,
val donut: Donut,
val access_key: String,
val donut: Donut?,
val access_key: String?,
val short_text_rate: Double
) : Parcelable {
@@ -30,10 +30,10 @@ data class BaseVkWall(
date = date,
text = text,
attachments = attachments,
comments = comments.count,
likes = likes.count,
reposts = reposts.count,
views = views.count,
comments = comments?.count,
likes = likes?.count,
reposts = reposts?.count,
views = views?.count,
isFavorite = is_favorite,
accessKey = access_key
)
@@ -0,0 +1,78 @@
package com.meloda.fast.api.network
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.base.ApiError
@Suppress("unused")
object VkErrorCodes {
const val UnknownError = 1
const val AppDisabled = 2
const val UnknownMethod = 3
const val InvalidSignature = 4
const val UserAuthorizationFailed = 5
const val TooManyRequests = 6
const val NoRights = 7
const val BadRequest = 8
const val TooManySimilarActions = 9
const val InternalServerError = 10
const val InTestMode = 11
const val ExecuteCodeCompileError = 12
const val ExecuteCodeRuntimeError = 13
const val CaptchaNeeded = 14
const val AccessDenied = 15
const val RequiresRequestsOverHttps = 16
const val ValidationRequired = 17
const val UserBannedOrDeleted = 18
const val ActionProhibited = 20
const val ActionAllowedOnlyForStandalone = 21
const val MethodOff = 23
const val ConfirmationRequired = 24
const val ParameterIsNotSpecified = 100
const val IncorrectAppId = 101
const val OutOfLimits = 103
const val IncorrectUserId = 113
const val IncorrectTimestamp = 150
const val AccessToAlbumDenied = 200
const val AccessToAudioDenied = 201
const val AccessToGroupDenied = 203
const val AlbumIsFull = 300
const val ActionDenied = 500
const val PermissionDenied = 600
const val CannotSendMessageBlackList = 900
const val CannotSendMessageGroup = 901
const val InvalidDocId = 1150
const val InvalidDocTitle = 1152
const val AccessToDocDenied = 1153
}
@Suppress("unused")
object VkErrors {
const val Unknown = "unknown_error"
const val NeedValidation = "need_validation"
const val NeedCaptcha = "need_captcha"
const val InvalidRequest = "invalid_request"
}
class AuthorizationError : ApiError()
data class ValidationRequiredError(
@SerializedName("validation_type")
val validationType: String,
@SerializedName("validation_sid")
val validationSid: String,
@SerializedName("phone_mask")
val phoneMask: String,
@SerializedName("redirect_uri")
val redirectUri: String,
@SerializedName("validation_resend")
val validationResend: String
) : ApiError()
data class CaptchaRequiredError(
@SerializedName("captcha_sid")
val captchaSid: String,
@SerializedName("captcha_img")
val captchaImg: String
) : ApiError()
@@ -11,16 +11,20 @@ class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().url.newBuilder()
.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
val url = builder.build().toUrl().toString()
if (!builder.build().toUrl().toString().contains(AccountUrls.SetOnline))
if (!url.contains("upload.php")) {
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
}
if (!url.contains(AccountUrls.SetOnline) && !url.contains("upload.php")) {
UserConfig.accessToken.let {
if (it.isNotBlank())
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
}
}
// TODO: 9/29/2021 crash on timeout
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
}
@@ -1,51 +0,0 @@
package com.meloda.fast.api.network
object VkErrorCodes {
const val UNKNOWN_ERROR = 1
const val APP_DISABLED = 2
const val UNKNOWN_METHOD = 3
const val INVALID_SIGNATURE = 4
const val USER_AUTHORIZATION_FAILED = 5
const val TOO_MANY_REQUESTS = 6
const val NO_RIGHTS = 7
const val BAD_REQUEST = 8
const val TOO_MANY_SIMILAR_ACTIONS = 9
const val INTERNAL_SERVER_ERROR = 10
const val IN_TEST_MODE = 11
const val EXECUTE_CODE_COMPILE_ERROR = 12
const val EXECUTE_CODE_RUNTIME_ERROR = 13
const val CAPTCHA_NEEDED = 14
const val ACCESS_DENIED = 15
const val REQUIRES_REQUESTS_OVER_HTTPS = 16
const val VALIDATION_REQUIRED = 17
const val USER_BANNED_OR_DELETED = 18
const val ACTION_PROHIBITED = 20
const val ACTION_ALLOWED_ONLY_FOR_STANDALONE = 21
const val METHOD_OFF = 23
const val CONFIRMATION_REQUIRED = 24
const val PARAMETER_IS_NOT_SPECIFIED = 100
const val INCORRECT_APP_ID = 101
const val OUT_OF_LIMITS = 103
const val INCORRECT_USER_ID = 113
const val INCORRECT_TIMESTAMP = 150
const val ACCESS_TO_ALBUM_DENIED = 200
const val ACCESS_TO_AUDIO_DENIED = 201
const val ACCESS_TO_GROUP_DENIED = 203
const val ALBUM_IS_FULL = 300
const val ACTION_DENIED = 500
const val PERMISSION_DENIED = 600
const val CANNOT_SEND_MESSAGE_BLACK_LIST = 900
const val CANNOT_SEND_MESSAGE_GROUP = 901
const val INVALID_DOC_ID = 1150
const val INVALID_DOC_TITLE = 1152
const val ACCESS_TO_DOC_DENIED = 1153
}
object VkErrors {
const val UNKNOWN = "unknown_error"
const val NEED_VALIDATION = "need_validation"
const val NEED_CAPTCHA = "need_captcha"
const val INVALID_REQUEST = "invalid_request"
}
@@ -1,15 +1,18 @@
@file:Suppress("UNCHECKED_CAST")
package com.meloda.fast.api.network
import com.meloda.fast.api.VKException
import com.google.gson.Gson
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.base.ApiError
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
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
class ResultCallFactory : CallAdapter.Factory() {
override fun get(
@@ -21,7 +24,7 @@ class ResultCallFactory : CallAdapter.Factory() {
if (rawReturnType == Call::class.java) {
if (returnType is ParameterizedType) {
val callInnerType: Type = getParameterUpperBound(0, returnType)
if (getRawType(callInnerType) == Answer::class.java) {
if (getRawType(callInnerType) == ApiAnswer::class.java) {
if (callInnerType is ParameterizedType) {
val resultInnerType = getParameterUpperBound(0, callInnerType)
return ResultCallAdapter<Any?>(resultInnerType)
@@ -55,16 +58,16 @@ internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : C
abstract fun cloneImpl(): Call<Out>
}
private class ResultCallAdapter<R>(private val type: Type) : CallAdapter<R, Call<Answer<R>>> {
private class ResultCallAdapter<R>(private val type: Type) : CallAdapter<R, Call<ApiAnswer<R>>> {
override fun responseType() = type
override fun adapt(call: Call<R>): Call<Answer<R>> = ResultCall(call)
override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call)
}
internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy) {
internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, ApiAnswer<T>>(proxy) {
override fun enqueueImpl(callback: Callback<Answer<T>>) {
override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
proxy.enqueue(ResultCallback(this, callback))
}
@@ -74,25 +77,34 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
private class ResultCallback<T>(
private val proxy: ResultCall<T>,
private val callback: Callback<Answer<T>>
private val callback: Callback<ApiAnswer<T>>
) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
var isVkException = true
val gson = Gson()
val result: Answer<T> =
override fun onResponse(call: Call<T>, response: Response<T>) {
val result: ApiAnswer<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)
if (baseBody !is ApiResponse<*>) {
ApiAnswer.Success(baseBody as T)
} else {
val body = baseBody as? ApiResponse<*>
if (body?.error != null) {
VkUtils.getApiError(gson, gson.toJson(body.error))
} else {
ApiAnswer.Success(body as T)
}
}
} else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
} else {
val errorBodyString = response.errorBody()?.string()
if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return
VkUtils.getApiError(gson, errorBodyString)
}
if (checkErrors(call, result)) {
return
}
callback.onResponse(proxy, Response.success(result))
}
@@ -100,30 +112,21 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
override fun onFailure(call: Call<T>, error: Throwable) {
callback.onResponse(
proxy,
Response.success(Answer.Error(throwable = error))
Response.success(ApiAnswer.Error(ApiError(throwable = error)))
)
}
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean {
if (result.throwable is ApiError) {
onFailure(call, result.throwable)
return true
private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
if (!result.isSuccessful()) {
result.error.throwable?.run {
onFailure(call, this)
return true
}
} else {
return false
}
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
return false
}
}
@@ -132,9 +135,16 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
}
}
sealed class Answer<out R> {
sealed class ApiAnswer<out R> {
data class Success<out T>(val data: T) : Answer<T>()
data class Error(val throwable: Throwable) : Answer<Nothing>()
data class Success<out T>(val data: T) : ApiAnswer<T>()
data class Error(val error: ApiError) : ApiAnswer<Nothing>()
@OptIn(ExperimentalContracts::class)
fun isSuccessful(): Boolean {
contract {
returns(false) implies (this@ApiAnswer is Error)
}
return this is Success
}
}
@@ -1,15 +0,0 @@
package com.meloda.fast.api.network.account
import javax.inject.Inject
class AccountDataSource @Inject constructor(
private val repo: AccountRepo
) {
suspend fun setOnline(params: AccountSetOnlineRequest) = repo.setOnline(params.map)
suspend fun setOffline(params: AccountSetOfflineRequest) = repo.setOffline(params.map)
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api.network.audio
@@ -0,0 +1,20 @@
package com.meloda.fast.api.network.audio
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class AudiosGetUploadServerResponse(
@SerializedName("upload_url")
val uploadUrl: String
) : Parcelable
@Parcelize
data class AudiosUploadResponse(
val redirect: String,
val server: Int,
val audio: String?,
val hash: String,
val error: String?
) : Parcelable
@@ -0,0 +1,11 @@
package com.meloda.fast.api.network.audio
import com.meloda.fast.api.network.VkUrls
object AudiosUrls {
const val GetUploadServer = "${VkUrls.API}/audio.getUploadServer"
const val Save = "${VkUrls.API}/audio.save"
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.network.auth
import javax.inject.Inject
class AuthDataSource @Inject constructor(
private val repo: AuthRepo
) {
suspend fun auth(params: AuthDirectRequest) = repo.auth(params.map)
suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
}
@@ -1,16 +0,0 @@
package com.meloda.fast.api.network.auth
import com.meloda.fast.api.network.Answer
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthRepo {
@GET(AuthUrls.DirectAuth)
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<AuthDirectResponse>
@GET(AuthUrls.SendSms)
suspend fun sendSms(@Query("sid") validationSid: String): Answer<SendSmsResponse>
}
@@ -1,22 +0,0 @@
package com.meloda.fast.api.network.conversations
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.database.dao.ConversationsDao
import javax.inject.Inject
class ConversationsDataSource @Inject constructor(
private val repo: ConversationsRepo,
private val dao: ConversationsDao
) {
suspend fun get(params: ConversationsGetRequest) = repo.get(params.map)
suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map)
suspend fun pin(params: ConversationsPinRequest) = repo.pin(params.map)
suspend fun unpin(params: ConversationsUnpinRequest) = repo.unpin(params.map)
suspend fun store(conversations: List<VkConversation>) = dao.insert(conversations)
}
@@ -1,31 +0,0 @@
package com.meloda.fast.api.network.conversations
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.Answer
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface ConversationsRepo {
@FormUrlEncoded
@POST(ConversationsUrls.Get)
suspend fun get(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
@FormUrlEncoded
@POST(ConversationsUrls.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.ReorderPinned)
suspend fun reorderPinned(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api.network.files
@@ -0,0 +1,25 @@
package com.meloda.fast.api.network.files
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.base.attachments.BaseVkFile
import com.meloda.fast.api.model.base.attachments.BaseVkVoiceMessage
import kotlinx.parcelize.Parcelize
@Parcelize
data class FilesGetMessagesUploadServerResponse(
@SerializedName("upload_url")
val uploadUrl: String
) : Parcelable
@Parcelize
data class FilesUploadFileResponse(val file: String?, val error: String?) : Parcelable
@Parcelize
data class FilesSaveFileResponse(
val type: String,
@SerializedName("doc")
val file: BaseVkFile?,
@SerializedName("audio_message")
val voiceMessage: BaseVkVoiceMessage?
) : Parcelable
@@ -0,0 +1,11 @@
package com.meloda.fast.api.network.files
import com.meloda.fast.api.network.VkUrls
object FilesUrls {
const val GetMessagesUploadServer = "${VkUrls.API}/docs.getMessagesUploadServer"
const val Save = "${VkUrls.API}/docs.save"
}
@@ -9,7 +9,8 @@ data class LongPollGetUpdatesRequest(
val key: String,
val ts: Int,
val wait: Int,
val mode: Int
val mode: Int,
val version: Int
) : Parcelable {
val map
@@ -18,7 +19,8 @@ data class LongPollGetUpdatesRequest(
"key" to key,
"ts" to ts.toString(),
"wait" to wait.toString(),
"mode" to mode.toString()
"mode" to mode.toString(),
"version" to version.toString()
)
}
@@ -1,50 +0,0 @@
package com.meloda.fast.api.network.messages
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
import com.meloda.fast.api.network.longpoll.LongPollRepo
import com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject
class MessagesDataSource @Inject constructor(
private val messagesRepo: MessagesRepo,
private val messagesDao: MessagesDao,
private val longPollRepo: LongPollRepo
) {
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
suspend fun getHistory(params: MessagesGetHistoryRequest) =
messagesRepo.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) =
messagesRepo.send(params.map)
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
messagesRepo.markAsImportant(params.map)
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
messagesRepo.getLongPollServer(params.map)
suspend fun pin(params: MessagesPinMessageRequest) =
messagesRepo.pin(params.map)
suspend fun unpin(params: MessagesUnPinMessageRequest) =
messagesRepo.unpin(params.map)
suspend fun delete(params: MessagesDeleteRequest) =
messagesRepo.delete(params.map)
suspend fun edit(params: MessagesEditRequest) =
messagesRepo.edit(params.map)
suspend fun getLongPollUpdates(
serverUrl: String,
params: LongPollGetUpdatesRequest
) = longPollRepo.getResponse(serverUrl, params.map)
suspend fun getById(params: MessagesGetByIdRequest) =
messagesRepo.getById(params.map)
}
@@ -41,7 +41,8 @@ data class MessagesSendRequest(
val stickerId: Int? = null,
val disableMentions: Boolean? = null,
val dontParseLinks: Boolean? = null,
val silent: Boolean? = null
val silent: Boolean? = null,
val attachments: List<VkAttachment>? = null
) : Parcelable {
val map
@@ -57,6 +58,11 @@ data class MessagesSendRequest(
disableMentions?.let { this["disable_mentions"] = it.intString }
dontParseLinks?.let { this["dont_parse_links"] = it.intString }
silent?.let { this["silent"] = it.toString() }
attachments?.let {
this["attachment"] = it.joinToString(separator = ",") { attachment ->
attachment.asString(true)
}
}
}
}
@@ -14,5 +14,6 @@ object MessagesUrls {
const val Delete = "${VkUrls.API}/messages.delete"
const val Edit = "${VkUrls.API}/messages.edit"
const val GetById = "${VkUrls.API}/messages.getById"
const val MarkAsRead = "${VkUrls.API}/messages.markAsRead"
}
@@ -0,0 +1,8 @@
package com.meloda.fast.api.network.ota
import android.os.Parcelable
import com.meloda.fast.model.UpdateItem
import kotlinx.parcelize.Parcelize
@Parcelize
data class OtaGetLatestReleaseResponse(val release: UpdateItem?) : Parcelable
@@ -0,0 +1,7 @@
package com.meloda.fast.api.network.ota
object OtaUrls {
const val GetActualUrl =
"https://raw.githubusercontent.com/melod1n/ota-server/master/ngrok_url.json"
}
@@ -0,0 +1,11 @@
package com.meloda.fast.api.network.photos
import com.meloda.fast.api.network.VkUrls
object PhotoUrls {
const val GetMessagesUploadServer = "${VkUrls.API}/photos.getMessagesUploadServer"
const val SaveMessagePhoto = "${VkUrls.API}/photos.saveMessagesPhoto"
}
@@ -0,0 +1,16 @@
package com.meloda.fast.api.network.photos
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class PhotosSaveMessagePhotoRequest(
val photo: String, val server: Int, val hash: String
) : Parcelable {
val map
get() = mapOf(
"photo" to photo,
"server" to server.toString(),
"hash" to hash
)
}
@@ -0,0 +1,18 @@
package com.meloda.fast.api.network.photos
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class PhotosGetMessagesUploadServerResponse(
@SerializedName("album_id")
val albumId: Int,
@SerializedName("upload_url")
val uploadUrl: String
) : Parcelable
@Parcelize
data class PhotosUploadPhotoResponse(
val server: Int, val photo: String, val hash: String
) : Parcelable
@@ -1,16 +0,0 @@
package com.meloda.fast.api.network.users
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.database.dao.UsersDao
import javax.inject.Inject
class UsersDataSource @Inject constructor(
private val repo: UsersRepo,
private val dao: UsersDao
) {
suspend fun getById(params: UsersGetRequest) = repo.getById(params.map)
suspend fun storeUsers(users: List<VkUser>) = dao.insert(users)
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api.network.videos
@@ -0,0 +1,35 @@
package com.meloda.fast.api.network.videos
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class VideosSaveResponse(
@SerializedName("access_key")
val accessKey: String,
val description: String,
@SerializedName("owner_id")
val ownerId: Int,
val title: String,
@SerializedName("upload_url")
val uploadUrl: String,
@SerializedName("video_id")
val videoId: Int
) : Parcelable {
}
@Parcelize
data class VideosUploadResponse(
@SerializedName("video_hash")
val hash: String?,
val size: Int,
@SerializedName("direct_link")
val directLink: String,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("video_id")
val videoId: Int,
val error: String?
) : Parcelable
@@ -0,0 +1,9 @@
package com.meloda.fast.api.network.videos
import com.meloda.fast.api.network.VkUrls
object VideosUrls {
const val Save = "${VkUrls.API}/video.save"
}
@@ -1,7 +1,10 @@
package com.meloda.fast.base
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
import com.meloda.fast.screens.main.MainActivity
abstract class BaseFragment : Fragment {
@@ -9,4 +12,36 @@ abstract class BaseFragment : Fragment {
constructor(@LayoutRes resId: Int) : super(resId)
protected var shouldNavBarShown: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments == null) arguments = Bundle()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(requireActivity() as? MainActivity)?.run {
toggleNavBarVisibility(shouldNavBarShown)
}
}
val activityRouter
get() = run {
if (requireActivity() is MainActivity) {
(requireActivity() as MainActivity).router
} else {
null
}
}
fun requireActivityRouter() = requireNotNull(activityRouter)
fun startProgress() = toggleProgress(true)
fun stopProgress() = toggleProgress(false)
protected open fun toggleProgress(isProgressing: Boolean) {}
}
@@ -1,45 +0,0 @@
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
abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
constructor() : super()
constructor(@LayoutRes resId: Int) : super(resId)
protected abstract val viewModel: VM
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksEvent.onEach { onEvent(it) }.collect()
}
}
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))
}
}
}
@@ -1,12 +1,14 @@
package com.meloda.fast.base
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
abstract class ResourceManager(protected val context: Context) {
abstract class ResourceProvider(protected val context: Context) {
protected fun getString(@StringRes resId: Int): String {
return context.getString(resId)
@@ -17,4 +19,8 @@ abstract class ResourceManager(protected val context: Context) {
return ContextCompat.getColor(context, resId)
}
protected fun getDrawable(@DrawableRes resId: Int): Drawable? {
return ContextCompat.getDrawable(context, resId)
}
}
@@ -1,25 +1,26 @@
package com.meloda.fast.base.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.meloda.fast.model.DataItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlin.properties.Delegates
@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
@SuppressLint("NotifyDataSetChanged")
abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
var context: Context,
diffUtil: DiffUtil.ItemCallback<T>,
preAddedValues: List<T> = emptyList(),
) : ListAdapter<T, VH>(diffUtil) {
) : ListAdapter<T, VH>(diffUtil), Filterable {
private var valuesFilter: ValuesFilter? = null
protected val adapterScope = CoroutineScope(Dispatchers.Default)
private val cleanList = mutableListOf<T>()
@@ -29,13 +30,19 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
var itemClickListener: ((position: Int) -> Unit)? = null
var itemLongClickListener: ((position: Int) -> Boolean)? = null
private val listForSave = mutableListOf<T>()
var isSearching: Boolean by Delegates.observable(false) { _, _, _ ->
updateSearchingState()
}
init {
cleanList.addAll(preAddedValues)
addAll(preAddedValues)
}
fun cloneCurrentList(): MutableList<T> {
return ArrayList(currentList)
return currentList.toMutableList()
}
open fun destroy() {}
@@ -142,6 +149,11 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
return currentList.indexOf(item)
}
fun searchIndexOf(item: T): Int? {
val index = indexOf(item)
return if (index == -1) null else index
}
val indices get() = currentList.indices
operator fun get(position: Int): T {
@@ -161,9 +173,8 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
fun isEmpty() = currentList.isEmpty()
fun isNotEmpty() = currentList.isNotEmpty()
@SuppressLint("NotifyDataSetChanged")
fun refreshList() {
notifyDataSetChanged()
notifyItemRangeChanged(0, itemCount)
}
fun updateCleanList(list: List<T>?) {
@@ -201,4 +212,86 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
}
val lastPosition get() = currentList.lastIndex
private fun updateSearchingState() {
Log.d("BaseAdapter", "updateSearchingState: $isSearching")
cleanList.clear()
if (isSearching) {
listForSave.clear()
listForSave += cloneCurrentList()
} else {
setItems(listForSave, commitCallback = {
listForSave.clear()
})
}
}
open fun filter(query: String) {
if (cleanList.isEmpty()) {
cleanList.addAll(listForSave)
}
val newList = mutableListOf<T>()
setItems(emptyList(), commitCallback = {
if (query.isEmpty()) {
newList.addAll(cleanList)
} else {
for (item in cleanList) {
if (onQueryItem(item, query)) {
newList.add(item)
}
}
}
setItems(newList)
})
}
open fun onQueryItem(item: T, query: String): Boolean {
return false
}
override fun getFilter(): Filter {
if (valuesFilter == null) {
valuesFilter = ValuesFilter()
}
return requireNotNull(valuesFilter)
}
private inner class ValuesFilter : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val results = FilterResults()
if (isEmpty()) return results
if (!constraint.isNullOrEmpty()) {
val filteredList = mutableListOf<T>()
for (item in listForSave) {
if (onQueryItem(item, constraint.toString())) {
filteredList.add(item)
}
}
results.count = filteredList.size
results.values = filteredList
} else {
results.count = listForSave.size
results.values = listForSave
}
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
val items = results.values as? List<T>
setItems(items)
}
}
override fun onCurrentListChanged(previousList: MutableList<T>, currentList: MutableList<T>) {
super.onCurrentListChanged(previousList, currentList)
}
}
@@ -1,3 +0,0 @@
package com.meloda.fast.base.adapter
abstract class BaseItem
@@ -2,7 +2,6 @@ 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) {
@@ -12,6 +11,4 @@ abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
open fun bind(position: Int, payloads: MutableList<Any>?) {}
}
abstract class BindingHolder<B : ViewBinding>(protected val binding: B) : BaseHolder(binding.root)
}
@@ -2,15 +2,19 @@ package com.meloda.fast.base.viewmodel
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.VkErrorCodes
import com.meloda.fast.api.network.VkErrors
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.AuthorizationError
import com.meloda.fast.api.network.CaptchaRequiredError
import com.meloda.fast.api.network.ValidationRequiredError
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseViewModel : ViewModel() {
var unknownErrorDefaultText: String = ""
@@ -18,19 +22,47 @@ abstract class BaseViewModel : ViewModel() {
protected val tasksEventChannel = Channel<VkEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow()
protected val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
viewModelScope.launch { onException(throwable) }
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(exceptionHandler, block = block)
}
protected suspend fun <T> makeSuspendJob(
job: suspend () -> ApiAnswer<T>, onAnswer: suspend (T) -> Unit = {},
onStart: (suspend () -> Unit)? = null,
onEnd: (suspend () -> Unit)? = null,
onError: (suspend (Throwable) -> Unit)? = null
): ApiAnswer<T> {
onStart?.invoke() ?: onStart()
val response = job()
when (response) {
is ApiAnswer.Success -> onAnswer(response.data)
is ApiAnswer.Error -> {
onError?.invoke(response.error) ?: checkErrors(response.error)
}
}
onEnd?.invoke()
return response
}
protected fun <T> makeJob(
job: suspend () -> Answer<T>,
job: suspend () -> ApiAnswer<T>,
onAnswer: suspend (T) -> Unit = {},
onStart: (suspend () -> Unit)? = null,
onEnd: (suspend () -> Unit)? = null,
onError: (suspend (Throwable) -> Unit)? = null
) = viewModelScope.launch {
): Job = viewModelScope.launch {
onStart?.invoke() ?: onStart()
when (val response = job()) {
is Answer.Success -> onAnswer(response.data)
is Answer.Error -> {
checkErrors(response.throwable)
onError?.invoke(response.throwable) ?: onError(response.throwable)
is ApiAnswer.Success -> onAnswer(response.data)
is ApiAnswer.Error -> {
onError?.invoke(response.error) ?: checkErrors(response.error)
}
}
}.also {
@@ -41,6 +73,10 @@ abstract class BaseViewModel : ViewModel() {
}
}
protected open suspend fun onException(throwable: Throwable) {
checkErrors(throwable)
}
protected suspend fun onStart() {
sendEvent(StartProgressEvent)
}
@@ -49,37 +85,24 @@ abstract class BaseViewModel : ViewModel() {
sendEvent(StopProgressEvent)
}
protected suspend fun onError(throwable: Throwable) {
sendEvent(ErrorEvent(throwable.message ?: unknownErrorDefaultText))
}
protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event)
private suspend fun checkErrors(throwable: Throwable) {
protected suspend fun checkErrors(throwable: Throwable) {
when (throwable) {
is ApiError -> {
when (throwable.errorCode) {
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
sendEvent(IllegalTokenEvent)
}
}
is AuthorizationError -> {
sendEvent(AuthorizationErrorEvent)
}
is VKException -> {
when (throwable.error) {
VkErrors.NEED_CAPTCHA -> {
val json = throwable.json ?: return
sendEvent(
CaptchaEvent(
sid = json.optString("captcha_sid"),
image = json.optString("captcha_img")
)
)
}
VkErrors.NEED_VALIDATION -> {
val json = throwable.json ?: return
sendEvent(ValidationEvent(sid = json.optString("validation_sid")))
}
}
is ValidationRequiredError -> {
sendEvent(ValidationRequiredEvent(throwable.validationSid))
}
is CaptchaRequiredError -> {
sendEvent(CaptchaRequiredEvent(throwable.captchaSid, throwable.captchaImg))
}
is ApiError -> {
sendEvent(ErrorTextEvent(errorText = throwable.errorMessage ?: unknownErrorDefaultText))
}
else -> {
sendEvent(ErrorTextEvent(throwable.message ?: unknownErrorDefaultText))
}
}
}
@@ -0,0 +1,34 @@
package com.meloda.fast.base.viewmodel
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.base.BaseFragment
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
constructor() : super()
constructor(@LayoutRes resId: Int) : super(resId)
protected abstract val viewModel: VM
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToViewModel(viewModel)
}
protected open fun onEvent(event: VkEvent) {
ViewModelUtils.parseEvent(this, event)
}
protected fun <T : BaseViewModel> subscribeToViewModel(viewModel: T) {
lifecycleScope.launch {
viewModel.tasksEvent.collect { onEvent(it) }
}
}
}
@@ -1,22 +1,17 @@
package com.meloda.fast.base.viewmodel
data class ShowDialogInfoEvent(
val title: String? = null,
val message: String,
val positiveBtn: String? = null,
val negativeBtn: String? = null
) : VkEvent()
data class ErrorEvent(val errorText: String) : VkEvent()
object IllegalTokenEvent : VkEvent()
data class CaptchaEvent(val sid: String, val image: String) : VkEvent()
data class ValidationEvent(val sid: String) : VkEvent()
object StartProgressEvent : VkEvent()
object StopProgressEvent : VkEvent()
abstract class VkEvent
abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent()
abstract class VkProgressEvent : VkEvent()
open class ErrorTextEvent(override val errorText: String) : VkErrorEvent()
object AuthorizationErrorEvent : VkErrorEvent()
data class CaptchaRequiredEvent(val sid: String, val image: String) : VkErrorEvent()
data class ValidationRequiredEvent(val sid: String) : VkErrorEvent()
object StartProgressEvent : VkProgressEvent()
object StopProgressEvent : VkProgressEvent()
interface VkEventCallback<in T : Any> {
fun onEvent(event: T)
@@ -0,0 +1,49 @@
package com.meloda.fast.base.viewmodel
import android.content.Intent
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.screens.main.MainActivity
import com.meloda.fast.util.ViewUtils.showErrorDialog
object ViewModelUtils {
@Suppress("MemberVisibilityCanBePrivate")
fun parseEvent(activity: FragmentActivity, event: VkEvent) {
when (event) {
is AuthorizationErrorEvent -> {
Toast.makeText(
activity, R.string.authorization_failed, Toast.LENGTH_LONG
).show()
UserConfig.clear()
activity.finishAffinity()
activity.startActivity(Intent(activity, MainActivity::class.java))
}
is VkErrorEvent -> {
event.errorText?.run {
activity.showErrorDialog(this)
}
}
}
}
fun parseEvent(fragment: Fragment, event: VkEvent) {
if (event is VkProgressEvent) {
if (fragment is BaseFragment) {
if (event is StartProgressEvent) {
fragment.startProgress()
} else if (event is StopProgressEvent) {
fragment.stopProgress()
}
}
} else {
parseEvent(fragment.requireActivity(), event)
}
}
}
@@ -0,0 +1,7 @@
package com.meloda.fast.common
object AppConstants {
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
}
@@ -1,6 +1,7 @@
package com.meloda.fast.common
import android.app.Application
import android.app.DownloadManager
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
@@ -12,10 +13,9 @@ import android.view.inputmethod.InputMethodManager
import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.meloda.fast.BuildConfig
import com.meloda.fast.database.AppDatabase
import dagger.hilt.android.HiltAndroidApp
import org.acra.ACRA
import kotlin.math.roundToInt
import kotlin.math.sqrt
@HiltAndroidApp
@@ -26,6 +26,7 @@ class AppGlobal : Application() {
lateinit var inputMethodManager: InputMethodManager
lateinit var connectivityManager: ConnectivityManager
lateinit var clipboardManager: ClipboardManager
lateinit var downloadManager: DownloadManager
lateinit var preferences: SharedPreferences
lateinit var resources: Resources
@@ -37,11 +38,13 @@ class AppGlobal : Application() {
lateinit var packageManager: PackageManager
var versionName = ""
var versionCode = 0L
var versionCode = 0
var screenWidth = 0
var screenHeight = 0
var screenWidth80 = 0
val Instance get() = instance
}
@@ -49,19 +52,15 @@ class AppGlobal : Application() {
super.onCreate()
instance = this
if (!BuildConfig.DEBUG) {
ACRA.init(this)
}
appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
.fallbackToDestructiveMigration()
// .fallbackToDestructiveMigration()
.build()
preferences = PreferenceManager.getDefaultSharedPreferences(this)
val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
versionName = info.versionName
versionCode = PackageInfoCompat.getLongVersionCode(info)
versionCode = PackageInfoCompat.getLongVersionCode(info).toInt()
Companion.resources = resources
Companion.packageName = packageName
@@ -70,6 +69,8 @@ class AppGlobal : Application() {
screenWidth = resources.displayMetrics.widthPixels
screenHeight = resources.displayMetrics.heightPixels
screenWidth80 = (screenWidth * 0.8).roundToInt()
val density = resources.displayMetrics.density
val densityDpi = resources.displayMetrics.densityDpi
val densityScaled = resources.displayMetrics.scaledDensity
@@ -82,11 +83,12 @@ class AppGlobal : Application() {
Log.i(
"Fast::DeviceInfo",
"width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
"width: $screenWidth; 70% width: $screenWidth80; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
)
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
}
}
@@ -12,7 +12,7 @@ import kotlinx.coroutines.Job
object AppSettings {
val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled")
val keyUseNavigationDrawer = booleanPreferencesKey("use_nav_drawer")
}
@@ -1,17 +1,50 @@
package com.meloda.fast.common
import android.os.Bundle
import com.github.terrakok.cicerone.androidx.FragmentScreen
import com.meloda.fast.api.model.VkConversation
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.model.UpdateItem
import com.meloda.fast.screens.conversations.ConversationsFragment
import com.meloda.fast.screens.login.LoginFragment
import com.meloda.fast.screens.main.MainFragment
import com.meloda.fast.screens.messages.ForwardedMessagesFragment
import com.meloda.fast.screens.messages.MessagesHistoryFragment
import com.meloda.fast.screens.settings.SettingsRootFragment
import com.meloda.fast.screens.updates.UpdatesFragment
@Suppress("FunctionName")
object Screens {
fun Main() = FragmentScreen { MainFragment() }
fun Login() = FragmentScreen { LoginFragment() }
fun Login(
getFastToken: Boolean = false
) = FragmentScreen {
LoginFragment.newInstance(getFastToken)
}
fun Conversations() = FragmentScreen { ConversationsFragment() }
fun MessagesHistory(bundle: Bundle) =
FragmentScreen { MessagesHistoryFragment.newInstance(bundle) }
fun MessagesHistory(
conversation: VkConversation,
user: VkUser?,
group: VkGroup?
) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) }
fun ForwardedMessages(
conversation: VkConversation,
messages: List<VkMessage>,
profiles: HashMap<Int, VkUser> = hashMapOf(),
groups: HashMap<Int, VkGroup> = hashMapOf()
) = FragmentScreen {
ForwardedMessagesFragment.newInstance(
conversation, messages, profiles, groups
)
}
fun Updates(updateItem: UpdateItem? = null) =
FragmentScreen { UpdatesFragment.newInstance(updateItem) }
fun Settings() = FragmentScreen { SettingsRootFragment() }
}
@@ -1,98 +0,0 @@
package com.meloda.fast.common
import android.content.Context
import android.content.IntentFilter
import com.meloda.fast.receiver.MinuteReceiver
import java.util.*
object TimeManager {
var currentHour = 0
var currentMinute = 0
var currentSecond = 0
private val onHourChangeListeners: ArrayList<OnHourChangeListener> = ArrayList()
private val onMinuteChangeListeners: ArrayList<OnMinuteChangeListener> = ArrayList()
private val onSecondChangeListeners: ArrayList<OnSecondChangeListener> = ArrayList()
private val onTimeChangeListeners: ArrayList<OnTimeChangeListener> = ArrayList()
fun init(context: Context) {
context.registerReceiver(MinuteReceiver(), IntentFilter("android.intent.action.TIME_TICK"))
addOnMinuteChangeListener(minuteChangeListener)
}
private var minuteChangeListener = object : OnMinuteChangeListener {
override fun onMinuteChange(currentMinute: Int) {
TimeManager.currentMinute = currentMinute
}
}
fun destroy() {
removeOnMinuteChangeListener(minuteChangeListener)
}
fun broadcastMinute() {
for (onMinuteChangeListener in onMinuteChangeListeners) {
onMinuteChangeListener.onMinuteChange(0)
}
}
val isMorning = currentHour in 7..11
val isAfternoon = currentHour in 12..16
val isEvening = currentHour in 17..22
val isNight = currentHour == 23 || currentHour < 6 && currentHour > -1
fun addOnHourChangeListener(onHourChangeListeners: OnHourChangeListener) {
TimeManager.onHourChangeListeners.add(onHourChangeListeners)
}
fun removeOnHourChangeListener(onHourChangeListener: OnHourChangeListener?) {
onHourChangeListeners.remove(onHourChangeListener)
}
fun addOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener) {
onMinuteChangeListeners.add(onMinuteChangeListener)
}
fun removeOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener?) {
onMinuteChangeListeners.remove(onMinuteChangeListener)
}
fun addOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener) {
onSecondChangeListeners.add(onSecondChangeListener)
}
fun removeOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener?) {
onSecondChangeListeners.remove(onSecondChangeListener)
}
fun addOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener) {
onTimeChangeListeners.add(onTimeChangeListener)
}
fun removeOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
onTimeChangeListeners.remove(onTimeChangeListener)
}
interface OnHourChangeListener {
fun onHourChange(currentHour: Int)
}
interface OnMinuteChangeListener {
fun onMinuteChange(currentMinute: Int)
}
interface OnSecondChangeListener {
fun onSecondChange(currentSecond: Int)
}
interface OnTimeChangeListener {
fun onHourChange(currentHour: Int)
fun onMinuteChange(currentMinute: Int)
fun onSecondChange(currentSecond: Int)
}
}
@@ -0,0 +1,100 @@
package com.meloda.fast.common
import androidx.lifecycle.MutableLiveData
import com.meloda.fast.BuildConfig
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
import com.meloda.fast.data.ota.OtaApi
import com.meloda.fast.extensions.setIfNotEquals
import com.meloda.fast.model.UpdateActualUrl
import com.meloda.fast.model.UpdateItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import kotlin.coroutines.CoroutineContext
class UpdateManager(private val repo: OtaApi) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default
companion object {
val newUpdate = MutableLiveData<UpdateItem?>(null)
val updateError = MutableLiveData<Throwable?>(null)
var otaBaseUrl: String? = null
private set
}
private var listener: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null
private fun getActualUrl() = launch {
val job: suspend () -> ApiAnswer<UpdateActualUrl> = { repo.getActualUrl() }
when (val jobResponse = job()) {
is ApiAnswer.Success -> {
val item = jobResponse.data
otaBaseUrl = item.url
getLatestRelease()
}
is ApiAnswer.Error -> {
otaBaseUrl = null
val throwable = jobResponse.error.throwable
listener?.invoke(null, throwable)
withContext(Dispatchers.Main) {
updateError.setIfNotEquals(throwable)
}
}
}
}
private fun getLatestRelease() = launch {
val url = "$otaBaseUrl/releases-latest"
val job: suspend () -> ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>> = {
repo.getLatestRelease(url = url, secretCode = getOtaSecret())
}
withContext(Dispatchers.Main) {
when (val jobResponse = job()) {
is ApiAnswer.Success -> {
val response = jobResponse.data.response ?: return@withContext
val latestRelease = response.release
if (latestRelease != null &&
(AppGlobal.versionName
.split("_")
.getOrNull(1) != latestRelease.versionName ||
AppGlobal.versionCode < latestRelease.versionCode)
) {
newUpdate.setIfNotEquals(latestRelease)
listener?.invoke(latestRelease, null)
} else {
newUpdate.setIfNotEquals(null)
listener?.invoke(null, null)
}
}
is ApiAnswer.Error -> {
val throwable = jobResponse.error.throwable
updateError.setIfNotEquals(throwable)
listener?.invoke(null, throwable)
}
}
}
}
private fun getOtaSecret(): String {
return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8")
}
fun checkUpdates(block: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null) = launch {
this@UpdateManager.listener = block
getActualUrl()
}
}
@@ -1,17 +1,18 @@
package com.meloda.fast.api.network.account
package com.meloda.fast.data.account
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.account.AccountUrls
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.QueryMap
interface AccountRepo {
interface AccountApi {
@GET(AccountUrls.SetOnline)
suspend fun setOnline(@QueryMap params: Map<String, String>): Answer<ApiResponse<Any>>
suspend fun setOnline(@QueryMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@POST(AccountUrls.SetOffline)
suspend fun setOffline(@QueryMap params: Map<String, String>): Answer<ApiResponse<Any>>
suspend fun setOffline(@QueryMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
}
@@ -0,0 +1,18 @@
package com.meloda.fast.data.account
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.meloda.fast.model.AppAccount
@Dao
interface AccountsDao {
@Query("SELECT * FROM accounts")
suspend fun getAll(): List<AppAccount>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(values: List<AppAccount>)
}
@@ -0,0 +1,17 @@
package com.meloda.fast.data.account
import com.meloda.fast.api.network.account.AccountSetOfflineRequest
import com.meloda.fast.api.network.account.AccountSetOnlineRequest
class AccountsRepository(
private val accountApi: AccountApi,
private val accountsDao: AccountsDao
) {
suspend fun setOnline(params: AccountSetOnlineRequest) = accountApi.setOnline(params.map)
suspend fun setOffline(params: AccountSetOfflineRequest) = accountApi.setOffline(params.map)
}
@@ -0,0 +1,28 @@
package com.meloda.fast.data.audios
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.attachments.BaseVkAudio
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.audio.AudiosGetUploadServerResponse
import com.meloda.fast.api.network.audio.AudiosUploadResponse
import com.meloda.fast.api.network.audio.AudiosUrls
import okhttp3.MultipartBody
import retrofit2.http.*
interface AudiosApi {
@POST(AudiosUrls.GetUploadServer)
suspend fun getUploadServer(): ApiAnswer<ApiResponse<AudiosGetUploadServerResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part file: MultipartBody.Part
): ApiAnswer<AudiosUploadResponse>
@FormUrlEncoded
@POST(AudiosUrls.Save)
suspend fun save(@FieldMap map: Map<String, String>): ApiAnswer<ApiResponse<BaseVkAudio>>
}
@@ -0,0 +1,21 @@
package com.meloda.fast.data.audios
import okhttp3.MultipartBody
class AudiosRepository(
private val audiosApi: AudiosApi
) {
suspend fun getUploadServer() = audiosApi.getUploadServer()
suspend fun upload(url: String, file: MultipartBody.Part) = audiosApi.upload(url, file)
suspend fun save(server: Int, audio: String, hash: String) = audiosApi.save(
mapOf(
"server" to server.toString(),
"audio" to audio,
"hash" to hash
)
)
}
@@ -0,0 +1,19 @@
package com.meloda.fast.data.auth
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.auth.AuthDirectResponse
import com.meloda.fast.api.network.auth.AuthUrls
import com.meloda.fast.api.network.auth.SendSmsResponse
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthApi {
@GET(AuthUrls.DirectAuth)
suspend fun auth(@QueryMap param: Map<String, String?>): ApiAnswer<AuthDirectResponse>
@GET(AuthUrls.SendSms)
suspend fun sendSms(@Query("sid") validationSid: String): ApiAnswer<SendSmsResponse>
}
@@ -0,0 +1,14 @@
package com.meloda.fast.data.auth
import com.meloda.fast.api.network.auth.AuthDirectRequest
class AuthRepository(
private val authApi: AuthApi
) {
suspend fun auth(params: AuthDirectRequest) = authApi.auth(params.map)
suspend fun sendSms(validationSid: String) = authApi.sendSms(validationSid)
}
@@ -0,0 +1,33 @@
package com.meloda.fast.data.conversations
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.conversations.ConversationsGetResponse
import com.meloda.fast.api.network.conversations.ConversationsUrls
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface ConversationsApi {
@FormUrlEncoded
@POST(ConversationsUrls.Get)
suspend fun get(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<ConversationsGetResponse>>
@FormUrlEncoded
@POST(ConversationsUrls.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.ReorderPinned)
suspend fun reorderPinned(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
}
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao
package com.meloda.fast.data.conversations
import androidx.room.Dao
import androidx.room.Insert
@@ -15,6 +15,4 @@ interface ConversationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(values: List<VkConversation>)
suspend fun insert(values: Array<out VkConversation>) = insert(values.toList())
}
@@ -0,0 +1,25 @@
package com.meloda.fast.data.conversations
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest
import com.meloda.fast.api.network.conversations.ConversationsGetRequest
import com.meloda.fast.api.network.conversations.ConversationsPinRequest
import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest
import kotlinx.coroutines.sync.Mutex
class ConversationsRepository(
private val conversationsApi: ConversationsApi,
private val conversationsDao: ConversationsDao
) {
suspend fun get(params: ConversationsGetRequest) = conversationsApi.get(params.map)
suspend fun delete(params: ConversationsDeleteRequest) = conversationsApi.delete(params.map)
suspend fun pin(params: ConversationsPinRequest) = conversationsApi.pin(params.map)
suspend fun unpin(params: ConversationsUnpinRequest) = conversationsApi.unpin(params.map)
suspend fun store(conversations: List<VkConversation>) = conversationsDao.insert(conversations)
}
@@ -0,0 +1,33 @@
package com.meloda.fast.data.files
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.files.FilesGetMessagesUploadServerResponse
import com.meloda.fast.api.network.files.FilesSaveFileResponse
import com.meloda.fast.api.network.files.FilesUploadFileResponse
import com.meloda.fast.api.network.files.FilesUrls
import okhttp3.MultipartBody
import retrofit2.http.*
interface FilesApi {
@FormUrlEncoded
@POST(FilesUrls.GetMessagesUploadServer)
suspend fun getUploadServer(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<FilesGetMessagesUploadServerResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part file: MultipartBody.Part
): ApiAnswer<FilesUploadFileResponse>
@FormUrlEncoded
@POST(FilesUrls.Save)
suspend fun save(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<FilesSaveFileResponse>>
}
@@ -0,0 +1,30 @@
package com.meloda.fast.data.files
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
class FilesRepository(
private val filesApi: FilesApi
) {
enum class FileType(val value: String) {
@SerializedName("doc")
File("doc"),
@SerializedName("audio_message")
VoiceMessage("audio_message")
}
suspend fun getMessagesUploadServer(peerId: Int, type: FileType) =
filesApi.getUploadServer(
mapOf(
"peer_id" to peerId.toString(),
"type" to type.value
)
)
suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesApi.upload(url, file)
suspend fun saveMessageFile(file: String) = filesApi.save(mapOf("file" to file))
}
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao
package com.meloda.fast.data.groups
import androidx.room.Dao
import androidx.room.Insert
@@ -0,0 +1,6 @@
package com.meloda.fast.data.groups
class GroupsRepository(
private val groupsDao: GroupsDao
) {
}
@@ -1,17 +1,17 @@
package com.meloda.fast.api.network.longpoll
package com.meloda.fast.data.longpoll
import com.google.gson.JsonObject
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.ApiAnswer
import retrofit2.http.GET
import retrofit2.http.QueryMap
import retrofit2.http.Url
interface LongPollRepo {
interface LongPollApi {
@GET
suspend fun getResponse(
@Url serverUrl: String,
@QueryMap params: Map<String, String>
): Answer<JsonObject>
): ApiAnswer<JsonObject>
}
@@ -1,49 +1,56 @@
package com.meloda.fast.api.network.messages
package com.meloda.fast.data.messages
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.BaseVkLongPoll
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.messages.MessagesGetByIdResponse
import com.meloda.fast.api.network.messages.MessagesGetHistoryResponse
import com.meloda.fast.api.network.messages.MessagesUrls
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface MessagesRepo {
interface MessagesApi {
@FormUrlEncoded
@POST(MessagesUrls.GetHistory)
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>>
suspend fun getHistory(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<MessagesGetHistoryResponse>>
@FormUrlEncoded
@POST(MessagesUrls.Send)
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
suspend fun send(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
@FormUrlEncoded
@POST(MessagesUrls.MarkAsImportant)
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
suspend fun markAsImportant(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<List<Int>>>
@FormUrlEncoded
@POST(MessagesUrls.GetLongPollServer)
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>>
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<BaseVkLongPoll>>
@FormUrlEncoded
@POST(MessagesUrls.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkMessage>>
suspend fun pin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<BaseVkMessage>>
@FormUrlEncoded
@POST(MessagesUrls.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
suspend fun unpin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(MessagesUrls.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
suspend fun delete(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(MessagesUrls.Edit)
suspend fun edit(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
suspend fun edit(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(MessagesUrls.GetById)
suspend fun getById(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetByIdResponse>>
suspend fun getById(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<MessagesGetByIdResponse>>
@FormUrlEncoded
@POST(MessagesUrls.MarkAsRead)
suspend fun markAsRead(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
}
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao
package com.meloda.fast.data.messages
import androidx.room.Dao
import androidx.room.Insert
@@ -0,0 +1,65 @@
package com.meloda.fast.data.messages
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
import com.meloda.fast.data.longpoll.LongPollApi
import com.meloda.fast.api.network.messages.*
class MessagesRepository(
private val messagesApi: MessagesApi,
private val messagesDao: MessagesDao,
private val longPollApi: LongPollApi
) {
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
suspend fun getHistory(params: MessagesGetHistoryRequest) =
messagesApi.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) =
messagesApi.send(params.map)
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
messagesApi.markAsImportant(params.map)
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
messagesApi.getLongPollServer(params.map)
suspend fun pin(params: MessagesPinMessageRequest) =
messagesApi.pin(params.map)
suspend fun unpin(params: MessagesUnPinMessageRequest) =
messagesApi.unpin(params.map)
suspend fun delete(params: MessagesDeleteRequest) =
messagesApi.delete(params.map)
suspend fun edit(params: MessagesEditRequest) =
messagesApi.edit(params.map)
suspend fun getLongPollUpdates(
serverUrl: String,
params: LongPollGetUpdatesRequest
) = longPollApi.getResponse(serverUrl, params.map)
suspend fun getById(params: MessagesGetByIdRequest) =
messagesApi.getById(params.map)
suspend fun markAsRead(
peerId: Int,
messagesIds: List<Int>? = null,
startMessageId: Int? = null
) = messagesApi.markAsRead(
mutableMapOf("peer_id" to peerId.toString()).apply {
messagesIds?.let {
this["message_ids"] = messagesIds.joinToString { it.toString() }
}
startMessageId?.let {
this["start_message_id"] = it.toString()
}
}
)
}
@@ -0,0 +1,26 @@
package com.meloda.fast.data.ota
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
import com.meloda.fast.api.network.ota.OtaUrls
import com.meloda.fast.model.UpdateActualUrl
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
import retrofit2.http.Url
interface OtaApi {
@GET(OtaUrls.GetActualUrl)
suspend fun getActualUrl(): ApiAnswer<UpdateActualUrl>
@GET
suspend fun getLatestRelease(
@Url url: String,
@Query("productId") productId: Int = 28,
@Query("branchId") branchId: Int = 10,
@Header("Secret-Code") secretCode: String
): ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>>
}
@@ -0,0 +1,33 @@
package com.meloda.fast.data.photos
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.photos.PhotoUrls
import com.meloda.fast.api.network.photos.PhotosGetMessagesUploadServerResponse
import com.meloda.fast.api.network.photos.PhotosUploadPhotoResponse
import okhttp3.MultipartBody
import retrofit2.http.*
interface PhotosApi {
@FormUrlEncoded
@POST(PhotoUrls.GetMessagesUploadServer)
suspend fun getUploadServer(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<PhotosGetMessagesUploadServerResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part photo: MultipartBody.Part
): ApiAnswer<PhotosUploadPhotoResponse>
@FormUrlEncoded
@POST(PhotoUrls.SaveMessagePhoto)
suspend fun save(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<List<BaseVkPhoto>>>
}
@@ -0,0 +1,18 @@
package com.meloda.fast.data.photos
import com.meloda.fast.api.network.photos.PhotosSaveMessagePhotoRequest
import okhttp3.MultipartBody
class PhotosRepository(
private val photosApi: PhotosApi
) {
suspend fun getMessagesUploadServer(peerId: Int) =
photosApi.getUploadServer(mapOf("peer_id" to peerId.toString()))
suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = photosApi.upload(url, photo)
suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) =
photosApi.save(body.map)
}
@@ -1,18 +1,19 @@
package com.meloda.fast.api.network.users
package com.meloda.fast.data.users
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.BaseVkUser
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.users.UsersUrls
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface UsersRepo {
interface UsersApi {
@FormUrlEncoded
@POST(UsersUrls.GetById)
suspend fun getById(
@FieldMap params: Map<String, String>?
): Answer<ApiResponse<List<BaseVkUser>>>
): ApiAnswer<ApiResponse<List<BaseVkUser>>>
}
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao
package com.meloda.fast.data.users
import androidx.room.Dao
import androidx.room.Insert
@@ -0,0 +1,17 @@
package com.meloda.fast.data.users
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.users.UsersGetRequest
class UsersRepository(
private val usersApi: UsersApi,
private val usersDao: UsersDao
) {
suspend fun getById(params: UsersGetRequest) = usersApi.getById(params.map)
suspend fun storeUsers(users: List<VkUser>) {
usersDao.insert(users)
}
}
@@ -0,0 +1,26 @@
package com.meloda.fast.data.videos
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.videos.VideosSaveResponse
import com.meloda.fast.api.network.videos.VideosUploadResponse
import com.meloda.fast.api.network.videos.VideosUrls
import okhttp3.MultipartBody
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Url
interface VideosApi {
@POST(VideosUrls.Save)
suspend fun save(): ApiAnswer<ApiResponse<VideosSaveResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part file: MultipartBody.Part
): ApiAnswer<VideosUploadResponse>
}
@@ -0,0 +1,13 @@
package com.meloda.fast.data.videos
import okhttp3.MultipartBody
class VideosRepository(
private val videosApi: VideosApi
) {
suspend fun save() = videosApi.save()
suspend fun upload(url: String, file: MultipartBody.Part) = videosApi.upload(url, file)
}
@@ -1,5 +1,6 @@
package com.meloda.fast.database
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@@ -7,27 +8,34 @@ import com.meloda.fast.api.model.VkConversation
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.database.dao.ConversationsDao
import com.meloda.fast.database.dao.GroupsDao
import com.meloda.fast.database.dao.MessagesDao
import com.meloda.fast.database.dao.UsersDao
import com.meloda.fast.data.account.AccountsDao
import com.meloda.fast.data.conversations.ConversationsDao
import com.meloda.fast.data.groups.GroupsDao
import com.meloda.fast.data.messages.MessagesDao
import com.meloda.fast.data.users.UsersDao
import com.meloda.fast.model.AppAccount
@Database(
entities = [
AppAccount::class,
VkConversation::class,
VkMessage::class,
VkUser::class,
VkGroup::class
],
version = 28,
exportSchema = false,
version = 34,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 33, to = 34)
]
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun conversationsDao(): ConversationsDao
abstract fun messagesDao(): MessagesDao
abstract fun usersDao(): UsersDao
abstract fun groupsDao(): GroupsDao
abstract val accountsDao: AccountsDao
abstract val conversationsDao: ConversationsDao
abstract val messagesDao: MessagesDao
abstract val usersDao: UsersDao
abstract val groupsDao: GroupsDao
}
@@ -4,6 +4,7 @@ import androidx.room.TypeConverter
import com.google.gson.Gson
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.api.model.base.BaseVkMessage
import org.json.JSONObject
@Suppress("UnnecessaryVariable")
@@ -13,6 +14,24 @@ class Converters {
private const val CACHE_SEPARATOR = "fastkruta228355"
}
@TypeConverter
fun fromGeoToString(geo: BaseVkMessage.Geo?): String? {
if (geo == null) return null
val string = Gson().toJson(geo)
return string
}
@TypeConverter
fun fromStringToGeo(string: String?): BaseVkMessage.Geo? {
if (string == null) return null
val geo = Gson().fromJson(string, BaseVkMessage.Geo::class.java)
return geo
}
@TypeConverter
fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
if (messages == null) return null
@@ -49,7 +68,9 @@ class Converters {
fun fromStringToVkMessage(string: String?): VkMessage? {
if (string == null) return null
return Gson().fromJson(string, VkMessage::class.java)
val message = Gson().fromJson(string, VkMessage::class.java)
return message
}
@TypeConverter
@@ -82,7 +103,9 @@ class Converters {
fun fromVkAttachmentToString(attachment: VkAttachment?): String? {
if (attachment == null) return null
return Gson().toJson(attachment)
val string = Gson().toJson(attachment)
return string
}
@TypeConverter
@@ -91,6 +114,8 @@ class Converters {
val className = JSONObject(string).optString("className")
return Gson().fromJson(string, Class.forName(className)) as VkAttachment?
val attachment = Gson().fromJson(string, Class.forName(className)) as? VkAttachment?
return attachment
}
}
@@ -0,0 +1,103 @@
package com.meloda.fast.di
import com.meloda.fast.data.longpoll.LongPollApi
import com.meloda.fast.data.account.AccountApi
import com.meloda.fast.data.account.AccountsDao
import com.meloda.fast.data.account.AccountsRepository
import com.meloda.fast.data.audios.AudiosApi
import com.meloda.fast.data.audios.AudiosRepository
import com.meloda.fast.data.auth.AuthApi
import com.meloda.fast.data.auth.AuthRepository
import com.meloda.fast.data.conversations.ConversationsApi
import com.meloda.fast.data.conversations.ConversationsDao
import com.meloda.fast.data.conversations.ConversationsRepository
import com.meloda.fast.data.files.FilesApi
import com.meloda.fast.data.files.FilesRepository
import com.meloda.fast.data.groups.GroupsDao
import com.meloda.fast.data.groups.GroupsRepository
import com.meloda.fast.data.messages.MessagesApi
import com.meloda.fast.data.messages.MessagesDao
import com.meloda.fast.data.messages.MessagesRepository
import com.meloda.fast.data.photos.PhotosApi
import com.meloda.fast.data.photos.PhotosRepository
import com.meloda.fast.data.users.UsersApi
import com.meloda.fast.data.users.UsersDao
import com.meloda.fast.data.users.UsersRepository
import com.meloda.fast.data.videos.VideosApi
import com.meloda.fast.data.videos.VideosRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object DataModule {
@Singleton
@Provides
fun provideConversationsRepository(
conversationsApi: ConversationsApi,
conversationsDao: ConversationsDao
): ConversationsRepository = ConversationsRepository(conversationsApi, conversationsDao)
@Singleton
@Provides
fun provideMessagesRepository(
messagesApi: MessagesApi,
messagesDao: MessagesDao,
longPollApi: LongPollApi
): MessagesRepository = MessagesRepository(messagesApi, messagesDao, longPollApi)
@Singleton
@Provides
fun provideUsersRepository(
usersApi: UsersApi,
usersDao: UsersDao
): UsersRepository = UsersRepository(usersApi, usersDao)
@Singleton
@Provides
fun provideGroupsRepository(
groupsDao: GroupsDao
): GroupsRepository = GroupsRepository(groupsDao)
@Singleton
@Provides
fun provideAuthRepository(
authApi: AuthApi
): AuthRepository = AuthRepository(authApi)
@Singleton
@Provides
fun provideAccountsRepository(
accountApi: AccountApi,
accountsDao: AccountsDao
): AccountsRepository = AccountsRepository(accountApi, accountsDao)
@Singleton
@Provides
fun providePhotosRepository(
photosApi: PhotosApi
): PhotosRepository = PhotosRepository(photosApi)
@Singleton
@Provides
fun provideVideosRepository(
videosApi: VideosApi
): VideosRepository = VideosRepository(videosApi)
@Singleton
@Provides
fun provideAudiosRepository(
audiosApi: AudiosApi
): AudiosRepository = AudiosRepository(audiosApi)
@Singleton
@Provides
fun provideFilesRepository(
filesApi: FilesApi
): FilesRepository = FilesRepository(filesApi)
}
@@ -1,11 +1,12 @@
package com.meloda.fast.di
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.data.account.AccountsDao
import com.meloda.fast.data.conversations.ConversationsDao
import com.meloda.fast.data.groups.GroupsDao
import com.meloda.fast.data.messages.MessagesDao
import com.meloda.fast.data.users.UsersDao
import com.meloda.fast.database.AppDatabase
import com.meloda.fast.database.dao.ConversationsDao
import com.meloda.fast.database.dao.GroupsDao
import com.meloda.fast.database.dao.MessagesDao
import com.meloda.fast.database.dao.UsersDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -23,22 +24,27 @@ object DatabaseModule {
@Provides
@Singleton
fun provideUsersDao(appDatabase: AppDatabase): UsersDao =
appDatabase.usersDao()
fun provideAccountsDao(appDatabase: AppDatabase): AccountsDao =
appDatabase.accountsDao
@Provides
@Singleton
fun provideConversationsDao(appDatabase: AppDatabase): ConversationsDao =
appDatabase.conversationsDao()
appDatabase.conversationsDao
@Provides
@Singleton
fun provideMessagesDao(appDatabase: AppDatabase): MessagesDao =
appDatabase.messagesDao()
appDatabase.messagesDao
@Provides
@Singleton
fun provideUsersDao(appDatabase: AppDatabase): UsersDao =
appDatabase.usersDao
@Provides
@Singleton
fun provideGroupsDao(appDatabase: AppDatabase): GroupsDao =
appDatabase.groupsDao()
appDatabase.groupsDao
}
@@ -1,24 +1,27 @@
package com.meloda.fast.di
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.meloda.fast.api.LongPollUpdatesParser
import com.meloda.fast.api.longpoll.LongPollUpdatesParser
import com.meloda.fast.api.network.AuthInterceptor
import com.meloda.fast.api.network.ResultCallFactory
import com.meloda.fast.api.network.account.AccountDataSource
import com.meloda.fast.api.network.account.AccountRepo
import com.meloda.fast.api.network.auth.AuthDataSource
import com.meloda.fast.api.network.auth.AuthRepo
import com.meloda.fast.api.network.conversations.ConversationsDataSource
import com.meloda.fast.api.network.conversations.ConversationsRepo
import com.meloda.fast.api.network.longpoll.LongPollRepo
import com.meloda.fast.api.network.messages.MessagesDataSource
import com.meloda.fast.api.network.messages.MessagesRepo
import com.meloda.fast.api.network.users.UsersDataSource
import com.meloda.fast.api.network.users.UsersRepo
import com.meloda.fast.database.dao.ConversationsDao
import com.meloda.fast.database.dao.MessagesDao
import com.meloda.fast.database.dao.UsersDao
import com.meloda.fast.api.network.VkUrls
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.UpdateManager
import com.meloda.fast.data.account.AccountApi
import com.meloda.fast.data.audios.AudiosApi
import com.meloda.fast.data.auth.AuthApi
import com.meloda.fast.data.conversations.ConversationsApi
import com.meloda.fast.data.files.FilesApi
import com.meloda.fast.data.longpoll.LongPollApi
import com.meloda.fast.data.messages.MessagesApi
import com.meloda.fast.data.messages.MessagesRepository
import com.meloda.fast.data.ota.OtaApi
import com.meloda.fast.data.photos.PhotosApi
import com.meloda.fast.data.users.UsersApi
import com.meloda.fast.data.videos.VideosApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -34,18 +37,68 @@ import javax.inject.Singleton
@Module
object NetworkModule {
/*
val chuckerCollector = ChuckerCollector(
context = this,
// Toggles visibility of the notification
showNotification = true,
// Allows to customize the retention period of collected data
retentionPeriod = RetentionManager.Period.ONE_HOUR
)
// Create the Interceptor
val chuckerInterceptor = ChuckerInterceptor.Builder(context)
// The previously created Collector
.collector(chuckerCollector)
// The max body content length in bytes, after this responses will be truncated.
.maxContentLength(250_000L)
// List of headers to replace with ** in the Chucker UI
.redactHeaders("Auth-Token", "Bearer")
// Read the whole response body even when the client does not consume the response completely.
// This is useful in case of parsing errors or when the response body
// is closed before being read like in Retrofit with Void and Unit types.
.alwaysReadResponseBody(true)
// Use decoder when processing request and response bodies. When multiple decoders are installed they
// are applied in an order they were added.
.addBodyDecoder(decoder)
// Controls Android shortcut creation. Available in SNAPSHOTS versions only at the moment
.createShortcut(true)
.build()
*/
@Singleton
@Provides
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(authInterceptor)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build()
fun provideChuckerCollector(): ChuckerCollector =
ChuckerCollector(AppGlobal.Instance)
@Singleton
@Provides
fun provideChuckerInterceptor(
chuckerCollector: ChuckerCollector
): ChuckerInterceptor =
ChuckerInterceptor.Builder(AppGlobal.Instance)
.collector(chuckerCollector)
.build()
@Singleton
@Provides
fun provideOkHttpClient(
authInterceptor: AuthInterceptor,
chuckerInterceptor: ChuckerInterceptor
): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(authInterceptor)
.addInterceptor(chuckerInterceptor)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
).build()
@Singleton
@Provides
@@ -59,7 +112,7 @@ object NetworkModule {
client: OkHttpClient,
gson: Gson
): Retrofit = Retrofit.Builder()
.baseUrl("https://api.vk.com/")
.baseUrl("${VkUrls.API}/")
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(ResultCallFactory())
.client(client)
@@ -71,73 +124,67 @@ object NetworkModule {
@Provides
@Singleton
fun provideAuthRepo(retrofit: Retrofit): AuthRepo =
retrofit.create(AuthRepo::class.java)
fun provideAuthApi(retrofit: Retrofit): AuthApi =
retrofit.create(AuthApi::class.java)
@Provides
@Singleton
fun provideConversationsRepo(retrofit: Retrofit): ConversationsRepo =
retrofit.create(ConversationsRepo::class.java)
fun provideConversationsApi(retrofit: Retrofit): ConversationsApi =
retrofit.create(ConversationsApi::class.java)
@Provides
@Singleton
fun provideUsersRepo(retrofit: Retrofit): UsersRepo =
retrofit.create(UsersRepo::class.java)
fun provideUsersApi(retrofit: Retrofit): UsersApi =
retrofit.create(UsersApi::class.java)
@Provides
@Singleton
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
retrofit.create(MessagesRepo::class.java)
fun provideMessagesApi(retrofit: Retrofit): MessagesApi =
retrofit.create(MessagesApi::class.java)
@Provides
@Singleton
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo =
retrofit.create(LongPollRepo::class.java)
fun provideLongPollApi(retrofit: Retrofit): LongPollApi =
retrofit.create(LongPollApi::class.java)
@Provides
@Singleton
fun provideAuthDataSource(
repo: AuthRepo
): AuthDataSource = AuthDataSource(repo)
fun provideLongPollUpdatesParser(messagesRepository: MessagesRepository): LongPollUpdatesParser =
LongPollUpdatesParser(messagesRepository)
@Provides
@Singleton
fun provideUsersDataSource(
repo: UsersRepo,
dao: UsersDao
): UsersDataSource = UsersDataSource(repo, dao)
fun provideAccountApi(retrofit: Retrofit): AccountApi =
retrofit.create(AccountApi::class.java)
@Provides
@Singleton
fun provideConversationsDataSource(
repo: ConversationsRepo,
dao: ConversationsDao
): ConversationsDataSource = ConversationsDataSource(repo, dao)
fun provideOtaApi(retrofit: Retrofit): OtaApi =
retrofit.create(OtaApi::class.java)
@Provides
@Singleton
fun provideMessagesDataSource(
messagesRepo: MessagesRepo,
messagesDao: MessagesDao,
longPollRepo: LongPollRepo
): MessagesDataSource = MessagesDataSource(
messagesRepo = messagesRepo,
messagesDao = messagesDao,
longPollRepo = longPollRepo
)
fun provideUpdateManager(otaApi: OtaApi): UpdateManager =
UpdateManager(otaApi)
@Provides
@Singleton
fun provideLongPollUpdatesParser(messagesDataSource: MessagesDataSource): LongPollUpdatesParser =
LongPollUpdatesParser(messagesDataSource)
fun providePhotosApi(retrofit: Retrofit): PhotosApi =
retrofit.create(PhotosApi::class.java)
@Provides
@Singleton
fun provideAccountRepo(retrofit: Retrofit): AccountRepo =
retrofit.create(AccountRepo::class.java)
fun provideVideosApi(retrofit: Retrofit): VideosApi =
retrofit.create(VideosApi::class.java)
@Provides
@Singleton
fun provideAccountDataSource(repo: AccountRepo): AccountDataSource =
AccountDataSource(repo)
fun provideAudiosApi(retrofit: Retrofit): AudiosApi =
retrofit.create(AudiosApi::class.java)
@Provides
@Singleton
fun provideFilesApi(retrofit: Retrofit): FilesApi =
retrofit.create(FilesApi::class.java)
}
@@ -2,17 +2,27 @@ package com.meloda.fast.extensions
import android.animation.ValueAnimator
import android.content.res.Resources
import android.os.Build
import android.graphics.drawable.Drawable
import android.os.Parcelable
import android.util.DisplayMetrics
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.annotation.StyleRes
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.lifecycle.MutableLiveData
import com.google.common.net.MediaType
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.databinding.ToolbarMenuItemAvatarBinding
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
fun Int.dpToPx(): Int {
val metrics = Resources.getSystem().displayMetrics
@@ -52,6 +62,11 @@ fun ValueAnimator.startWithIntValues(from: Int, to: Int) {
start()
}
fun ValueAnimator.startWithFloatValues(from: Float, to: Float) {
setFloatValues(from, to)
start()
}
fun View.setMarginsPx(
@Px leftMargin: Int? = null,
@Px topMargin: Int? = null,
@@ -84,4 +99,87 @@ fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE)
@JvmOverloads
fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse
}
}
fun View.showKeyboard(flags: Int = 0) {
AppGlobal.inputMethodManager.showSoftInput(this, flags)
}
fun View.hideKeyboard(focusedView: View? = null, flags: Int = 0) {
AppGlobal.inputMethodManager.hideSoftInputFromWindow(
focusedView?.windowToken ?: this.windowToken, flags
)
}
fun Toolbar.tintMenuItemIcons(@ColorInt colorToTint: Int) {
menu.forEach { item ->
item.icon?.setTint(colorToTint)
}
}
fun Toolbar.addAvatarMenuItem(urlToLoad: String? = null, drawable: Drawable? = null): MenuItem {
val avatarMenuItemBinding = ToolbarMenuItemAvatarBinding.inflate(
LayoutInflater.from(context), null, false
)
val avatarMenuItem = menu.add("Profile")
avatarMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
avatarMenuItem.actionView = avatarMenuItemBinding.root
val imageView = avatarMenuItemBinding.avatar
when {
urlToLoad != null -> {
imageView.loadWithGlide(
url = urlToLoad,
transformations = ImageLoader.userAvatarTransformations
)
}
drawable != null -> {
imageView.loadWithGlide(
drawable = drawable,
transformations = ImageLoader.userAvatarTransformations
)
}
}
return avatarMenuItem
}
fun <T> MutableLiveData<T>.notifyObservers() {
this.value = this.value
}
fun <T> MutableLiveData<T>.setIfNotEquals(item: T) {
if (this.value != item) this.value = item
}
fun <T> MutableLiveData<T>.requireValue(): T {
return this.value!!
}
val EditText.trimmedText: String get() = text.toString().trim()
val MediaType.mimeType: String get() = "${type()}/${subtype()}"
fun EditText.selectLast() {
setSelection(text.length)
}
fun <T> T?.requireNotNull(): T {
return requireNotNull(this)
}
fun String?.orDots(count: Int = 3): String {
return this ?: ("." * count)
}
private operator fun String.times(count: Int): String {
val builder = StringBuilder()
for (i in 0 until count) {
builder.append(this)
}
return builder.toString()
}
@@ -1,7 +1,6 @@
package com.meloda.fast.extensions
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
@@ -33,10 +32,13 @@ object ImageLoader {
uri: Uri? = null,
drawableRes: Int? = null,
drawable: Drawable? = null,
placeholderDrawable: Drawable = ColorDrawable(Color.TRANSPARENT),
errorDrawable: Drawable = placeholderDrawable,
placeholderDrawable: Drawable? = null,
placeholderColor: Int? = null,
errorDrawable: Drawable? = placeholderDrawable,
errorColor: Int? = null,
crossFade: Boolean = false,
crossFadeDuration: Int = 200,
crossFadeDuration: Int? = null,
asCircle: Boolean = false,
transformations: List<TypeTransformations> = emptyList(),
onLoadedAction: (() -> Unit)? = null,
onFailedAction: (() -> Unit)? = null,
@@ -53,16 +55,27 @@ object ImageLoader {
else -> request.load(null as Drawable?)
}
val transforms = transformations.toMutableList()
if (asCircle) {
transforms += TypeTransformations.CircleCrop
}
builder = builder
.apply(TypeTransformations.createRequestOptions(transformations))
.error(errorDrawable)
.placeholder(placeholderDrawable)
.apply(TypeTransformations.createRequestOptions(transforms))
.error(
errorDrawable
?: if (errorColor != null) ColorDrawable(errorColor) else null
)
.placeholder(
placeholderDrawable
?: if (placeholderColor != null) ColorDrawable(placeholderColor) else null
)
.addListener(ImageLoadRequestListener(onLoadedAction, onFailedAction))
.diskCacheStrategy(cacheStrategy)
.priority(priority)
if (crossFade) {
builder = builder.transition(withCrossFade(crossFadeDuration))
if (crossFade || crossFadeDuration != null) {
builder = builder.transition(withCrossFade(crossFadeDuration ?: 200))
}
builder.into(this)
@@ -0,0 +1,15 @@
package com.meloda.fast.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Entity(tableName = "accounts")
@Parcelize
data class AppAccount(
@PrimaryKey(autoGenerate = false)
val userId: Int,
val accessToken: String,
val fastToken: String?
) : Parcelable
@@ -1,6 +1,6 @@
package com.meloda.fast.model
sealed class DataItem<IdType> {
abstract class DataItem<IdType> {
abstract val dataItemId: IdType
object Header : DataItem<Int>() {
@@ -0,0 +1,35 @@
package com.meloda.fast.model
import android.os.Parcelable
import com.google.gson.Gson
import kotlinx.parcelize.Parcelize
@Parcelize
data class UpdateItem(
val id: Int,
val versionName: String,
val versionCode: Int,
val mandatory: Int,
val changelog: String?,
val enabled: Int,
val fileName: String,
val date: Long,
val extension: String,
val originalName: String,
val fileSize: Int,
val preRelease: Int,
val downloadLink: String
) : Parcelable {
fun isMandatory(): Boolean = mandatory == 1
fun isEnabled(): Boolean = enabled == 1
fun isPreRelease(): Boolean = preRelease == 1
override fun toString(): String {
return Gson().toJson(this)
}
}
@Parcelize
data class UpdateActualUrl(val url: String) : Parcelable

Some files were not shown because too many files have changed in this diff Show More