@@ -1,10 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.meloda.fast">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name=".common.AppGlobal"
|
||||
@@ -16,11 +24,15 @@
|
||||
android:supportsRtl="true"
|
||||
android:testOnly="false"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:replace="android:allowBackup">
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:allowBackup"
|
||||
tools:ignore="DataExtractionRules">
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:name=".screens.main.MainActivity"
|
||||
android:theme="@style/AppTheme.Splash"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -29,7 +41,12 @@
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.MessagesUpdateService"
|
||||
android:name=".service.LongPollService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".service.OnlineService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
|
||||
@@ -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
-1
@@ -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
|
||||
+63
-66
@@ -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>
|
||||
|
||||
}
|
||||
-22
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+7
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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
-3
@@ -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
-1
@@ -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
|
||||
) {
|
||||
}
|
||||
+4
-4
@@ -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>
|
||||
|
||||
}
|
||||
+19
-12
@@ -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
-1
@@ -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)
|
||||
|
||||
}
|
||||
+5
-4
@@ -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
-1
@@ -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>() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user