domain module

This commit is contained in:
2024-08-11 17:40:13 +03:00
parent 648850f1c8
commit 0500969d81
77 changed files with 282 additions and 267 deletions
+3 -3
View File
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.fast.android.library)
// alias(libs.plugins.fast.koin)
}
android {
@@ -13,9 +14,8 @@ dependencies {
api(projects.core.network)
api(projects.core.database)
implementation(libs.koin.android)
// TODO: 05/05/2024, Danil Nikolaev: research, maybe remove
// TODO: 11/08/2024, Danil Nikolaev: remove?
implementation(libs.retrofit)
implementation(libs.eithernet)
implementation(libs.koin.android)
}
@@ -0,0 +1,24 @@
package dev.meloda.fast.data
import androidx.core.net.toUri
import okhttp3.Interceptor
import okhttp3.Response
import java.net.URLEncoder
class AccessTokenInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().url.newBuilder()
val uri = builder.build().toUri().toString().toUri()
if (uri.getQueryParameter("access_token") == null) {
builder.addQueryParameter(
"access_token",
URLEncoder.encode(UserConfig.accessToken, "utf-8")
)
}
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
}
}
@@ -1,360 +0,0 @@
package dev.meloda.fast.data
import android.util.Log
import dev.meloda.fast.common.UserConfig
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.asInt
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.api.messages.MessagesUseCase
import dev.meloda.fast.model.ApiEvent
import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollEvent
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LongPollUpdatesParser(
private val messagesUseCase: MessagesUseCase
) {
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace()
}
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job + exceptionHandler
private val coroutineScope = CoroutineScope(coroutineContext)
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
mutableMapOf()
fun parseNextUpdate(event: List<Any>) {
val eventId = event.first().asInt()
val eventType: ApiEvent = try {
ApiEvent.parse(eventId)
} catch (e: Exception) {
e.printStackTrace()
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.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
ApiEvent.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event)
ApiEvent.TYPING,
ApiEvent.AUDIO_MESSAGE_RECORDING,
ApiEvent.PHOTO_UPLOADING,
ApiEvent.VIDEO_UPLOADING,
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event)
}
}
private fun onNewEvent(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
}
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val interactionType = when (eventType) {
ApiEvent.TYPING -> InteractionType.Typing
ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage
ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo
ApiEvent.VIDEO_UPLOADING -> InteractionType.Video
ApiEvent.FILE_UPLOADING -> InteractionType.File
else -> return
}
val peerId = event[1].asInt()
val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId }
val totalCount = event[3].asInt()
val timestamp = event[4].asInt()
// if userIds contains only account's id, then we don't need to show our status
if (userIds.isEmpty()) return
coroutineScope.launch {
listenersMap[eventType]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.Interaction>)
.onEvent(
LongPollEvent.Interaction(
interactionType = interactionType,
peerId = peerId,
userIds = userIds,
totalCount = totalCount,
timestamp = timestamp
)
)
}
}
}
}
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val majorId = event[2].asInt()
coroutineScope.launch {
listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
.onEvent(
LongPollEvent.VkConversationPinStateChangedEvent(
peerId = peerId,
majorId = majorId
)
)
}
}
}
}
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt()
coroutineScope.launch(Dispatchers.IO) {
val newMessageEvent: LongPollEvent.VkMessageNewEvent? =
loadNormalMessage(
eventType,
messageId
)
newMessageEvent?.let { event ->
listenersMap[ApiEvent.MESSAGE_NEW]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
.onEvent(event)
}
}
}
}
}
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt()
coroutineScope.launch {
val editedMessageEvent: LongPollEvent.VkMessageEditEvent? =
loadNormalMessage(
eventType,
messageId
)
editedMessageEvent?.let { event ->
listenersMap[ApiEvent.MESSAGE_EDIT]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
.onEvent(event)
}
}
}
}
}
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val messageId = event[2].asInt()
val unreadCount = event[3].asInt()
coroutineScope.launch {
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
.onEvent(
LongPollEvent.VkMessageReadIncomingEvent(
peerId = peerId,
messageId = messageId,
unreadCount = unreadCount
)
)
}
}
}
}
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val messageId = event[2].asInt()
val unreadCount = event[3].asInt()
coroutineScope.launch {
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
.onEvent(
LongPollEvent.VkMessageReadOutgoingEvent(
peerId = peerId,
messageId = messageId,
unreadCount = unreadCount
)
)
}
}
}
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private suspend inline fun <reified T : LongPollEvent> loadNormalMessage(
eventType: ApiEvent,
messageId: Int
): T? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) {
messagesUseCase.getById(
messageIds = listOf(messageId),
extended = true,
fields = VkConstants.ALL_FIELDS
).listenValue(this) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error")
},
success = { messages ->
val message = messages.singleOrNull() ?: run {
continuation.resume(null)
return@listenValue
}
VkMemoryCache[message.id] = message
messagesUseCase.storeMessage(message)
val resumeValue: LongPollEvent? = when (eventType) {
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
else -> {
continuation.resume(null)
null
}
}
resumeValue?.let { value -> continuation.resume(value as T) }
}
)
}
}
}
private fun <T : Any> registerListener(
eventType: ApiEvent,
listener: VkEventCallback<T>
) {
listenersMap.let { map ->
map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) }
}
}
private fun <T : Any> registerListeners(
eventTypes: List<ApiEvent>,
listener: VkEventCallback<T>
) {
eventTypes.forEach { eventType -> registerListener(eventType, listener) }
}
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener)
}
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
onConversationPinStateChanged(assembleEventCallback(block))
}
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener)
}
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
onMessageIncomingRead(assembleEventCallback(block))
}
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener)
}
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
onMessageOutgoingRead(assembleEventCallback(block))
}
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
registerListener(ApiEvent.MESSAGE_NEW, listener)
}
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
onNewMessage(assembleEventCallback(block))
}
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
registerListener(ApiEvent.MESSAGE_EDIT, listener)
}
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
onMessageEdited(assembleEventCallback(block))
}
fun onInteractions(listener: VkEventCallback<LongPollEvent.Interaction>) {
registerListeners(
eventTypes = listOf(
ApiEvent.TYPING,
ApiEvent.AUDIO_MESSAGE_RECORDING,
ApiEvent.PHOTO_UPLOADING,
ApiEvent.VIDEO_UPLOADING,
ApiEvent.FILE_UPLOADING
),
listener = listener
)
}
fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) {
onInteractions(assembleEventCallback(block))
}
fun clearListeners() {
listenersMap.clear()
}
}
internal inline fun <R : Any> assembleEventCallback(
crossinline block: (R) -> Unit,
): VkEventCallback<R> {
return VkEventCallback { event -> block.invoke(event) }
}
fun interface VkEventCallback<in T : Any> {
fun onEvent(event: T)
}
@@ -1,23 +0,0 @@
package dev.meloda.fast.data
import dev.meloda.fast.model.api.data.LongPollUpdates
import dev.meloda.fast.model.api.data.VkLongPollData
import kotlinx.coroutines.flow.Flow
interface LongPollUseCase {
fun getLongPollServer(
needPts: Boolean,
version: Int
): Flow<State<VkLongPollData>>
fun getLongPollUpdates(
serverUrl: String,
act: String = "a_check",
key: String,
ts: Int,
wait: Int,
mode: Int,
version: Int
): Flow<State<LongPollUpdates>>
}
@@ -1,49 +0,0 @@
package dev.meloda.fast.data
import dev.meloda.fast.data.api.longpoll.LongPollRepository
import dev.meloda.fast.model.api.data.LongPollUpdates
import dev.meloda.fast.model.api.data.VkLongPollData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class LongPollUseCaseImpl(
private val repository: LongPollRepository
) : LongPollUseCase {
override fun getLongPollServer(
needPts: Boolean,
version: Int
): Flow<State<VkLongPollData>> = flow {
emit(State.Loading)
val newState = repository.getLongPollServer(
needPts = needPts,
version = version
).mapToState()
emit(newState)
}
override fun getLongPollUpdates(
serverUrl: String,
act: String,
key: String,
ts: Int,
wait: Int,
mode: Int,
version: Int
): Flow<State<LongPollUpdates>> = flow {
emit(State.Loading)
val newState = repository.getLongPollUpdates(
serverUrl,
act = act,
key = key,
ts = ts,
wait = wait,
mode = mode,
version = version
).mapToState()
emit(newState)
}
}
@@ -0,0 +1,31 @@
package dev.meloda.fast.data
import dev.meloda.fast.datastore.AppSettings
object UserConfig {
private const val ARG_CURRENT_USER_ID = "current_user_id"
var currentUserId: Int = -1
get() = AppSettings.getInt(ARG_CURRENT_USER_ID, -1)
set(value) {
field = value
AppSettings.edit { putInt(ARG_CURRENT_USER_ID, value) }
}
var userId: Int = -1
var accessToken: String = ""
var fastToken: String? = ""
var trustedHash: String? = null
fun clear() {
currentUserId = -1
accessToken = ""
fastToken = ""
userId = -1
}
fun isLoggedIn(): Boolean {
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
}
}
@@ -1,16 +0,0 @@
package dev.meloda.fast.data.api.account
import dev.meloda.fast.data.State
import kotlinx.coroutines.flow.Flow
interface AccountUseCase {
suspend fun setOnline(
voip: Boolean,
accessToken: String
): Flow<State<Int>>
suspend fun setOffline(
accessToken: String
): Flow<State<Int>>
}
@@ -1,30 +0,0 @@
package dev.meloda.fast.data.api.account
import dev.meloda.fast.data.State
import dev.meloda.fast.data.mapToState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class AccountUseCaseImpl(
private val repository: AccountRepository
) : AccountUseCase {
override suspend fun setOnline(
voip: Boolean,
accessToken: String
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.setOnline(voip = voip).mapToState()
emit(newState)
}
override suspend fun setOffline(
accessToken: String
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.setOffline().mapToState()
emit(newState)
}
}
@@ -1,19 +0,0 @@
package dev.meloda.fast.data.api.conversations
import dev.meloda.fast.data.State
import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow
interface ConversationsUseCase {
fun getConversations(
count: Int?,
offset: Int?,
): Flow<State<List<VkConversation>>>
fun delete(peerId: Int): Flow<State<Int>>
fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>>
suspend fun storeConversations(conversations: List<VkConversation>)
}
@@ -1,26 +0,0 @@
package dev.meloda.fast.data.api.friends
import dev.meloda.fast.data.State
import dev.meloda.fast.model.FriendsInfo
import dev.meloda.fast.model.api.domain.VkUser
import kotlinx.coroutines.flow.Flow
interface FriendsUseCase {
fun getAllFriends(
count: Int?,
offset: Int?
): Flow<State<FriendsInfo>>
fun getFriends(
count: Int?,
offset: Int?
): Flow<State<List<VkUser>>>
fun getOnlineFriends(
count: Int?,
offset: Int?
): Flow<State<List<Int>>>
suspend fun storeUsers(users: List<VkUser>)
}
@@ -1,46 +0,0 @@
package dev.meloda.fast.data.api.messages
import dev.meloda.fast.data.State
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage
import kotlinx.coroutines.flow.Flow
interface MessagesUseCase {
fun getMessagesHistory(
conversationId: Int,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>>
fun getById(
messageIds: List<Int>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>>
fun sendMessage(
peerId: Int,
randomId: Int,
message: String?,
replyTo: Int?,
attachments: List<VkAttachment>?
): Flow<State<Int>>
fun markAsRead(
peerId: Int,
startMessageId: Int
): Flow<State<Int>>
fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): Flow<State<List<VkAttachmentHistoryMessage>>>
suspend fun storeMessage(message: VkMessage)
suspend fun storeMessages(messages: List<VkMessage>)
}
@@ -1,19 +0,0 @@
package dev.meloda.fast.data.api.users
import dev.meloda.fast.data.State
import dev.meloda.fast.model.api.domain.VkUser
import kotlinx.coroutines.flow.Flow
interface UsersUseCase {
fun get(
userIds: List<Int>?,
fields: String?,
nomCase: String?
): Flow<State<List<VkUser>>>
fun getLocalUser(userId: Int): Flow<State<VkUser?>>
suspend fun storeUser(user: VkUser)
suspend fun storeUsers(users: List<VkUser>)
}
@@ -1,39 +0,0 @@
package dev.meloda.fast.data.api.users
import dev.meloda.fast.data.State
import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkUser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class UsersUseCaseImpl(
private val repository: UsersRepository,
) : UsersUseCase {
override fun get(
userIds: List<Int>?,
fields: String?,
nomCase: String?
): Flow<State<List<VkUser>>> = flow {
emit(State.Loading)
val newState = repository.get(userIds, fields, nomCase).mapToState()
emit(newState)
}
override fun getLocalUser(userId: Int): Flow<State<VkUser?>> = flow {
emit(State.Loading)
val newState = kotlin.runCatching {
repository.getLocalUsers(listOf(userId)).singleOrNull()
}.fold(
onSuccess = { user -> State.Success(user) },
onFailure = { State.Error.InternalError }
)
emit(newState)
}
override suspend fun storeUser(user: VkUser) = repository.storeUsers(listOf(user))
override suspend fun storeUsers(users: List<VkUser>) = repository.storeUsers(users)
}
@@ -1,13 +0,0 @@
package dev.meloda.fast.data.db
import dev.meloda.fast.common.UserConfig
import dev.meloda.fast.model.database.AccountEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class GetCurrentAccountUseCase(private val accountsRepository: AccountsRepository) {
suspend operator fun invoke(): AccountEntity? = withContext(Dispatchers.IO) {
accountsRepository.getAccountById(UserConfig.currentUserId)
}
}
@@ -1,10 +1,8 @@
package dev.meloda.fast.data.di
import dev.meloda.fast.common.di.commonModule
import dev.meloda.fast.data.AccessTokenInterceptor
import dev.meloda.fast.data.api.account.AccountRepository
import dev.meloda.fast.data.api.account.AccountRepositoryImpl
import dev.meloda.fast.data.api.account.AccountUseCase
import dev.meloda.fast.data.api.account.AccountUseCaseImpl
import dev.meloda.fast.data.api.audios.AudiosRepository
import dev.meloda.fast.data.api.auth.AuthRepository
import dev.meloda.fast.data.api.auth.AuthRepositoryImpl
@@ -22,29 +20,26 @@ import dev.meloda.fast.data.api.oauth.OAuthRepositoryImpl
import dev.meloda.fast.data.api.photos.PhotosRepository
import dev.meloda.fast.data.api.users.UsersRepository
import dev.meloda.fast.data.api.users.UsersRepositoryImpl
import dev.meloda.fast.data.api.users.UsersUseCase
import dev.meloda.fast.data.api.users.UsersUseCaseImpl
import dev.meloda.fast.data.api.videos.VideosRepository
import dev.meloda.fast.data.db.AccountsRepository
import dev.meloda.fast.data.db.AccountsRepositoryImpl
import dev.meloda.fast.data.db.GetCurrentAccountUseCase
import dev.meloda.fast.database.di.databaseModule
import dev.meloda.fast.datastore.di.dataStoreModule
import dev.meloda.fast.network.di.networkModule
import okhttp3.Interceptor
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
val dataModule = module {
includes(
commonModule,
databaseModule,
dataStoreModule,
networkModule,
)
singleOf(::AccountRepositoryImpl) bind AccountRepository::class
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
singleOf(::AudiosRepository)
@@ -63,12 +58,15 @@ val dataModule = module {
singleOf(::PhotosRepository)
singleOf(::UsersRepositoryImpl) bind UsersRepository::class
singleOf(::UsersUseCaseImpl) bind UsersUseCase::class
singleOf(::VideosRepository)
singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class
singleOf(::GetCurrentAccountUseCase)
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
// TODO: 11/08/2024, Danil Nikolaev: find a better solution
single<Interceptor>(named("token_interceptor")) {
AccessTokenInterceptor()
}
}