2 Commits

41 changed files with 1219 additions and 409 deletions
+1
View File
@@ -92,6 +92,7 @@ dependencies {
implementation(projects.feature.photoviewer) implementation(projects.feature.photoviewer)
implementation(projects.feature.createchat) implementation(projects.feature.createchat)
implementation(projects.core.logger)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.data) implementation(projects.core.data)
@@ -2,7 +2,6 @@ package dev.meloda.fast
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
@@ -23,6 +22,7 @@ import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.Main
@@ -67,7 +67,8 @@ class MainViewModelImpl(
private val getCurrentAccountUseCase: GetCurrentAccountUseCase, private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val loadUserByIdUseCase: LoadUserByIdUseCase, private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val userSettings: UserSettings, private val userSettings: UserSettings,
private val longPollController: LongPollController private val longPollController: LongPollController,
private val logger: FastLogger
) : MainViewModel, ViewModel() { ) : MainViewModel, ViewModel() {
override val startDestination = MutableStateFlow<Any?>(null) override val startDestination = MutableStateFlow<Any?>(null)
@@ -203,7 +204,10 @@ class MainViewModelImpl(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val currentAccount = getCurrentAccountUseCase() val currentAccount = getCurrentAccountUseCase()
Log.d("MainViewModel", "currentAccount: $currentAccount") logger.debug(
this@MainViewModelImpl::class,
"loadAccounts(): currentAccount: $currentAccount"
)
listenLongPollState() listenLongPollState()
@@ -10,6 +10,8 @@ import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
import dev.meloda.fast.auth.BuildConfig import dev.meloda.fast.auth.BuildConfig
import dev.meloda.fast.common.di.applicationModule import dev.meloda.fast.common.di.applicationModule
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.logger.FastLogLevel
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.presentation.CrashActivity import dev.meloda.fast.presentation.CrashActivity
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@@ -30,6 +32,14 @@ class AppGlobal : Application(), ImageLoaderFactory {
initKoin() initKoin()
initCrashHandler() initCrashHandler()
val logLevel =
if (BuildConfig.DEBUG) FastLogLevel.DEBUG
else FastLogLevel.ERROR
get<FastLogger>()
.apply { setLogLevel(logLevel) }
.also { FastLogger.setInstance(it) }
} }
override fun newImageLoader(): ImageLoader = get() override fun newImageLoader(): ImageLoader = get()
@@ -19,6 +19,7 @@ import dev.meloda.fast.convos.di.createChatModule
import dev.meloda.fast.domain.di.domainModule import dev.meloda.fast.domain.di.domainModule
import dev.meloda.fast.friends.di.friendsModule import dev.meloda.fast.friends.di.friendsModule
import dev.meloda.fast.languagepicker.di.languagePickerModule import dev.meloda.fast.languagepicker.di.languagePickerModule
import dev.meloda.fast.logger.loggerModule
import dev.meloda.fast.messageshistory.di.messagesHistoryModule import dev.meloda.fast.messageshistory.di.messagesHistoryModule
import dev.meloda.fast.photoviewer.di.photoViewModule import dev.meloda.fast.photoviewer.di.photoViewModule
import dev.meloda.fast.profile.di.profileModule import dev.meloda.fast.profile.di.profileModule
@@ -49,6 +50,8 @@ val applicationModule = module {
createChatModule createChatModule
) )
includes(loggerModule)
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class // TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
singleOf(PreferenceManager::getDefaultSharedPreferences) singleOf(PreferenceManager::getDefaultSharedPreferences)
single<Resources> { androidContext().resources } single<Resources> { androidContext().resources }
@@ -9,12 +9,11 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -24,10 +23,13 @@ import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.service.OnlineService import dev.meloda.fast.service.OnlineService
import dev.meloda.fast.service.longpolling.LongPollingService import dev.meloda.fast.service.longpolling.LongPollingService
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalLogger
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -64,27 +66,28 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermissions() requestNotificationPermissions()
setContent { setContent {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val logger: FastLogger = koinInject()
LaunchedEffect(viewModel) {
Log.d("VM_CREATE", "onCreate: viewModel: $viewModel")
}
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LifecycleResumeEffect(true) { LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent) viewModel.onAppResumed(intent)
onPauseOrDispose {} onPauseOrDispose {}
} }
CompositionLocalProvider(LocalLogger provides logger) {
RootScreen( RootScreen(
toggleLongPollService = { enable, inBackground -> toggleLongPollService = { enable, inBackground ->
toggleLongPollService( toggleLongPollService(
enable = enable, enable = enable,
inBackground = inBackground ?: AppSettings.Experimental.longPollInBackground inBackground = inBackground
?: AppSettings.Experimental.longPollInBackground
) )
}, },
toggleOnlineService = ::toggleOnlineService toggleOnlineService = ::toggleOnlineService
) )
} }
} }
}
private fun createNotificationChannels() { private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -4,7 +4,6 @@ import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -63,6 +62,7 @@ import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog
import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.navigateToSettings
import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalLogger
import dev.meloda.fast.ui.common.LocalSizeConfig import dev.meloda.fast.ui.common.LocalSizeConfig
import dev.meloda.fast.ui.model.DeviceSize import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig import dev.meloda.fast.ui.model.SizeConfig
@@ -83,6 +83,7 @@ fun RootScreen(
toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit, toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit,
toggleOnlineService: (enable: Boolean) -> Unit toggleOnlineService: (enable: Boolean) -> Unit
) { ) {
val logger = LocalLogger.current
val resources = LocalResources.current val resources = LocalResources.current
val userSettings: UserSettings = koinInject() val userSettings: UserSettings = koinInject()
@@ -92,10 +93,6 @@ fun RootScreen(
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle() val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LaunchedEffect(viewModel) {
Log.d("VM_CREATE", "RootScreen(): viewModel: $viewModel")
}
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
val permissionState = val permissionState =
@@ -126,13 +123,12 @@ fun RootScreen(
} }
LifecycleResumeEffect(longPollStateToApply) { LifecycleResumeEffect(longPollStateToApply) {
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply") logger.debug("RootScreen", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) { if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched() if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply && longPollCurrentState != longPollStateToApply
) { ) {
toggleLongPollService(false, null) toggleLongPollService(false, null)
Log.d("LongPoll", "recreate()")
} }
toggleLongPollService( toggleLongPollService(
@@ -6,8 +6,9 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import dev.meloda.fast.common.extensions.createTimerFlow import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.domain.AccountUseCase
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.domain.AccountUseCase
import dev.meloda.fast.logger.FastLogger
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -24,11 +25,12 @@ import kotlin.time.Duration.Companion.minutes
class OnlineService : Service() { class OnlineService : Service() {
private val logger: FastLogger by inject()
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "error: $throwable") logger.error(this::class.java, "CoroutineException", throwable)
throwable.printStackTrace()
} }
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
@@ -42,17 +44,20 @@ class OnlineService : Service() {
private var onlineJob: Job? = null private var onlineJob: Job? = null
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
Log.d(STATE_TAG, "onBind: intent: $intent") logger.debug(this::class, "STATE: onBind(): intent: $intent")
return null return null
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY if (startId > 1) return START_STICKY
Log.d(STATE_TAG, "onStartCommand: flags: $flags; startId: $startId\ninstance: $this") logger.debug(
this::class,
"STATE: onStartCommand(): flags: %s; startId: %s;\ninstance: %s"
.format("$flags", "$startId", "$this")
)
createTimer() createTimer()
return START_STICKY return START_STICKY
} }
@@ -68,13 +73,13 @@ class OnlineService : Service() {
private fun setOnline() { private fun setOnline() {
if (onlineJob != null) return if (onlineJob != null) return
Log.d(TAG, "setOnline()") logger.debug(this::class, "setOnline()")
onlineJob = coroutineScope.launch { onlineJob = coroutineScope.launch {
val token = UserConfig.fastToken ?: UserConfig.accessToken val token = UserConfig.fastToken ?: UserConfig.accessToken
if (token.isBlank()) { if (token.isBlank()) {
Log.d(TAG, "setOnline: token is empty") logger.debug(this::class, "setOnline(): token is empty")
return@launch return@launch
} }
@@ -84,10 +89,10 @@ class OnlineService : Service() {
).onEach { state -> ).onEach { state ->
state.processState( state.processState(
error = { error -> error = { error ->
Log.w(TAG, "setOnline(): error: $error") logger.error(this@OnlineService::class, "setOnline(): ERROR: $error")
}, },
success = { response -> success = { response ->
Log.d(TAG, "setOnline(): success: $response") logger.debug(this@OnlineService::class, "setOnline(): response: $response")
} }
) )
}.collect() }.collect()
@@ -96,7 +101,7 @@ class OnlineService : Service() {
} }
override fun onDestroy() { override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy") logger.debug(this::class, "onDestroy()")
timerJob?.cancel("OnlineService destroyed") timerJob?.cancel("OnlineService destroyed")
onlineJob?.cancel("OnlineService destroyed") onlineJob?.cancel("OnlineService destroyed")
@@ -7,7 +7,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import com.conena.nanokt.android.app.stopForegroundCompat import com.conena.nanokt.android.app.stopForegroundCompat
@@ -19,8 +18,10 @@ import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.LongPollEventsHandler
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.domain.LongPollUseCase
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.api.data.LongPollUpdates import dev.meloda.fast.model.api.data.LongPollUpdates
import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.model.api.data.VkLongPollData
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
@@ -40,6 +41,8 @@ import kotlin.time.Duration.Companion.seconds
class LongPollingService : Service() { class LongPollingService : Service() {
private val logger: FastLogger by inject()
private val longPollController: LongPollController by inject() private val longPollController: LongPollController by inject()
private val job = SupervisorJob() private val job = SupervisorJob()
@@ -56,6 +59,7 @@ class LongPollingService : Service() {
private val longPollUseCase: LongPollUseCase by inject() private val longPollUseCase: LongPollUseCase by inject()
private val updatesParser: LongPollUpdatesParser by inject() private val updatesParser: LongPollUpdatesParser by inject()
private val eventsHandler: LongPollEventsHandler by inject()
private var currentJob: Job? = null private var currentJob: Job? = null
@@ -63,20 +67,21 @@ class LongPollingService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(STATE_TAG, "onCreate()") logger.debug(this::class, "STATE: onCreate()")
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
Log.d(STATE_TAG, "onBind: intent: $intent") logger.debug(this::class, "STATE: onBind(): intent: $intent")
return null return null
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY if (startId > 1) return START_STICKY
Log.d( logger.debug(
STATE_TAG, this::class,
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this" "STATE: onStartCommand(): asForeground: %s; flags: %s; startId: %s;\ninstance: %s"
.format("$inBackground", "$flags", "$startId", "$this")
) )
startJob() startJob()
@@ -131,11 +136,15 @@ class LongPollingService : Service() {
private fun startPolling(): Job { private fun startPolling(): Job {
if (job.isCompleted || job.isCancelled) { if (job.isCompleted || job.isCancelled) {
Log.d(STATE_TAG, "Job is completed or cancelled") logger.debug(
this::class,
"startPolling(): Job is already done. isCompleted: %s; isCancelled: %s"
.format("${job.isCompleted}", "${job.isCancelled}")
)
throw Exception("Job is over") throw Exception("Job is over")
} }
Log.d(STATE_TAG, "Starting job...") logger.debug(this::class, "startPolling(): Starting job.")
return coroutineScope.launch(coroutineContext) { return coroutineScope.launch(coroutineContext) {
longPollController.updateCurrentState( longPollController.updateCurrentState(
@@ -193,7 +202,7 @@ class LongPollingService : Service() {
if (updates == null) { if (updates == null) {
failCount++ failCount++
} else { } else {
updates.forEach(updatesParser::parseNextUpdate) parseUpdates(updates)
} }
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
@@ -211,11 +220,11 @@ class LongPollingService : Service() {
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
success = { response -> success = { response ->
Log.d(TAG, "getServerInfo: serverInfoResponse: $response") logger.debug(this::class, "getServerInfo(): response: $response")
it.resume(response) it.resume(response)
}, },
error = { error -> error = { error ->
Log.e(TAG, "getServerInfo: $error") logger.error(this::class, "getServerInfo(): ERROR: $error")
it.resume(null) it.resume(null)
} }
) )
@@ -235,19 +244,24 @@ class LongPollingService : Service() {
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
success = { response -> success = { response ->
Log.d(TAG, "lastUpdateResponse: $response") logger.debug(this::class, "getUpdatesResponse(): response: $response")
it.resume(response) it.resume(response)
}, },
error = { error -> error = { error ->
Log.d(TAG, "getUpdatesResponse: error: $error") logger.debug(this::class, "getUpdatesResponse(): error: $error")
it.resume(null) it.resume(null)
} }
) )
} }
} }
private suspend fun parseUpdates(updates: List<List<Any>>) {
val parsedUpdates = updates.flatMap { updatesParser.parseNextUpdate(it) }
eventsHandler.handleEvents(parsedUpdates)
}
private fun handleError(throwable: Throwable) { private fun handleError(throwable: Throwable) {
Log.e(TAG, "error: $throwable") logger.error(this::class, "CoroutineException", throwable)
if (throwable !is NoAccessTokenException) { if (throwable !is NoAccessTokenException) {
throwable.printStackTrace() throwable.printStackTrace()
@@ -262,7 +276,7 @@ class LongPollingService : Service() {
} }
override fun onDestroy() { override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy") logger.debug(this::class, "STATE: onDestroy()")
longPollController.updateCurrentState(LongPollState.Stopped) longPollController.updateCurrentState(LongPollState.Stopped)
try { try {
AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) } AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) }
@@ -274,7 +288,7 @@ class LongPollingService : Service() {
} }
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
Log.d(STATE_TAG, "onTrimMemory. Level: $level") logger.debug(this::class, "STATE: onTrimMemory(): Level: $level")
super.onTrimMemory(level) super.onTrimMemory(level)
} }
@@ -1,5 +1,6 @@
package dev.meloda.fast.service.longpolling.di package dev.meloda.fast.service.longpolling.di
import dev.meloda.fast.domain.LongPollEventsHandler
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.domain.LongPollUseCase
import dev.meloda.fast.domain.LongPollUseCaseImpl import dev.meloda.fast.domain.LongPollUseCaseImpl
@@ -10,4 +11,5 @@ import org.koin.dsl.module
val longPollModule = module { val longPollModule = module {
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
singleOf(::LongPollUpdatesParser) singleOf(::LongPollUpdatesParser)
singleOf(::LongPollEventsHandler)
} }
@@ -1,13 +1,13 @@
package dev.meloda.fast.common.model package dev.meloda.fast.common.model
enum class LogLevel(val value: Int) { enum class NetworkLogLevel(val value: Int) {
NONE(0), NONE(0),
BASIC(1), BASIC(1),
HEADERS(2), HEADERS(2),
BODY(3); BODY(3);
companion object { companion object {
fun parse(value: Int): LogLevel = entries.firstOrNull { it.value == value } fun parse(value: Int): NetworkLogLevel = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown log level with value: $value") ?: throw IllegalArgumentException("Unknown log level with value: $value")
} }
} }
@@ -0,0 +1,425 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "5eca3b3da167aaf7e772977a1f4e56e2",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isOnline",
"columnName": "isOnline",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnlineMobile",
"columnName": "isOnlineMobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT"
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `isImportant` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `isSpam` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cmId",
"columnName": "cmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT"
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT"
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT"
},
{
"fieldPath": "actionCmId",
"columnName": "actionCmId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT"
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER"
},
{
"fieldPath": "isImportant",
"columnName": "isImportant",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT"
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT"
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT"
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDeleted",
"columnName": "isDeleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSpam",
"columnName": "isSpam",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "convos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCmId",
"columnName": "lastCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReadCmId",
"columnName": "inReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outReadCmId",
"columnName": "outReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canChangeInfo",
"columnName": "canChangeInfo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5eca3b3da167aaf7e772977a1f4e56e2')"
]
}
}
@@ -21,7 +21,7 @@ import dev.meloda.fast.model.database.VkUserEntity
VkConvoEntity::class VkConvoEntity::class
], ],
version = 11 version = 12
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
@@ -13,7 +13,7 @@ abstract class ConvoDao : EntityDao<VkConvoEntity> {
abstract suspend fun getAll(): List<VkConvoEntity> abstract suspend fun getAll(): List<VkConvoEntity>
@Query("SELECT * FROM convos WHERE id IN (:ids)") @Query("SELECT * FROM convos WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConvoEntity> abstract suspend fun getAllByIds(ids: List<Long>): List<VkConvoEntity>
@Query("SELECT * FROM convos WHERE id IS (:id)") @Query("SELECT * FROM convos WHERE id IS (:id)")
abstract suspend fun getById(id: Long): VkConvoEntity? abstract suspend fun getById(id: Long): VkConvoEntity?
@@ -23,8 +23,23 @@ abstract class ConvoDao : EntityDao<VkConvoEntity> {
abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage? abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage?
@Query("DELETE FROM convos WHERE rowid IN (:ids)") @Query("DELETE FROM convos WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Long>): Int
@Query("UPDATE convos SET inReadCmId = :cmId, unreadCount = :unreadCount WHERE id = :convoId")
abstract suspend fun updateReadIncoming(convoId: Long, cmId: Long, unreadCount: Int): Int
@Query("UPDATE convos SET outReadCmId = :cmId, unreadCount = :unreadCount WHERE id = :convoId")
abstract suspend fun updateReadOutgoing(convoId: Long, cmId: Long, unreadCount: Int): Int
@Query("UPDATE convos SET isArchived = :isArchived WHERE id = :convoId")
abstract suspend fun updateIsArchived(convoId: Long, isArchived: Boolean): Int
@Query("UPDATE convos SET majorId = :majorId WHERE id = :convoId")
abstract suspend fun updateMajorId(convoId: Long, majorId: Int): Int
@Query("UPDATE convos SET minorId = :minorId WHERE id = :convoId")
abstract suspend fun updateMinorId(convoId: Long, minorId: Int): Int
@Query("UPDATE convos SET lastCmId = :cmId WHERE id = :convoId")
abstract suspend fun updateLastCmId(convoId: Long, cmId: Long): Int
} }
@@ -7,7 +7,7 @@ import dev.meloda.fast.model.database.VkMessageEntity
@Dao @Dao
abstract class MessageDao : EntityDao<VkMessageEntity> { abstract class MessageDao : EntityDao<VkMessageEntity> {
@Query("SELECT * FROM messages") @Query("SELECT * FROM messages WHERE isDeleted = 0 AND isSpam = 0")
abstract suspend fun getAll(): List<VkMessageEntity> abstract suspend fun getAll(): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:convoId)") @Query("SELECT * FROM messages WHERE peerId IS (:convoId)")
@@ -21,4 +21,13 @@ abstract class MessageDao : EntityDao<VkMessageEntity> {
@Query("DELETE FROM messages WHERE id IN (:ids)") @Query("DELETE FROM messages WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Int>): Int
@Query("UPDATE messages SET isDeleted = :isDeleted WHERE peerId = :convoId AND cmId = :cmId")
abstract suspend fun markAsDeleted(convoId: Long, cmId: Long, isDeleted: Boolean): Int
@Query("UPDATE messages SET isImportant = :isImportant WHERE peerId = :convoId AND cmId = :cmId")
abstract suspend fun markAsImportant(convoId: Long, cmId: Long, isImportant: Boolean): Int
@Query("UPDATE messages SET isSpam = :isSpam WHERE peerId = :convoId AND cmId = :cmId")
abstract suspend fun markAsSpam(convoId: Long, cmId: Long, isSpam: Boolean): Int
} }
@@ -3,7 +3,7 @@ package dev.meloda.fast.datastore
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.LogLevel import dev.meloda.fast.common.model.NetworkLogLevel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -238,11 +238,11 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value) set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value)
var networkLogLevel: LogLevel var networkLogLevel: NetworkLogLevel
get() = get( get() = get(
SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL,
SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL
).let(LogLevel::parse) ).let(NetworkLogLevel::parse)
set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value) set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value)
var showDebugCategory: Boolean var showDebugCategory: Boolean
@@ -0,0 +1,188 @@
package dev.meloda.fast.domain
import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.LongPollParsedEvent
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlin.coroutines.CoroutineContext
class LongPollEventsHandler(
private val logger: FastLogger,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase,
private val convoDao: ConvoDao,
private val messageDao: MessageDao,
) {
private val job = SupervisorJob()
private val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
logger.error(this::class, "CoroutineException", throwable)
}
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job + exceptionHandler
private val coroutineScope = CoroutineScope(coroutineContext)
suspend fun handleEvents(events: List<LongPollParsedEvent>) {
events.forEach { handleNextEvent(it) }
}
private suspend fun handleNextEvent(event: LongPollParsedEvent) {
when (event) {
is LongPollParsedEvent.AudioMessageListened -> {
}
is LongPollParsedEvent.ChatArchived -> {
val affectedRows = convoDao.updateIsArchived(
convoId = event.convo.id,
isArchived = event.convo.isArchived
)
logger.debug(
this::class,
"isArchived ${event.convo.isArchived}: updated $affectedRows rows."
)
}
is LongPollParsedEvent.ChatCleared -> {
val affectedRows = convoDao.updateLastCmId(
convoId = event.peerId,
cmId = event.toCmId
)
logger.debug(
this::class,
"updateLastCmId: updated $affectedRows rows."
)
}
is LongPollParsedEvent.ChatMajorChanged -> {
val affectedRows = convoDao.updateMajorId(
convoId = event.peerId,
majorId = event.majorId
)
logger.debug(
this::class,
"updateMajorId: updated $affectedRows rows."
)
}
is LongPollParsedEvent.ChatMinorChanged -> {
val affectedRows = convoDao.updateMinorId(
convoId = event.peerId,
minorId = event.minorId
)
logger.debug(
this::class,
"updateMinorId: updated $affectedRows rows."
)
}
is LongPollParsedEvent.Interaction -> {
}
is LongPollParsedEvent.MessageCacheClear -> {
messagesUseCase.storeMessage(event.message)
}
is LongPollParsedEvent.MessageDeleted -> {
val affectedRows = messageDao.markAsDeleted(
convoId = event.peerId,
cmId = event.cmId,
isDeleted = true
)
logger.debug(
this::class,
"markDeleted: updated $affectedRows rows."
)
}
is LongPollParsedEvent.MessageEdited -> {
messagesUseCase.storeMessage(event.message)
}
is LongPollParsedEvent.MessageMarkedAsImportant -> {
val affectedRows = messageDao.markAsImportant(
convoId = event.peerId,
cmId = event.cmId,
isImportant = event.marked
)
logger.debug(
this::class,
"markImportant: updated $affectedRows rows."
)
}
is LongPollParsedEvent.MessageMarkedAsNotSpam -> {
messagesUseCase.storeMessage(event.message)
}
is LongPollParsedEvent.MessageMarkedAsSpam -> {
val affectedRows = messageDao.markAsSpam(
convoId = event.peerId,
cmId = event.cmId,
isSpam = true
)
logger.debug(
this::class,
"markSpam: updated $affectedRows rows."
)
}
is LongPollParsedEvent.MessageRestored -> {
messagesUseCase.storeMessage(event.message)
}
is LongPollParsedEvent.MessageUpdated -> {
messagesUseCase.storeMessage(event.message)
}
is LongPollParsedEvent.NewMessage -> {
messagesUseCase.storeMessage(event.message)
}
is LongPollParsedEvent.IncomingMessageRead -> {
val affectedRows = convoDao.updateReadIncoming(
convoId = event.peerId,
cmId = event.cmId,
unreadCount = event.unreadCount
)
logger.debug(
this::class,
"inMessageRead: updated $affectedRows rows."
)
}
is LongPollParsedEvent.OutgoingMessageRead -> {
val affectedRows = convoDao.updateReadOutgoing(
convoId = event.peerId,
cmId = event.cmId,
unreadCount = event.unreadCount
)
logger.debug(
this::class,
"outMessageRead: updated $affectedRows rows."
)
}
is LongPollParsedEvent.UnreadCounter -> {
}
}
}
}
@@ -1,6 +1,5 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import android.util.Log
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.asInt import dev.meloda.fast.common.extensions.asInt
import dev.meloda.fast.common.extensions.asLong import dev.meloda.fast.common.extensions.asLong
@@ -8,6 +7,7 @@ import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.toList import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.ApiEvent import dev.meloda.fast.model.ApiEvent
import dev.meloda.fast.model.ConvoFlags import dev.meloda.fast.model.ConvoFlags
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
@@ -22,12 +22,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LongPollUpdatesParser( class LongPollUpdatesParser(
private val logger: FastLogger,
private val convoUseCase: ConvoUseCase, private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase private val messagesUseCase: MessagesUseCase
) { ) {
@@ -35,8 +35,7 @@ class LongPollUpdatesParser(
private val exceptionHandler = private val exceptionHandler =
CoroutineExceptionHandler { _, throwable -> CoroutineExceptionHandler { _, throwable ->
Log.e("LongPollUpdatesParser", "error: $throwable") logger.error(this::class, "CoroutineException", throwable)
throwable.printStackTrace()
} }
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
@@ -47,11 +46,14 @@ class LongPollUpdatesParser(
private val listenersMap: MutableMap<LongPollEvent, MutableList<VkEventCallback<LongPollParsedEvent>>> = private val listenersMap: MutableMap<LongPollEvent, MutableList<VkEventCallback<LongPollParsedEvent>>> =
mutableMapOf() mutableMapOf()
fun parseNextUpdate(event: List<Any>) { suspend fun parseNextUpdate(event: List<Any>): List<LongPollParsedEvent> {
val eventId = event.first().asInt() val eventId = event.first().asInt()
when (val eventType = ApiEvent.parseOrNull(eventId)) { return when (val eventType = ApiEvent.parseOrNull(eventId)) {
null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") null -> {
logger.debug(this::class, "parseNextUpdate(): unknownEvent: $event")
emptyList()
}
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
@@ -77,8 +79,11 @@ class LongPollUpdatesParser(
} }
} }
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) { private fun parseMessageSetFlags(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseMessageSetFlags(): $eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
@@ -97,12 +102,8 @@ class LongPollUpdatesParser(
) )
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]
listeners.map { vkEventCallback -> ?.forEach { it.onEvent(eventToSend) }
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
} }
MessageFlags.SPAM -> { MessageFlags.SPAM -> {
@@ -111,13 +112,7 @@ class LongPollUpdatesParser(
cmId = cmId cmId = cmId
) )
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_SPAM]?.forEach { it.onEvent(eventToSend) }
listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsSpam>)
?.onEvent(eventToSend)
}
}
} }
MessageFlags.DELETED -> { MessageFlags.DELETED -> {
@@ -136,13 +131,7 @@ class LongPollUpdatesParser(
) )
} }
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_DELETED]?.forEach { it.onEvent(eventToSend) }
listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageDeleted>)
?.onEvent(eventToSend)
}
}
} }
MessageFlags.AUDIO_LISTENED -> { MessageFlags.AUDIO_LISTENED -> {
@@ -152,29 +141,37 @@ class LongPollUpdatesParser(
) )
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners -> listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]
listeners.map { vkEventCallback -> ?.forEach { it.onEvent(eventToSend) }
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.AudioMessageListened>)
?.onEvent(eventToSend)
}
}
} }
else -> Unit MessageFlags.UNREAD -> Unit
MessageFlags.OUTGOING -> Unit
MessageFlags.FROM_GROUP_CHAT -> Unit
MessageFlags.CANCEL_SPAM -> Unit
MessageFlags.DELETED_FOR_ALL -> Unit
MessageFlags.DO_NOT_SHOW_NOTIFICATION -> Unit
MessageFlags.MESSAGE_WITH_REPLY -> Unit
MessageFlags.REACTION -> Unit
} }
} }
eventsToSend.forEach { eventToSend -> eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners -> listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback -> listeners.forEach { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(eventToSend) vkEventCallback.onEvent(eventToSend)
}
} }
} }
} }
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) { return eventsToSend
Log.d("LongPollUpdatesParser", "$eventType: $event") }
private suspend fun parseMessageClearFlags(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
logger.debug(this::class, "parseMessageClearFlags(): $eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
@@ -184,7 +181,9 @@ class LongPollUpdatesParser(
val parsedFlags = MessageFlags.parse(flags) val parsedFlags = MessageFlags.parse(flags)
coroutineScope.launch { coroutineScope.launch(Dispatchers.IO) {
val message = loadMessage(peerId = peerId, cmId = cmId)
parsedFlags.forEach { flag -> parsedFlags.forEach { flag ->
when (flag) { when (flag) {
MessageFlags.IMPORTANT -> { MessageFlags.IMPORTANT -> {
@@ -195,81 +194,65 @@ class LongPollUpdatesParser(
) )
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]
listeners.map { vkEventCallback -> ?.forEach { it.onEvent(eventToSend) }
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
} }
MessageFlags.SPAM -> { MessageFlags.SPAM -> {
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) { if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) {
withContext(Dispatchers.IO) { if (message != null) {
val message = loadMessage(
peerId = peerId,
cmId = cmId
)
message?.let {
val eventToSend = val eventToSend =
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message) LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners -> listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]
listeners.map { vkEventCallback -> ?.forEach { it.onEvent(eventToSend) }
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsNotSpam>)
?.onEvent(eventToSend)
}
}
}
} }
} }
} }
MessageFlags.DELETED -> { MessageFlags.DELETED -> {
withContext(Dispatchers.IO) { if (message != null) {
val message = loadMessage(
peerId = peerId,
cmId = cmId
)
message?.let {
val eventToSend = val eventToSend =
LongPollParsedEvent.MessageRestored(message = message) LongPollParsedEvent.MessageRestored(message = message)
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners -> listenersMap[LongPollEvent.MESSAGE_RESTORED]
listeners.map { vkEventCallback -> ?.forEach { it.onEvent(eventToSend) }
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageRestored>)
?.onEvent(eventToSend)
}
}
}
}
}
else -> Unit
} }
} }
eventsToSend.forEach { eventToSend -> MessageFlags.UNREAD -> Unit
listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners -> MessageFlags.OUTGOING -> Unit
listeners.map { vkEventCallback -> MessageFlags.AUDIO_LISTENED -> Unit
vkEventCallback.onEvent(eventToSend) MessageFlags.FROM_GROUP_CHAT -> Unit
} MessageFlags.CANCEL_SPAM -> Unit
} MessageFlags.DELETED_FOR_ALL -> Unit
} MessageFlags.DO_NOT_SHOW_NOTIFICATION -> Unit
MessageFlags.MESSAGE_WITH_REPLY -> Unit
MessageFlags.REACTION -> Unit
} }
} }
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) { listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.forEach { listener ->
Log.d("LongPollUpdatesParser", "$eventType: $event") eventsToSend.forEach { listener.onEvent(it) }
}
continuation.resume(eventsToSend)
}
}
private suspend fun parseMessageNew(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
logger.debug(this::class, "parseMessageNew(): $eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val peerId = event[4].asLong() val peerId = event[4].asLong()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
val message = val message = async { loadMessage(peerId = peerId, cmId = cmId) }.await()
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
val convo = val convo =
async { async {
@@ -280,88 +263,88 @@ class LongPollUpdatesParser(
) )
}.await() }.await()
message?.let { if (message != null) {
listenersMap[LongPollEvent.MESSAGE_NEW]?.let { val event = LongPollParsedEvent.NewMessage(
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.NewMessage>)
.onEvent(
LongPollParsedEvent.NewMessage(
message = message, message = message,
inArchive = convo?.isArchived == true inArchive = convo?.isArchived == true
// TODO: 03-Apr-25, Danil Nikolaev: // TODO: 03-Apr-25, Danil Nikolaev:
// load user settings about restoring chats with // load user settings about restoring chats with
// enabled notifications from archive // enabled notifications from archive
) )
)
} listenersMap[LongPollEvent.MESSAGE_NEW]?.forEach { it.onEvent(event) }
} continuation.resume(listOf(event))
} else {
continuation.resume(emptyList())
} }
} }
} }
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) { private suspend fun parseMessageEdit(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
logger.debug(this::class, "parseMessageEdit(): $eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val peerId = event[3].asLong() val peerId = event[3].asLong()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
loadMessage( val message = loadMessage(peerId = peerId, cmId = cmId)
peerId = peerId, if (message != null) {
cmId = cmId val event = LongPollParsedEvent.MessageEdited(message)
)?.let { message -> listenersMap[LongPollEvent.MESSAGE_EDITED]?.forEach { it.onEvent(event) }
listenersMap[LongPollEvent.MESSAGE_EDITED]?.let { continuation.resume(listOf(event))
it.map { vkEventCallback -> } else {
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageEdited>) continuation.resume(emptyList())
.onEvent(LongPollParsedEvent.MessageEdited(message))
}
}
} }
} }
} }
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) { private fun parseMessageReadIncoming(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseMessageReadIncoming(): $eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val cmId = event[2].asLong() val cmId = event[2].asLong()
val unreadCount = event[3].asInt() val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners -> val event = LongPollParsedEvent.IncomingMessageRead(
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.IncomingMessageRead>)
.onEvent(
LongPollParsedEvent.IncomingMessageRead(
peerId = peerId, peerId = peerId,
cmId = cmId, cmId = cmId,
unreadCount = unreadCount unreadCount = unreadCount
) )
) listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.forEach { it.onEvent(event) }
} return listOf(event)
}
} }
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) { private fun parseMessageReadOutgoing(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseMessageReadOutgoing(): $eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val cmId = event[2].asLong() val cmId = event[2].asLong()
val unreadCount = event[3].asInt() val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners -> val event = LongPollParsedEvent.OutgoingMessageRead(
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.OutgoingMessageRead>)
.onEvent(
LongPollParsedEvent.OutgoingMessageRead(
peerId = peerId, peerId = peerId,
cmId = cmId, cmId = cmId,
unreadCount = unreadCount unreadCount = unreadCount
) )
)
} listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.forEach { it.onEvent(event) }
} return listOf(event)
} }
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) { private suspend fun parseChatClearFlags(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
logger.debug(this::class, "parseChatClearFlags(): $eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
@@ -391,32 +374,37 @@ class LongPollUpdatesParser(
) )
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners -> listenersMap[LongPollEvent.CHAT_ARCHIVED]?.forEach { it.onEvent(eventToSend) }
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.ChatArchived>)
?.onEvent(eventToSend)
} }
ConvoFlags.DISABLE_PUSH -> Unit
ConvoFlags.DISABLE_SOUND -> Unit
ConvoFlags.INCOMING_CHAT_REQUEST -> Unit
ConvoFlags.DECLINED_CHAT_REQUEST -> Unit
ConvoFlags.MENTION -> Unit
ConvoFlags.HIDE_CHAT_FROM_SEARCH -> Unit
ConvoFlags.BUSINESS_CHAT -> Unit
ConvoFlags.MARKED_MESSAGE -> Unit
ConvoFlags.DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE -> Unit
ConvoFlags.DO_NOT_NOTIFY_ALL_MENTIONS -> Unit
ConvoFlags.MARKED_AS_UNREAD -> Unit
ConvoFlags.CALL_IN_PROGRESS -> Unit
} }
} }
else -> Unit listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.forEach { listener ->
eventsToSend.forEach { listener.onEvent(it) }
}
continuation.resume(eventsToSend)
} }
} }
eventsToSend.forEach { eventToSend -> private suspend fun parseChatSetFlags(
listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.let { listeners -> eventType: ApiEvent,
listeners.map { vkEventCallback -> event: List<Any>
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent( ): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
eventToSend logger.debug(this::class, "parseChatSetFlags(): $eventType: $event")
)
}
}
}
}
}
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
@@ -446,89 +434,88 @@ class LongPollUpdatesParser(
) )
eventsToSend += eventToSend eventsToSend += eventToSend
listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners -> listenersMap[LongPollEvent.CHAT_ARCHIVED]?.forEach { it.onEvent(eventToSend) }
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.ChatArchived>)
?.onEvent(eventToSend)
} }
ConvoFlags.DISABLE_PUSH -> Unit
ConvoFlags.DISABLE_SOUND -> Unit
ConvoFlags.INCOMING_CHAT_REQUEST -> Unit
ConvoFlags.DECLINED_CHAT_REQUEST -> Unit
ConvoFlags.MENTION -> Unit
ConvoFlags.HIDE_CHAT_FROM_SEARCH -> Unit
ConvoFlags.BUSINESS_CHAT -> Unit
ConvoFlags.MARKED_MESSAGE -> Unit
ConvoFlags.DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE -> Unit
ConvoFlags.DO_NOT_NOTIFY_ALL_MENTIONS -> Unit
ConvoFlags.MARKED_AS_UNREAD -> Unit
ConvoFlags.CALL_IN_PROGRESS -> Unit
} }
} }
else -> Unit listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.forEach { listener ->
eventsToSend.forEach { listener.onEvent(it) }
}
continuation.resume(eventsToSend)
} }
} }
eventsToSend.forEach { eventToSend -> private fun parseMessagesDeleted(
listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.let { listeners -> eventType: ApiEvent,
listeners.map { vkEventCallback -> event: List<Any>
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent( ): List<LongPollParsedEvent> {
eventToSend logger.debug(this::class, "parseMessagesDeleted(): $eventType: $event")
)
}
}
}
}
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val cmId = event[2].asLong() val cmId = event[2].asLong()
listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners -> val event = LongPollParsedEvent.ChatCleared(
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatCleared>)
.onEvent(
LongPollParsedEvent.ChatCleared(
peerId = peerId, peerId = peerId,
toCmId = cmId toCmId = cmId
) )
) listenersMap[LongPollEvent.CHAT_CLEARED]?.forEach { it.onEvent(event) }
} return listOf(event)
}
} }
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) { private fun parseChatMajorChanged(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseChatMajorChanged(): $eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val majorId = event[2].asInt() val majorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners -> val event = LongPollParsedEvent.ChatMajorChanged(
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMajorChanged>)
.onEvent(
LongPollParsedEvent.ChatMajorChanged(
peerId = peerId, peerId = peerId,
majorId = majorId, majorId = majorId,
) )
) listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.forEach { it.onEvent(event) }
} return listOf(event)
}
} }
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) { private fun parseChatMinorChanged(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseChatMinorChanged(): $eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val minorId = event[2].asInt() val minorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners -> val event = LongPollParsedEvent.ChatMinorChanged(
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMinorChanged>)
.onEvent(
LongPollParsedEvent.ChatMinorChanged(
peerId = peerId, peerId = peerId,
minorId = minorId, minorId = minorId,
) )
) listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.forEach { it.onEvent(event) }
} return listOf(event)
}
} }
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) { private fun parseInteraction(
Log.d("LongPollUpdatesParser", "$eventType: $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseInteraction(): $eventType: $event")
val interactionType = when (eventType) { val interactionType = when (eventType) {
ApiEvent.TYPING -> InteractionType.Typing ApiEvent.TYPING -> InteractionType.Typing
@@ -536,7 +523,7 @@ class LongPollUpdatesParser(
ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo
ApiEvent.VIDEO_UPLOADING -> InteractionType.Video ApiEvent.VIDEO_UPLOADING -> InteractionType.Video
ApiEvent.FILE_UPLOADING -> InteractionType.File ApiEvent.FILE_UPLOADING -> InteractionType.File
else -> return else -> return emptyList()
} }
val longPollEvent: LongPollEvent = when (eventType) { val longPollEvent: LongPollEvent = when (eventType) {
@@ -545,7 +532,6 @@ class LongPollUpdatesParser(
ApiEvent.PHOTO_UPLOADING -> LongPollEvent.PHOTO_UPLOADING ApiEvent.PHOTO_UPLOADING -> LongPollEvent.PHOTO_UPLOADING
ApiEvent.VIDEO_UPLOADING -> LongPollEvent.VIDEO_UPLOADING ApiEvent.VIDEO_UPLOADING -> LongPollEvent.VIDEO_UPLOADING
ApiEvent.FILE_UPLOADING -> LongPollEvent.FILE_UPLOADING ApiEvent.FILE_UPLOADING -> LongPollEvent.FILE_UPLOADING
else -> return
} }
val peerId = event[1].asLong() val peerId = event[1].asLong()
@@ -554,26 +540,25 @@ class LongPollUpdatesParser(
val timestamp = event[4].asInt() val timestamp = event[4].asInt()
// if userIds contains only account's id, then we don't need to show our status // if userIds contains only account's id, then we don't need to show our status
if (userIds.isEmpty()) return if (userIds.isEmpty()) return emptyList()
listenersMap[longPollEvent]?.let { listeners -> val event = LongPollParsedEvent.Interaction(
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.Interaction>)
.onEvent(
LongPollParsedEvent.Interaction(
interactionType = interactionType, interactionType = interactionType,
peerId = peerId, peerId = peerId,
userIds = userIds, userIds = userIds,
totalCount = totalCount, totalCount = totalCount,
timestamp = timestamp timestamp = timestamp
) )
)
} listenersMap[longPollEvent]?.forEach { it.onEvent(event) }
} return listOf(event)
} }
private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) { private fun parseUnreadCounterUpdate(
Log.d("LongPollUpdatesParser", "$eventType $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
logger.debug(this::class, "parseUnreadCounterUpdate(): $eventType: $event")
val unreadCount = event[1].asInt() val unreadCount = event[1].asInt()
val unreadUnmutedCount = event[2].asInt() val unreadUnmutedCount = event[2].asInt()
@@ -583,11 +568,7 @@ class LongPollUpdatesParser(
val archiveUnreadUnmutedCount = event[8].asInt() val archiveUnreadUnmutedCount = event[8].asInt()
val archiveMentionsCount = event[9].asInt() val archiveMentionsCount = event[9].asInt()
listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.let { listeners -> val event = LongPollParsedEvent.UnreadCounter(
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.UnreadCounter>)
.onEvent(
LongPollParsedEvent.UnreadCounter(
unread = unreadCount, unread = unreadCount,
unreadUnmuted = unreadUnmutedCount, unreadUnmuted = unreadUnmutedCount,
showOnlyMuted = showOnlyMuted, showOnlyMuted = showOnlyMuted,
@@ -596,45 +577,48 @@ class LongPollUpdatesParser(
archiveUnmuted = archiveUnreadUnmutedCount, archiveUnmuted = archiveUnreadUnmutedCount,
archiveMentions = archiveMentionsCount archiveMentions = archiveMentionsCount
) )
) listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.forEach { it.onEvent(event) }
} return listOf(event)
}
} }
private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) { private suspend fun parseMessageUpdated(
Log.d("LongPollUpdatesParser", "$eventType $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
logger.debug(this::class, "parseMessageUpdated(): $eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val peerId = event[4].asLong() val peerId = event[4].asLong()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
loadMessage( val message = loadMessage(peerId = peerId, cmId = cmId)
peerId = peerId,
cmId = cmId if (message != null) {
)?.let { message -> val event = LongPollParsedEvent.MessageUpdated(message)
listenersMap[LongPollEvent.MESSAGE_UPDATED]?.let { listenersMap[LongPollEvent.MESSAGE_UPDATED]?.forEach { it.onEvent(event) }
it.map { vkEventCallback -> continuation.resume(listOf(event))
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageUpdated>) } else {
.onEvent(LongPollParsedEvent.MessageUpdated(message)) continuation.resume(emptyList())
}
}
} }
} }
} }
private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) { private suspend fun parseMessageCacheClear(
Log.d("LongPollUpdatesParser", "$eventType $event") eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
logger.debug(this::class, "parseMessageCacheClear(): $eventType: $event")
val messageId = event[1].asLong() val messageId = event[1].asLong()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
loadMessage(messageId = messageId)?.let { message -> val message = loadMessage(messageId = messageId)
listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.let { if (message != null) {
it.map { vkEventCallback -> val event = LongPollParsedEvent.MessageCacheClear(message)
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageCacheClear>) listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.forEach { it.onEvent(event) }
.onEvent(LongPollParsedEvent.MessageCacheClear(message)) continuation.resume(listOf(event))
} } else {
} continuation.resume(emptyList())
} }
} }
} }
@@ -643,7 +627,7 @@ class LongPollUpdatesParser(
peerId: Long? = null, peerId: Long? = null,
cmId: Long? = null, cmId: Long? = null,
messageId: Long? = null messageId: Long? = null
): VkMessage? = suspendCoroutine { continuation -> ): VkMessage? = suspendCancellableCoroutine { continuation ->
require((peerId != null && cmId != null) || messageId != null) require((peerId != null && cmId != null) || messageId != null)
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
@@ -657,7 +641,7 @@ class LongPollUpdatesParser(
).listenValue(this) { state -> ).listenValue(this) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
Log.e("LongPollUpdatesParser", "loadMessage: error: $error") logger.error(this::class, "loadMessage(): ERROR: $error")
continuation.resume(null) continuation.resume(null)
}, },
success = { response -> success = { response ->
@@ -677,7 +661,7 @@ class LongPollUpdatesParser(
peerId: Long, peerId: Long,
extended: Boolean = false, extended: Boolean = false,
fields: String? = null fields: String? = null
): VkConvo? = suspendCoroutine { continuation -> ): VkConvo? = suspendCancellableCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
convoUseCase.getById( convoUseCase.getById(
peerIds = listOf(peerId), peerIds = listOf(peerId),
@@ -686,7 +670,7 @@ class LongPollUpdatesParser(
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
Log.e("LongPollUpdatesParser", "loadConvo: error: $error") logger.error(this::class, "loadConvo(): ERROR: $error")
continuation.resume(null) continuation.resume(null)
}, },
success = { response -> success = { response ->
+1
View File
@@ -0,0 +1 @@
/build
+11
View File
@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.fast.android.library)
}
android {
namespace = "dev.meloda.fast.logger"
}
dependencies {
implementation(libs.koin.android)
}
@@ -0,0 +1,17 @@
package dev.meloda.fast.logger;
enum class FastLogLevel {
VERBOSE,
DEBUG,
INFO,
WARNING,
ERROR,
ASSERT;
companion object {
fun parse(value: Int): FastLogLevel {
if (value !in 0..5) throw IllegalArgumentException("Unknown LogLevel value $value")
return entries.first { it.ordinal == value }
}
}
}
@@ -0,0 +1,108 @@
package dev.meloda.fast.logger
import android.util.Log
import kotlin.reflect.KClass
class FastLogger {
companion object {
@Volatile
private lateinit var instance: FastLogger
fun setInstance(logger: FastLogger) {
if (::instance.isInitialized) {
throw IllegalStateException("FastLogger has already been initialized.")
}
instance = logger
}
fun getInstance(): FastLogger {
if (!::instance.isInitialized) {
throw UninitializedPropertyAccessException("FastLogger is not initialized.")
}
return instance
}
}
private var logLevel: FastLogLevel = FastLogLevel.ERROR
fun setLogLevel(logLevel: FastLogLevel) {
Log.v(this::class.java.simpleName, "Set LogLevel from ${this.logLevel} to $logLevel")
this.logLevel = logLevel
}
fun verbose(clazz: Class<*>, message: String, throwable: Throwable? = null) {
verbose(clazz.simpleName, message, throwable)
}
fun verbose(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.VERBOSE)) {
Log.v(tag, message, throwable)
}
}
fun debug(clazz: KClass<*>, message: String, throwable: Throwable? = null) {
debug(clazz.java, message, throwable)
}
fun debug(clazz: Class<*>, message: String, throwable: Throwable? = null) {
debug(clazz.simpleName, message, throwable)
}
fun debug(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.DEBUG)) {
Log.d(tag, message, throwable)
}
}
fun info(clazz: KClass<*>, message: String, throwable: Throwable? = null) {
info(clazz.java, message, throwable)
}
fun info(clazz: Class<*>, message: String, throwable: Throwable? = null) {
info(clazz.simpleName, message, throwable)
}
fun info(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.INFO)) {
Log.i(tag, message, throwable)
}
}
fun warning(clazz: Class<*>, message: String, throwable: Throwable? = null) {
warning(clazz.simpleName, message, throwable)
}
fun warning(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.WARNING)) {
Log.w(tag, message, throwable)
}
}
fun error(clazz: KClass<*>, message: String, throwable: Throwable? = null) {
error(clazz.java, message, throwable)
}
fun error(clazz: Class<*>, message: String, throwable: Throwable? = null) {
error(clazz.simpleName, message, throwable)
}
fun error(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.ERROR)) {
Log.e(tag, message, throwable)
}
}
fun assert(clazz: Class<*>, message: String, throwable: Throwable? = null) {
assert(clazz.simpleName, message, throwable)
}
fun assert(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.ASSERT)) {
Log.wtf(tag, message, throwable)
}
}
private fun shouldLog(level: FastLogLevel): Boolean = level.ordinal >= logLevel.ordinal
}
@@ -0,0 +1,8 @@
package dev.meloda.fast.logger
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val loggerModule = module {
singleOf(::FastLogger)
}
@@ -1,7 +1,5 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import android.util.Log
enum class AttachmentType(var value: String) { enum class AttachmentType(var value: String) {
UNKNOWN("unknown"), UNKNOWN("unknown"),
PHOTO("photo"), PHOTO("photo"),
@@ -42,10 +40,6 @@ enum class AttachmentType(var value: String) {
it.value == value it.value == value
} ?: UNKNOWN } ?: UNKNOWN
if (parsedValue == UNKNOWN) {
Log.e("AttachmentType", "Unknown attachment type: $value")
}
return parsedValue return parsedValue
} }
} }
@@ -117,5 +117,6 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
pinnedAt = pinnedAt, pinnedAt = pinnedAt,
isPinned = isPinned == true, isPinned = isPinned == true,
formatData = formatData?.asDomain(), formatData = formatData?.asDomain(),
isSpam = false isSpam = false,
isDeleted = false
) )
@@ -56,5 +56,6 @@ data class VkPinnedMessageData(
isPinned = true, isPinned = true,
isSpam = false, isSpam = false,
formatData = null, formatData = null,
isDeleted = false
) )
} }
@@ -36,6 +36,8 @@ data class VkMessage(
val group: VkGroupDomain?, val group: VkGroupDomain?,
val actionUser: VkUser?, val actionUser: VkUser?,
val actionGroup: VkGroupDomain?, val actionGroup: VkGroupDomain?,
val isDeleted: Boolean
) { ) {
fun isPeerChat() = peerId > 2_000_000_000 fun isPeerChat() = peerId > 2_000_000_000
@@ -111,7 +113,7 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
actionCmId = actionCmId, actionCmId = actionCmId,
actionMessage = actionMessage, actionMessage = actionMessage,
updateTime = updateTime, updateTime = updateTime,
important = isImportant, isImportant = isImportant,
forwardIds = forwards.orEmpty().map(VkMessage::id), forwardIds = forwards.orEmpty().map(VkMessage::id),
// TODO: 05/05/2024, Danil Nikolaev: save attachments // TODO: 05/05/2024, Danil Nikolaev: save attachments
attachments = emptyList(), attachments = emptyList(),
@@ -119,4 +121,6 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
geoType = geoType, geoType = geoType,
pinnedAt = pinnedAt, pinnedAt = pinnedAt,
isPinned = isPinned, isPinned = isPinned,
isDeleted = isDeleted,
isSpam = isSpam
) )
@@ -21,13 +21,15 @@ data class VkMessageEntity(
val actionCmId: Long?, val actionCmId: Long?,
val actionMessage: String?, val actionMessage: String?,
val updateTime: Int?, val updateTime: Int?,
val important: Boolean, val isImportant: Boolean,
val forwardIds: List<Long>?, val forwardIds: List<Long>?,
val attachments: List<String>?, // TODO: 01/05/2024, Danil Nikolaev: how to store??? val attachments: List<String>?, // TODO: 01/05/2024, Danil Nikolaev: how to store???
val replyMessageId: Long?, val replyMessageId: Long?,
val geoType: String?, val geoType: String?,
val pinnedAt: Int?, val pinnedAt: Int?,
val isPinned: Boolean val isPinned: Boolean,
val isDeleted: Boolean,
val isSpam: Boolean
) )
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
@@ -45,7 +47,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
actionCmId = actionCmId, actionCmId = actionCmId,
actionMessage = actionMessage, actionMessage = actionMessage,
updateTime = updateTime, updateTime = updateTime,
isImportant = important, isImportant = isImportant,
forwards = emptyList(),//forwards.orEmpty().map(VkMessageEntity::asExternalModel), forwards = emptyList(),//forwards.orEmpty().map(VkMessageEntity::asExternalModel),
// TODO: 05/05/2024, Danil Nikolaev: restore attachments // TODO: 05/05/2024, Danil Nikolaev: restore attachments
attachments = attachments.orEmpty().map { VkUnknownAttachment }, attachments = attachments.orEmpty().map { VkUnknownAttachment },
@@ -59,4 +61,5 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
isPinned = isPinned, isPinned = isPinned,
isSpam = false, isSpam = false,
formatData = null, formatData = null,
isDeleted = isDeleted
) )
+1
View File
@@ -15,6 +15,7 @@ dependencies {
api(projects.core.common) api(projects.core.common)
api(projects.core.model) api(projects.core.model)
api(projects.core.datastore) api(projects.core.datastore)
api(projects.core.logger)
implementation(libs.moshi.kotlin) implementation(libs.moshi.kotlin)
implementation(libs.koin.android) implementation(libs.koin.android)
@@ -1,10 +1,10 @@
package dev.meloda.fast.network package dev.meloda.fast.network
import android.util.Log
import com.slack.eithernet.ApiException import com.slack.eithernet.ApiException
import com.slack.eithernet.errorType import com.slack.eithernet.errorType
import com.slack.eithernet.toType import com.slack.eithernet.toType
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import dev.meloda.fast.logger.FastLogger
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Converter import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
@@ -16,7 +16,10 @@ import java.lang.reflect.Type
* *
* допускает Unit как SuccessType в случае невозможности каста ответа в ErrorType * допускает Unit как SuccessType в случае невозможности каста ответа в ErrorType
*/ */
class ResponseConverterFactory(private val converter: JsonConverter) : Converter.Factory() { class ResponseConverterFactory(
private val converter: JsonConverter,
private val logger: FastLogger
) : Converter.Factory() {
override fun responseBodyConverter( override fun responseBodyConverter(
type: Type, type: Type,
@@ -29,6 +32,7 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
successType = type, successType = type,
errorRaw = errorRaw, errorRaw = errorRaw,
converter = converter, converter = converter,
logger = logger
) )
} }
@@ -36,6 +40,7 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
private val successType: Type, private val successType: Type,
private val errorRaw: Class<*>, private val errorRaw: Class<*>,
private val converter: JsonConverter, private val converter: JsonConverter,
private val logger: FastLogger
) : Converter<ResponseBody, Any?> { ) : Converter<ResponseBody, Any?> {
override fun convert(value: ResponseBody): Any? { override fun convert(value: ResponseBody): Any? {
val string = value.string() val string = value.string()
@@ -53,7 +58,7 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
}, },
onFailure = { failure -> onFailure = { failure ->
if (failure is JsonDataException) { if (failure is JsonDataException) {
Log.d("ResponseBodyConverter", "convertJsonDataException: $failure") logger.error(this::class, "convert(): ERROR", failure)
throw ApiException( throw ApiException(
RestApiError( RestApiError(
errorCode = -1, errorCode = -1,
@@ -68,10 +73,11 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
converter.fromJson(errorRaw, string) converter.fromJson(errorRaw, string)
}.fold( }.fold(
onSuccess = { errorModel -> onSuccess = { errorModel ->
Log.d("ResponseBodyConverter", "convert: $errorModel") logger.debug(this::class, "convert(): errorModel: $errorModel")
throw ApiException(errorModel) throw ApiException(errorModel)
}, },
onFailure = { exception -> onFailure = { exception ->
logger.error(this::class, "convert(): INNER: ERROR", exception)
if (!isUnit) { if (!isUnit) {
throw exception throw exception
} else { } else {
@@ -1,9 +1,9 @@
package dev.meloda.fast.network.interceptor package dev.meloda.fast.network.interceptor
import android.util.Log
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.CaptchaTokenResult import dev.meloda.fast.datastore.CaptchaTokenResult
import dev.meloda.fast.logger.FastLogger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -14,11 +14,7 @@ import org.json.JSONObject
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
class Error14HandlingInterceptor( class Error14HandlingInterceptor(private val logger: FastLogger) : Interceptor {
// private val domains: Set<String> = emptySet(),
) : Interceptor {
private val cookie = AtomicReference<String?>(null)
private companion object { private companion object {
private const val CAPTCHA_ERROR_CODE = 14 private const val CAPTCHA_ERROR_CODE = 14
@@ -26,6 +22,8 @@ class Error14HandlingInterceptor(
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
} }
private val cookie = AtomicReference<String?>(null)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().withCookie() val request = chain.request().withCookie()
val response = chain.proceed(request) val response = chain.proceed(request)
@@ -41,23 +39,23 @@ class Error14HandlingInterceptor(
executor.submit { executor.submit {
AppSettings.setCaptchaRedirectUri(redirectUri) AppSettings.setCaptchaRedirectUri(redirectUri)
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $redirectUri") logger.debug(this::class, "passCaptchaAndGetToken: $redirectUri")
var job: Job? = null var job: Job? = null
job = AppSettings.getCaptchaResultFlow() job = AppSettings.getCaptchaResultFlow()
.listenValue(CoroutineScope(Dispatchers.IO)) { .listenValue(CoroutineScope(Dispatchers.IO)) {
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $it") logger.debug(this::class, "passCaptchaAndGetToken: $it")
if (it != CaptchaTokenResult.Initial) { if (it != CaptchaTokenResult.Initial) {
synchronized(tokenResult) { synchronized(tokenResult) {
Log.d( logger.debug(
"Error14Interceptor", this::class,
"passCaptchaAndGetToken: SYNCHRONIZED: $it" "passCaptchaAndGetToken: SYNCHRONIZED: $it"
) )
tokenResult.set(wrapResult(it)) tokenResult.set(wrapResult(it))
tokenResult.notifyAll() tokenResult.notifyAll()
job?.cancel() job?.cancel()
Log.d( logger.debug(
"Error14Interceptor", this::class,
"passCaptchaAndGetToken: NULL RESULT" "passCaptchaAndGetToken: NULL RESULT"
) )
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial) AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
@@ -71,7 +69,7 @@ class Error14HandlingInterceptor(
tokenResult.wait() tokenResult.wait()
} }
Log.d("Error14Interceptor", "passCaptchaAndGetToken: GET VALUE") logger.debug(this::class, "passCaptchaAndGetToken: GET VALUE")
tokenResult.get().getOrThrow() tokenResult.get().getOrThrow()
} }
} }
+1
View File
@@ -12,6 +12,7 @@ android {
dependencies { dependencies {
api(projects.core.common) api(projects.core.common)
api(projects.core.model) api(projects.core.model)
api(projects.core.logger)
implementation(projects.core.presentation) implementation(projects.core.presentation)
implementation(libs.haze) implementation(libs.haze)
@@ -0,0 +1,6 @@
package dev.meloda.fast.ui.common
import androidx.compose.runtime.compositionLocalOf
import dev.meloda.fast.logger.FastLogger
val LocalLogger = compositionLocalOf { FastLogger.getInstance() }
@@ -1,7 +1,6 @@
package dev.meloda.fast.auth.captcha.presentation package dev.meloda.fast.auth.captcha.presentation
import android.graphics.Bitmap import android.graphics.Bitmap
import android.util.Log
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
@@ -32,7 +31,9 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalLogger
import dev.meloda.fast.ui.components.ActionInvokeDismiss import dev.meloda.fast.ui.components.ActionInvokeDismiss
import dev.meloda.fast.ui.components.FullScreenDialog import dev.meloda.fast.ui.components.FullScreenDialog
import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.components.MaterialDialog
@@ -46,6 +47,8 @@ fun CaptchaScreen(
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onResult: (String) -> Unit = {} onResult: (String) -> Unit = {}
) { ) {
val logger = LocalLogger.current
if (captchaRedirectUri != null) { if (captchaRedirectUri != null) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
@@ -114,7 +117,10 @@ fun CaptchaScreen(
view: WebView?, view: WebView?,
request: WebResourceRequest? request: WebResourceRequest?
): Boolean { ): Boolean {
Log.i(TAG, "shouldOverrideUrlLoading: $request") logger.info(
"CaptchaScreen",
"WebViewClient(): shouldOverrideUrlLoading(): request: $request"
)
return false return false
} }
@@ -176,19 +182,18 @@ fun CaptchaScreen(
class WebCaptchaListener( class WebCaptchaListener(
private val onSuccessTokenReceived: (String) -> Unit, private val onSuccessTokenReceived: (String) -> Unit,
private val onCloseRequested: (String) -> Unit private val onCloseRequested: (String) -> Unit,
private val logger: FastLogger
) { ) {
private val tag = "WebCaptchaListener"
@JavascriptInterface @JavascriptInterface
fun VKCaptchaGetResult(arg: String) { fun VKCaptchaGetResult(arg: String) {
onSuccessTokenReceived(arg) onSuccessTokenReceived(arg)
Log.i(tag, "VKCaptchaGetResult($arg)") logger.info(this::class, "VKCaptchaGetResult(): arg: $arg")
} }
@JavascriptInterface @JavascriptInterface
fun VKCaptchaCloseCaptcha(arg: String) { fun VKCaptchaCloseCaptcha(arg: String) {
onCloseRequested(arg) onCloseRequested(arg)
Log.i(tag, "VKCaptchaCloseCaptcha($arg)") logger.info(this::class, "VKCaptchaCloseCaptcha(): arg: $arg")
} }
} }
@@ -2,7 +2,6 @@ package dev.meloda.fast.auth.login
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dev.meloda.fast.auth.login.model.CaptchaArguments import dev.meloda.fast.auth.login.model.CaptchaArguments
@@ -28,6 +27,7 @@ import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.domain.OAuthUseCase
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.database.AccountEntity import dev.meloda.fast.model.database.AccountEntity
import dev.meloda.fast.network.OAuthErrorDomain import dev.meloda.fast.network.OAuthErrorDomain
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@@ -48,7 +48,8 @@ class LoginViewModel(
private val accountsRepository: AccountsRepository, private val accountsRepository: AccountsRepository,
private val loginValidator: LoginValidator, private val loginValidator: LoginValidator,
private val longPollController: LongPollController, private val longPollController: LongPollController,
private val userSettings: UserSettings private val userSettings: UserSettings,
private val logger: FastLogger
) : ViewModel() { ) : ViewModel() {
private val _screenState = MutableStateFlow(LoginScreenState.EMPTY) private val _screenState = MutableStateFlow(LoginScreenState.EMPTY)
val screenState = _screenState.asStateFlow() val screenState = _screenState.asStateFlow()
@@ -189,7 +190,7 @@ class LoginViewModel(
).listenValue(viewModelScope) { state -> ).listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
Log.d("LoginViewModelImpl", "login: error: $error") logger.error(this::class, "getSilentToken(): ERROR: $error")
_screenState.updateValue { copy(isLoading = false) } _screenState.updateValue { copy(isLoading = false) }
@@ -1,6 +1,5 @@
package dev.meloda.fast.chatmaterials.util package dev.meloda.fast.chatmaterials.util
import android.util.Log
import dev.meloda.fast.chatmaterials.model.UiChatMaterial import dev.meloda.fast.chatmaterials.model.UiChatMaterial
import dev.meloda.fast.common.util.AndroidUtils import dev.meloda.fast.common.util.AndroidUtils
import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.data.AttachmentType
@@ -135,8 +134,5 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
) )
} }
else -> { else -> null
Log.w("ChatMaterialMapper", "Unsupported type: $type")
null
}
} }
@@ -613,6 +613,7 @@ class ConvosViewModel(
if (convoIndex == null) { // диалога нет в списке if (convoIndex == null) { // диалога нет в списке
// pizdets // pizdets
} else { } else {
// TODO: 30.05.2026, Danil Nikolaev: reimplement
newConvos.removeAt(convoIndex) newConvos.removeAt(convoIndex)
replaceConvos(newConvos.sorted()) replaceConvos(newConvos.sorted())
@@ -6,7 +6,6 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@@ -685,8 +684,6 @@ class MessagesHistoryViewModelImpl(
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
val message = event.message val message = event.message
Log.d("MessagesHistoryViewModel", "handleNewMessage: $message")
if (message.peerId != screenState.value.convoId) return if (message.peerId != screenState.value.convoId) return
if (messages.value.indexOfFirstOrNull { it.id == message.id } != null) return if (messages.value.indexOfFirstOrNull { it.id == message.id } != null) return
@@ -835,8 +832,6 @@ class MessagesHistoryViewModelImpl(
} }
private fun loadConvo() { private fun loadConvo() {
Log.d("MessagesHistoryViewModelImpl", "loadConvo()")
loadConvosByIdUseCase( loadConvosByIdUseCase(
peerIds = listOf(screenState.value.convoId), peerIds = listOf(screenState.value.convoId),
extended = true, extended = true,
@@ -904,8 +899,6 @@ class MessagesHistoryViewModelImpl(
} }
private fun loadMessagesHistory(offset: Int = currentOffset.value) { private fun loadMessagesHistory(offset: Int = currentOffset.value) {
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
messagesUseCase.getMessagesHistory( messagesUseCase.getMessagesHistory(
convoId = screenState.value.convoId, convoId = screenState.value.convoId,
count = MESSAGES_LOAD_COUNT, count = MESSAGES_LOAD_COUNT,
@@ -1037,6 +1030,7 @@ class MessagesHistoryViewModelImpl(
isSpam = false, isSpam = false,
pinnedAt = null, pinnedAt = null,
formatData = formatData, formatData = formatData,
isDeleted = false
) )
formatData = formatData.copy(items = emptyList()) formatData = formatData.copy(items = emptyList())
sendingMessages += newMessage sendingMessages += newMessage
@@ -1078,8 +1072,6 @@ class MessagesHistoryViewModelImpl(
state.processState( state.processState(
any = { sendingMessages.remove(newMessage) }, any = { sendingMessages.remove(newMessage) },
error = { error -> error = { error ->
Log.d("MessagesHistoryViewModelImpl", "sendMessage: ERROR: $error")
val failedId = -500_000L - failedMessages.size val failedId = -500_000L - failedMessages.size
val newFailedMessage = newMessage.copy(id = failedId) val newFailedMessage = newMessage.copy(id = failedId)
failedMessages += newFailedMessage failedMessages += newFailedMessage
@@ -1143,8 +1135,6 @@ class MessagesHistoryViewModelImpl(
) ?: return ) ?: return
// TODO: 13/03/2026, Danil Nikolaev: check if message is exact same, then do not edit // TODO: 13/03/2026, Danil Nikolaev: check if message is exact same, then do not edit
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
} }
private fun markAsImportant( private fun markAsImportant(
@@ -1,7 +1,6 @@
package dev.meloda.fast.messageshistory.presentation package dev.meloda.fast.messageshistory.presentation
import android.content.Intent import android.content.Intent
import android.util.Log
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
@@ -129,7 +128,6 @@ fun MessagesList(
when (attachment) { when (attachment) {
is VkPhotoDomain -> { is VkPhotoDomain -> {
val maxSize = attachment.getMaxSize() val maxSize = attachment.getMaxSize()
Log.d("MessagesList", "onPhotoLongClicked. Max size: ${maxSize?.url}")
} }
} }
} }
@@ -64,7 +64,6 @@ fun DynamicPreviewGrid(
val spacingPx = with(LocalDensity.current) { spacing.toPx() } val spacingPx = with(LocalDensity.current) { spacing.toPx() }
val rows = previews.chunked(3) val rows = previews.chunked(3)
Log.d("ROWS", "DynamicPreviewGrid: ${rows.size}")
Column(verticalArrangement = Arrangement.spacedBy(spacing)) { Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
rows.forEachIndexed { outerIndex, row -> rows.forEachIndexed { outerIndex, row ->
@@ -12,7 +12,7 @@ import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.LogLevel import dev.meloda.fast.common.model.NetworkLogLevel
import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString import dev.meloda.fast.common.model.parseString
@@ -497,10 +497,10 @@ class SettingsViewModel(
) )
val logLevelValues = listOf( val logLevelValues = listOf(
LogLevel.NONE to UiText.Simple("None"), NetworkLogLevel.NONE to UiText.Simple("None"),
LogLevel.BASIC to UiText.Simple("Basic"), NetworkLogLevel.BASIC to UiText.Simple("Basic"),
LogLevel.HEADERS to UiText.Simple("Headers"), NetworkLogLevel.HEADERS to UiText.Simple("Headers"),
LogLevel.BODY to UiText.Simple("Body") NetworkLogLevel.BODY to UiText.Simple("Body")
).toMap() ).toMap()
val debugNetworkLogLevel = SettingsItem.ListItem( val debugNetworkLogLevel = SettingsItem.ListItem(
@@ -509,10 +509,10 @@ class SettingsViewModel(
valueClass = Int::class, valueClass = Int::class,
defaultValue = SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL, defaultValue = SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL,
titles = logLevelValues.values.toList(), titles = logLevelValues.values.toList(),
values = logLevelValues.keys.toList().map(LogLevel::value) values = logLevelValues.keys.toList().map(NetworkLogLevel::value)
).apply { ).apply {
textProvider = TextProvider { item -> textProvider = TextProvider { item ->
val textValue = logLevelValues[LogLevel.parse(item.value)].parseString(resources) val textValue = logLevelValues[NetworkLogLevel.parse(item.value)].parseString(resources)
UiText.Simple("Current value: $textValue") UiText.Simple("Current value: $textValue")
} }
+1
View File
@@ -55,3 +55,4 @@ include(":feature:friends")
include(":feature:profile") include(":feature:profile")
include(":feature:createchat") include(":feature:createchat")
include(":core:presentation") include(":core:presentation")
include(":core:logger")